From 710dc3ca4b432b70ac150eaf2e1eae313d1edfb6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:17:23 +0100 Subject: [PATCH] Add Docker Compose support for image retention during cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support for Docker Compose applications with build: directives that create images with uuid_servicename naming pattern (e.g., app-uuid_web:commit). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Actions/Server/CleanupDocker.php | 6 ++- .../Unit/Actions/Server/CleanupDockerTest.php | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index df8ac7193..cdbe10427 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -97,8 +97,9 @@ private function buildImagePruneCommand($applicationImageRepos): string // - Are not application images (don't match app repos) // - Don't have coolify.managed=true label // Images in use by containers will fail silently with docker rmi + // Pattern matches both uuid:tag and uuid_servicename:tag (Docker Compose with build) $commands[] = "docker images --format '{{.Repository}}:{{.Tag}}' | ". - "grep -v -E '^({$excludePatterns}):' | ". + "grep -v -E '^({$excludePatterns})[_:].+' | ". "grep -v '' | ". "xargs -r -I {} sh -c 'docker inspect --format \"{{{{index .Config.Labels \\\"coolify.managed\\\"}}}}\" \"{}\" 2>/dev/null | grep -q true || docker rmi \"{}\" 2>/dev/null' || true"; } @@ -124,7 +125,8 @@ private function cleanupApplicationImages(Server $server, $applications = null): $currentTag = trim($currentTag ?? ''); // List all images for this application with their creation timestamps - $listCommand = "docker images --format '{{.Repository}}:{{.Tag}}#{{.CreatedAt}}' --filter reference='{$imageRepository}' 2>/dev/null || true"; + // Use wildcard to match both uuid:tag and uuid_servicename:tag (Docker Compose with build) + $listCommand = "docker images --format '{{.Repository}}:{{.Tag}}#{{.CreatedAt}}' --filter reference='{$imageRepository}*' 2>/dev/null || true"; $output = instant_remote_process([$listCommand], $server, false); if (empty($output)) { diff --git a/tests/Unit/Actions/Server/CleanupDockerTest.php b/tests/Unit/Actions/Server/CleanupDockerTest.php index 2b28e7749..ebf73da06 100644 --- a/tests/Unit/Actions/Server/CleanupDockerTest.php +++ b/tests/Unit/Actions/Server/CleanupDockerTest.php @@ -202,3 +202,52 @@ // Skip 2, leaving 0 to delete expect($imagesToDelete)->toHaveCount(0); }); + +it('handles Docker Compose service images with uuid_servicename pattern', function () { + // Docker Compose with build: directive creates images like uuid_servicename:tag + $images = collect([ + ['repository' => 'app-uuid_web', 'tag' => 'commit1', 'created_at' => '2024-01-01 10:00:00', 'image_ref' => 'app-uuid_web:commit1'], + ['repository' => 'app-uuid_web', 'tag' => 'commit2', 'created_at' => '2024-01-02 10:00:00', 'image_ref' => 'app-uuid_web:commit2'], + ['repository' => 'app-uuid_worker', 'tag' => 'commit1', 'created_at' => '2024-01-01 10:00:00', 'image_ref' => 'app-uuid_worker:commit1'], + ['repository' => 'app-uuid_worker', 'tag' => 'commit2', 'created_at' => '2024-01-02 10:00:00', 'image_ref' => 'app-uuid_worker:commit2'], + ]); + + // All images should be categorized as regular images (not PR, not build) + $prImages = $images->filter(fn ($image) => str_starts_with($image['tag'], 'pr-')); + $regularImages = $images->filter(fn ($image) => ! str_starts_with($image['tag'], 'pr-') && ! str_ends_with($image['tag'], '-build')); + + expect($prImages)->toHaveCount(0); + expect($regularImages)->toHaveCount(4); +}); + +it('correctly excludes Docker Compose images from general prune', function () { + // Test the grep pattern that excludes application images + // Pattern should match both uuid:tag and uuid_servicename:tag + $appUuid = 'abc123def456'; + $excludePattern = preg_quote($appUuid, '/'); + + // Images that should be EXCLUDED (protected) + $protectedImages = [ + "{$appUuid}:commit1", // Standard app image + "{$appUuid}_web:commit1", // Docker Compose service + "{$appUuid}_worker:commit2", // Docker Compose service + ]; + + // Images that should be INCLUDED (deleted) + $deletableImages = [ + 'other-app:latest', + 'nginx:alpine', + 'postgres:15', + ]; + + // Test the regex pattern used in buildImagePruneCommand + $pattern = "/^({$excludePattern})[_:].+/"; + + foreach ($protectedImages as $image) { + expect(preg_match($pattern, $image))->toBe(1, "Image {$image} should be protected"); + } + + foreach ($deletableImages as $image) { + expect(preg_match($pattern, $image))->toBe(0, "Image {$image} should be deletable"); + } +});