fix(cleanup): stop loading deployment logs and unused backup executions

The stuck resources cleanup selected every column of every deployment,
including logs, and ran out of memory during app:init. Select only the
id and application_id. Also drop the unused executions property on the
backup pages and an unused execution loader.

Closes #12435

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude Opus 5.5 committed 2026-11-03 18:14:23 +01:00
1 parent bb6dfd147e
commit 9c6138fe18
9 files changed
+50 -20

No files matched your search

+2
View File
@@ -19,6 +19,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Fixed
- Finish the startup and the nightly cleanup of stuck resources on instances with many large deployment logs. Before, the cleanup loaded the logs of all deployments and stopped with "Allowed memory size exhausted", so stuck deployments, tasks, and backups were not marked as failed and the proxy configuration was not set up ([#12435](https://github.com/coollabsio/coolify/issues/12435))
- Open the database backup pages and **Settings > Backup** faster when a backup has many executions. Before, these pages loaded all executions of the backup on each request, but they did not show them
- Queue a deployment from the MCP `deploy` tool and the MCP `control` start action while a deployment of the same application is in progress, as the API and the **Deploy** button do. Before, both started at once, so two rolling updates could remove the new container of each other and stop the application. A deployment of the same commit is now skipped, and the response gives the UUID of the deployment in progress. A rolling update now also removes only the containers that ran before it started ([#12442](https://github.com/coollabsio/coolify/issues/12442))
- Do not copy the preview deployments of an application when you clone it. A clone of an application with a fixed preview URL template (for example `demo-mr-{{pr_id}}.example.com`) failed with a duplicate preview domain error and left a stopped, partial clone. The clone gets its own previews from new pull requests
- Handle an existing Cloudflare DNS record with a different IP when Coolify adds the DNS record of a new domain automatically. Coolify now replaces the record when it created the record and no other resource uses it. Otherwise it opens the **Configure DNS** dialog, where you can replace the record, and the domain shows the conflict instead of "Not checked"
@@ -70,7 +70,9 @@ class CleanupStuckedResources extends Command
echo "Error in cleaning stuck servers: {$e->getMessage()}\n";
}
try {
$applicationsDeploymentQueue = ApplicationDeploymentQueue::query()->lazyById();
$applicationsDeploymentQueue = ApplicationDeploymentQueue::query()
->select(['id', 'application_id'])
->lazyById();
foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) {
if (is_null($applicationDeploymentQueue->application)) {
echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n";
+1 -2
View File
@@ -120,8 +120,7 @@ class Emails extends Command
case 'application-deployment-success-daily':
$applications = Application::all();
foreach ($applications as $application) {
$deployments = $application->get_last_days_deployments();
if ($deployments->isEmpty()) {
if (! $application->hasDeploymentsInLastDays()) {
continue;
}
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
@@ -23,8 +23,6 @@ class Execution extends Component
public ?ScheduledDatabaseBackup $backup;
public $executions;
public $s3s;
public array $parameters = [];
@@ -73,10 +71,8 @@ class Execution extends Component
if (! $backup) {
return redirect()->route('dashboard');
}
$executions = collect($backup->executions)->sortByDesc('created_at');
$this->database = $database;
$this->backup = $backup;
$this->executions = $executions;
$this->s3s = S3Storage::query()
->where('team_id', $database->team()->id)
->where('is_usable', true)
-5
View File
@@ -28,9 +28,6 @@ class SettingsBackup extends Component
#[Locked]
public $s3s;
#[Locked]
public $executions = [];
#[Validate(['nullable'])]
public ?string $description = null;
@@ -58,7 +55,6 @@ class SettingsBackup extends Component
$this->backup->enabled = false;
$this->backup->save();
}
$this->executions = $this->backup?->executions ?? [];
}
$this->settings = $settings;
$this->is_backup_before_update_enabled = $settings->is_backup_before_update_enabled;
@@ -112,7 +108,6 @@ class SettingsBackup extends Component
$this->s3s = S3Storage::whereTeamId(0)->get();
$this->description = $this->database->description;
$this->executions = $this->backup->executions;
} catch (\Exception $e) {
return handleError($e, $this);
+2 -2
View File
@@ -1323,9 +1323,9 @@ class Application extends BaseModel
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED->value)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first($columns);
}
public function get_last_days_deployments()
public function hasDeploymentsInLastDays(int $days = 7): bool
{
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get();
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays($days))->exists();
}
/**
-5
View File
@@ -87,11 +87,6 @@ class ScheduledDatabaseBackup extends BaseModel
return $this->belongsToMany(S3Storage::class, 'scheduled_database_backup_s3_storage')->withTimestamps();
}
public function get_last_days_backup_status($days = 7)
{
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
}
public function executionsPaginated(int $skip = 0, int $take = 10)
{
$executions = $this->hasMany(ScheduledDatabaseBackupExecution::class)->with('s3Replicas.s3')->orderBy('created_at', 'desc');
@@ -1,6 +1,7 @@
<?php
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
@@ -9,6 +10,7 @@ use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
@@ -42,3 +44,43 @@ it('deletes scheduled tasks whose configured owner no longer exists', function (
expect($validTask->fresh())->not->toBeNull()
->and($orphanedTask->fresh())->toBeNull();
});
it('deletes orphaned deployments without loading their logs', function () {
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
Queue::fake();
$team = Team::factory()->create();
$server = Server::factory()->create(['team_id' => $team->id]);
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
$project = Project::factory()->create(['team_id' => $team->id]);
$environment = $project->environments()->first()
?? Environment::factory()->create(['project_id' => $project->id]);
$application = Application::factory()->create([
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
$validDeployment = ApplicationDeploymentQueue::create([
'application_id' => $application->id,
'deployment_uuid' => 'valid-deployment',
'server_id' => $server->id,
'logs' => str_repeat('x', 1000),
]);
$orphanedDeployment = ApplicationDeploymentQueue::create([
'application_id' => PHP_INT_MAX,
'deployment_uuid' => 'orphaned-deployment',
'server_id' => $server->id,
'logs' => str_repeat('x', 1000),
]);
DB::enableQueryLog();
$this->artisan('cleanup:stucked-resources')->assertSuccessful();
$deploymentSelects = collect(DB::getQueryLog())
->pluck('query')
->filter(fn (string $query) => str_starts_with($query, 'select') && str_contains($query, 'from "application_deployment_queues"'));
expect($validDeployment->fresh())->not->toBeNull()
->and($orphanedDeployment->fresh())->toBeNull()
->and($deploymentSelects)->not->toBeEmpty()
->and($deploymentSelects->filter(fn (string $query) => str_contains($query, '*')))->toBeEmpty();
});
@@ -136,7 +136,6 @@ test('instance backup settings render when the instance database has no backup s
Livewire::test(SettingsBackup::class)
->assertOk()
->assertSet('executions', [])
->assertSee('Backup is not configured')
->assertSee('Configure backup');
});