From d07bc9c0ffb89658675acef45ca84a7bd7923271 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:22:08 +0200 Subject: [PATCH] fix(docker): preserve resource containers during cleanup --- app/Actions/Server/CleanupDocker.php | 16 +++++++++++++- config/constants.php | 2 +- other/nightly/versions.json | 2 +- tests/Feature/DockerCleanupJobTest.php | 29 +++++++++++++++++++++++++- versions.json | 2 +- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 201f86b367..2ace4d7426 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -161,7 +161,7 @@ class CleanupDocker implements ShouldBeUnique ); $commands = [ - 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true" --filter "label!=coolify.type=database" --filter "label!=coolify.type=application" --filter "label!=coolify.type=service"', + $this->buildContainerPruneCommand(), $imagePruneCmd, 'docker builder prune -af', "docker run --rm -v {$buildxMetadataVolume}:/root/.docker/buildx -v /var/run/docker.sock:/var/run/docker.sock {$helperImageWithVersion} docker buildx prune --builder coolify-railpack -af 2>/dev/null || true", @@ -207,6 +207,20 @@ class CleanupDocker implements ShouldBeUnique return instant_remote_process([$command], $server, false, timeout: $remaining); } + /** + * Build a command that removes stopped Coolify containers, except the proxy and the + * containers of databases, applications and services. + * + * docker container prune only excludes a container that matches all of its label!= filters + * together, so we list the stopped containers and filter them by label in the shell. + */ + private function buildContainerPruneCommand(): string + { + return "docker ps -a --filter status=created --filter status=exited --filter status=dead --filter label=coolify.managed=true --format '{{.ID}} coolify.proxy={{.Label \"coolify.proxy\"}} coolify.type={{.Label \"coolify.type\"}}' | ". + "grep -v -E 'coolify\.proxy=true |coolify\.type=(database|application|service)$' | ". + "awk '{print $1}' | xargs -r docker rm"; + } + /** * Build a docker image prune command that excludes application image repositories. * diff --git a/config/constants.php b/config/constants.php index 894e2d4db6..6307bbfaeb 100644 --- a/config/constants.php +++ b/config/constants.php @@ -5,7 +5,7 @@ $sshCommandTimeout = (int) env('SSH_COMMAND_TIMEOUT', 3600); return [ 'coolify' => [ - 'version' => env('COOLIFY_VERSION') ?: '4.4.3', + 'version' => env('COOLIFY_VERSION') ?: '4.4.4', 'helper_version' => '1.0.17', 'railpack_version' => '0.23.0', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index bb76d98724..de9c6b55b4 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "4.4.3" + "version": "4.4.4" }, "nightly": { "version": "4.5-rc.1" diff --git a/tests/Feature/DockerCleanupJobTest.php b/tests/Feature/DockerCleanupJobTest.php index 31b12bb7ac..475a3a2da1 100644 --- a/tests/Feature/DockerCleanupJobTest.php +++ b/tests/Feature/DockerCleanupJobTest.php @@ -372,7 +372,7 @@ it('gives remote cleanup commands a local timeout and fails once the cleanup dea expect(fn () => CleanupDocker::run($server)) ->toThrow(RuntimeException::class, 'Docker cleanup did not finish within '.CleanupDocker::REMOTE_COMMANDS_DEADLINE.' seconds'); - $sshCommands = $commands->filter(fn (string $command) => str_contains($command, 'docker container prune') || str_contains($command, 'docker image prune'))->values(); + $sshCommands = $commands->filter(fn (string $command) => str_contains($command, 'label=coolify.managed=true') || str_contains($command, 'docker image prune'))->values(); expect(CleanupDocker::REMOTE_COMMANDS_DEADLINE)->toBeLessThan(CleanupDocker::JOB_TIMEOUT) ->and($sshCommands)->toHaveCount(2) @@ -498,3 +498,30 @@ it('scans application images with a fixed number of remote commands and keeps th expect($removal)->not->toContain($kept); } }); + +it('removes stopped Coolify containers but keeps the proxy, databases, applications and services', function () { + $command = (new ReflectionMethod(CleanupDocker::class, 'buildContainerPruneCommand'))->invoke(new CleanupDocker); + preg_match("/--format '([^']+)'/", $command, $format); + $stoppedContainers = collect([ + ['proxy', 'true', ''], + ['database', '', 'database'], + ['application', '', 'application'], + ['service', '', 'service'], + ['helper', '', ''], + ])->map(fn (array $container) => str_replace(['{{.ID}}', '{{.Label "coolify.proxy"}}', '{{.Label "coolify.type"}}'], $container, $format[1])); + + // Fake docker on PATH: `docker ps` prints the stopped containers, `docker rm` prints what it is asked to remove + $bin = sys_get_temp_dir().'/docker-cleanup-'.bin2hex(random_bytes(4)); + mkdir($bin); + file_put_contents("$bin/containers", $stoppedContainers->implode("\n")."\n"); + file_put_contents("$bin/docker", "#!/bin/sh\nif [ \"\$1\" = ps ]; then cat $bin/containers; else echo \"\$@\"; fi\n"); + chmod("$bin/docker", 0755); + + $output = shell_exec('PATH='.escapeshellarg("$bin:".getenv('PATH')).' /bin/sh -c '.escapeshellarg($command)); + + array_map('unlink', glob("$bin/*")); + rmdir($bin); + + expect($command)->toContain('--filter label=coolify.managed=true') + ->and(trim((string) $output))->toBe('rm helper'); +}); diff --git a/versions.json b/versions.json index bb76d98724..de9c6b55b4 100644 --- a/versions.json +++ b/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "4.4.3" + "version": "4.4.4" }, "nightly": { "version": "4.5-rc.1"