fix(backups): keep non-deployment helper containers during cleanup

Closes #12431
This commit is contained in:
Andras Bacsai committed 2026-11-02 13:59:29 +01:00
1 parent 31653c27d1
commit 33a6682791
2 files changed
+66 -41

No files matched your search

+27 -41
View File
@@ -37,56 +37,42 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
return 'docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image|test("(^|/)coollabsio/coolify-helper(:|@)")))\'';
}
/**
* Removes helper containers of finished deployments. Backups, imports, and file browsers also run in
* helper containers, so a container is removed only when its name is the UUID of a deployment that
* is not queued or in progress.
*/
public function handle(): void
{
try {
// Get all active deployments on this server
$activeDeployments = ApplicationDeploymentQueue::where('server_id', $this->server->id)
->whereIn('status', [
$containers = instant_remote_process_with_timeout([self::helperContainersCommand()], $this->server, false);
$helperContainers = collect(json_decode((string) $containers, true) ?: [])
->filter(fn (mixed $container): bool => is_array($container) && filled(data_get($container, 'ID')) && filled(data_get($container, 'Names')));
if ($helperContainers->isEmpty()) {
return;
}
$finishedDeploymentUuids = ApplicationDeploymentQueue::query()
->whereIn('deployment_uuid', $helperContainers->pluck('Names')->all())
->whereNotIn('status', [
ApplicationDeploymentStatus::IN_PROGRESS->value,
ApplicationDeploymentStatus::QUEUED->value,
])
->pluck('deployment_uuid')
->toArray();
->all();
\Log::info('CleanupHelperContainersJob - Active deployments', [
'server' => $this->server->name,
'active_deployment_uuids' => $activeDeployments,
]);
$containers = instant_remote_process_with_timeout([self::helperContainersCommand()], $this->server, false);
$helperContainers = collect(json_decode($containers));
if ($helperContainers->count() > 0) {
foreach ($helperContainers as $container) {
$containerId = data_get($container, 'ID');
$containerName = data_get($container, 'Names');
// Check if this container belongs to an active deployment
$isActiveDeployment = false;
foreach ($activeDeployments as $deploymentUuid) {
if (str_contains($containerName, $deploymentUuid)) {
$isActiveDeployment = true;
break;
}
}
if ($isActiveDeployment) {
\Log::info('CleanupHelperContainersJob - Skipping active deployment container', [
'container' => $containerName,
'id' => $containerId,
]);
continue;
}
\Log::info('CleanupHelperContainersJob - Removing orphaned helper container', [
'container' => $containerName,
'id' => $containerId,
]);
instant_remote_process_with_timeout(['docker container rm -f '.$containerId], $this->server, false);
foreach ($helperContainers as $container) {
if (! in_array($container['Names'], $finishedDeploymentUuids, true)) {
continue;
}
\Log::info('CleanupHelperContainersJob - Removing orphaned deployment helper container', [
'container' => $container['Names'],
'id' => $container['ID'],
]);
instant_remote_process_with_timeout(['docker container rm -f '.escapeshellarg($container['ID'])], $this->server, false);
}
} catch (\Throwable $e) {
send_internal_notification('CleanupHelperContainersJob failed with error: '.$e->getMessage());
@@ -1,10 +1,16 @@
<?php
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
@@ -24,3 +30,36 @@ it('queues one helper container cleanup per server', function () {
Queue::assertPushed(CleanupHelperContainersJob::class, 2);
});
it('removes only helper containers of finished deployments', function () {
Storage::fake('ssh-keys');
Storage::fake('ssh-mux');
config(['constants.ssh.mux_enabled' => false]);
$privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
$server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $privateKey->id]);
foreach (['finished-uuid' => ApplicationDeploymentStatus::FINISHED, 'failed-uuid' => ApplicationDeploymentStatus::FAILED, 'running-uuid' => ApplicationDeploymentStatus::IN_PROGRESS] as $uuid => $status) {
ApplicationDeploymentQueue::create(['application_id' => '1', 'deployment_uuid' => $uuid, 'server_id' => $server->id, 'status' => $status->value]);
}
$containers = collect([
'finished-uuid' => 'aaa111',
'failed-uuid' => 'bbb222',
'running-uuid' => 'ccc333',
'volume-upload-abc' => 'ddd444',
'volume-backup-abc' => 'eee555',
'backup-of-abc' => 'fff666',
])->map(fn (string $id, string $name): array => ['ID' => $id, 'Names' => $name, 'Image' => 'ghcr.io/coollabsio/coolify-helper:1.0.13'])->values();
$removed = [];
Process::fake(function (PendingProcess $process) use ($containers, &$removed) {
$script = remoteProcessScript($process);
if (preg_match("/docker container rm -f '?([a-z0-9]+)/", $script, $match) === 1) {
$removed[] = $match[1];
}
return Process::result(str_contains($script, 'docker container ps') ? $containers->toJson() : '');
});
(new CleanupHelperContainersJob($server))->handle();
expect($removed)->toBe(['aaa111', 'bbb222']);
});