From 33a6682791e4fae5af0c6fe29f3a76ee695f7a0b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:59:25 +0100 Subject: [PATCH] fix(backups): keep non-deployment helper containers during cleanup Closes #12431 --- app/Jobs/CleanupHelperContainersJob.php | 68 ++++++++----------- .../CleanupHelperContainersJobDedupTest.php | 39 +++++++++++ 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php index 368bad5eaf..2c44b57dde 100644 --- a/app/Jobs/CleanupHelperContainersJob.php +++ b/app/Jobs/CleanupHelperContainersJob.php @@ -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()); diff --git a/tests/Feature/CleanupHelperContainersJobDedupTest.php b/tests/Feature/CleanupHelperContainersJobDedupTest.php index 9fd52d4404..7a6cbde9cc 100644 --- a/tests/Feature/CleanupHelperContainersJobDedupTest.php +++ b/tests/Feature/CleanupHelperContainersJobDedupTest.php @@ -1,10 +1,16 @@ 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']); +});