Fix: Prevent coolify-helper and coolify-realtime images from being pruned
Current version of infrastructure images (coolify-helper, coolify-realtime) are now protected from deletion during docker cleanup, regardless of which registry they're pulled from (ghcr.io, docker.io, or Docker Hub implicit). Old versions continue to be cleaned up as intended.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5d38147899
commit
6ea563c6ac
2 changed files with 137 additions and 24 deletions
|
|
@ -37,9 +37,15 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
$applicationCleanupLog = $this->cleanupApplicationImages($server, $applications);
|
||||
$cleanupLog = array_merge($cleanupLog, $applicationCleanupLog);
|
||||
|
||||
// Build image prune command that excludes application images
|
||||
// This ensures we clean up non-Coolify images while preserving rollback images
|
||||
$imagePruneCmd = $this->buildImagePruneCommand($applicationImageRepos);
|
||||
// Build image prune command that excludes application images and current Coolify infrastructure images
|
||||
// This ensures we clean up non-Coolify images while preserving rollback images and current helper/realtime images
|
||||
// Note: Only the current version is protected; old versions will be cleaned up by explicit commands below
|
||||
// We pass the version strings so all registry variants are protected (ghcr.io, docker.io, no prefix)
|
||||
$imagePruneCmd = $this->buildImagePruneCommand(
|
||||
$applicationImageRepos,
|
||||
$helperImageVersion,
|
||||
$realtimeImageVersion
|
||||
);
|
||||
|
||||
$commands = [
|
||||
'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
|
||||
|
|
@ -78,33 +84,51 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
* Since docker image prune doesn't support excluding by repository name directly,
|
||||
* we use a shell script approach to delete unused images while preserving application images.
|
||||
*/
|
||||
private function buildImagePruneCommand($applicationImageRepos): string
|
||||
{
|
||||
private function buildImagePruneCommand(
|
||||
$applicationImageRepos,
|
||||
string $helperImageVersion,
|
||||
string $realtimeImageVersion
|
||||
): string {
|
||||
// Step 1: Always prune dangling images (untagged)
|
||||
$commands = ['docker image prune -f'];
|
||||
|
||||
if ($applicationImageRepos->isEmpty()) {
|
||||
// No applications, add original prune command for all unused images
|
||||
$commands[] = 'docker image prune -af --filter "label!=coolify.managed=true"';
|
||||
} else {
|
||||
// Build grep pattern to exclude application image repositories
|
||||
$excludePatterns = $applicationImageRepos->map(function ($repo) {
|
||||
// Escape special characters for grep extended regex (ERE)
|
||||
// ERE special chars: . \ + * ? [ ^ ] $ ( ) { } |
|
||||
return preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $repo);
|
||||
})->implode('|');
|
||||
// Build grep pattern to exclude application image repositories (matches repo:tag and repo_service:tag)
|
||||
$appExcludePatterns = $applicationImageRepos->map(function ($repo) {
|
||||
// Escape special characters for grep extended regex (ERE)
|
||||
// ERE special chars: . \ + * ? [ ^ ] $ ( ) { } |
|
||||
return preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $repo);
|
||||
})->implode('|');
|
||||
|
||||
// Delete unused images that:
|
||||
// - 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 '<none>' | ".
|
||||
"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";
|
||||
// Build grep pattern to exclude Coolify infrastructure images (current version only)
|
||||
// This pattern matches the image name regardless of registry prefix:
|
||||
// - ghcr.io/coollabsio/coolify-helper:1.0.12
|
||||
// - docker.io/coollabsio/coolify-helper:1.0.12
|
||||
// - coollabsio/coolify-helper:1.0.12
|
||||
// Pattern: (^|/)coollabsio/coolify-(helper|realtime):VERSION$
|
||||
$escapedHelperVersion = preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $helperImageVersion);
|
||||
$escapedRealtimeVersion = preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $realtimeImageVersion);
|
||||
$infraExcludePattern = "(^|/)coollabsio/coolify-helper:{$escapedHelperVersion}$|(^|/)coollabsio/coolify-realtime:{$escapedRealtimeVersion}$";
|
||||
|
||||
// Delete unused images that:
|
||||
// - Are not application images (don't match app repos)
|
||||
// - Are not current Coolify infrastructure images (any registry)
|
||||
// - 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)
|
||||
$grepCommands = "grep -v '<none>'";
|
||||
|
||||
// Add application repo exclusion if there are applications
|
||||
if ($applicationImageRepos->isNotEmpty()) {
|
||||
$grepCommands .= " | grep -v -E '^({$appExcludePatterns})[_:].+'";
|
||||
}
|
||||
|
||||
// Add infrastructure image exclusion (matches any registry prefix)
|
||||
$grepCommands .= " | grep -v -E '{$infraExcludePattern}'";
|
||||
|
||||
$commands[] = "docker images --format '{{.Repository}}:{{.Tag}}' | ".
|
||||
$grepCommands.' | '.
|
||||
"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";
|
||||
|
||||
return implode(' && ', $commands);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -251,3 +251,92 @@
|
|||
expect(preg_match($pattern, $image))->toBe(0, "Image {$image} should be deletable");
|
||||
}
|
||||
});
|
||||
|
||||
it('excludes current version of Coolify infrastructure images from any registry', function () {
|
||||
// Test the regex pattern used to protect the current version of infrastructure images
|
||||
// regardless of which registry they come from (ghcr.io, docker.io, or no prefix)
|
||||
$helperVersion = '1.0.12';
|
||||
$realtimeVersion = '1.0.10';
|
||||
|
||||
// Build the exclusion pattern the same way CleanupDocker does
|
||||
// Pattern: (^|/)coollabsio/coolify-helper:VERSION$|(^|/)coollabsio/coolify-realtime:VERSION$
|
||||
$escapedHelperVersion = preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $helperVersion);
|
||||
$escapedRealtimeVersion = preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $realtimeVersion);
|
||||
|
||||
// For PHP preg_match, escape forward slashes
|
||||
$infraPattern = "(^|\\/)coollabsio\\/coolify-helper:{$escapedHelperVersion}$|(^|\\/)coollabsio\\/coolify-realtime:{$escapedRealtimeVersion}$";
|
||||
$pattern = "/{$infraPattern}/";
|
||||
|
||||
// Current versioned infrastructure images from ANY registry should be PROTECTED
|
||||
$protectedImages = [
|
||||
// ghcr.io registry
|
||||
"ghcr.io/coollabsio/coolify-helper:{$helperVersion}",
|
||||
"ghcr.io/coollabsio/coolify-realtime:{$realtimeVersion}",
|
||||
// docker.io registry (explicit)
|
||||
"docker.io/coollabsio/coolify-helper:{$helperVersion}",
|
||||
"docker.io/coollabsio/coolify-realtime:{$realtimeVersion}",
|
||||
// No registry prefix (Docker Hub implicit)
|
||||
"coollabsio/coolify-helper:{$helperVersion}",
|
||||
"coollabsio/coolify-realtime:{$realtimeVersion}",
|
||||
];
|
||||
|
||||
// Verify current infrastructure images ARE protected from any registry
|
||||
foreach ($protectedImages as $image) {
|
||||
expect(preg_match($pattern, $image))->toBe(1, "Current infrastructure image {$image} should be protected");
|
||||
}
|
||||
|
||||
// Verify OLD versions of infrastructure images are NOT protected (can be deleted)
|
||||
$oldVersionImages = [
|
||||
'ghcr.io/coollabsio/coolify-helper:1.0.11',
|
||||
'docker.io/coollabsio/coolify-helper:1.0.10',
|
||||
'coollabsio/coolify-helper:1.0.9',
|
||||
'ghcr.io/coollabsio/coolify-realtime:1.0.9',
|
||||
'ghcr.io/coollabsio/coolify-helper:latest',
|
||||
'coollabsio/coolify-realtime:latest',
|
||||
];
|
||||
|
||||
foreach ($oldVersionImages as $image) {
|
||||
expect(preg_match($pattern, $image))->toBe(0, "Old infrastructure image {$image} should NOT be protected");
|
||||
}
|
||||
|
||||
// Verify other images are NOT protected (can be deleted)
|
||||
$deletableImages = [
|
||||
'nginx:alpine',
|
||||
'postgres:15',
|
||||
'redis:7',
|
||||
'mysql:8.0',
|
||||
'node:20',
|
||||
];
|
||||
|
||||
foreach ($deletableImages as $image) {
|
||||
expect(preg_match($pattern, $image))->toBe(0, "Image {$image} should NOT be protected");
|
||||
}
|
||||
});
|
||||
|
||||
it('protects current infrastructure images from any registry even when no applications exist', function () {
|
||||
// When there are no applications, current versioned infrastructure images should still be protected
|
||||
// regardless of which registry they come from
|
||||
$helperVersion = '1.0.12';
|
||||
$realtimeVersion = '1.0.10';
|
||||
|
||||
// Build the pattern the same way CleanupDocker does
|
||||
$escapedHelperVersion = preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $helperVersion);
|
||||
$escapedRealtimeVersion = preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $realtimeVersion);
|
||||
|
||||
// For PHP preg_match, escape forward slashes
|
||||
$infraPattern = "(^|\\/)coollabsio\\/coolify-helper:{$escapedHelperVersion}$|(^|\\/)coollabsio\\/coolify-realtime:{$escapedRealtimeVersion}$";
|
||||
$pattern = "/{$infraPattern}/";
|
||||
|
||||
// Verify current infrastructure images from any registry are protected
|
||||
expect(preg_match($pattern, "ghcr.io/coollabsio/coolify-helper:{$helperVersion}"))->toBe(1);
|
||||
expect(preg_match($pattern, "docker.io/coollabsio/coolify-helper:{$helperVersion}"))->toBe(1);
|
||||
expect(preg_match($pattern, "coollabsio/coolify-helper:{$helperVersion}"))->toBe(1);
|
||||
expect(preg_match($pattern, "ghcr.io/coollabsio/coolify-realtime:{$realtimeVersion}"))->toBe(1);
|
||||
|
||||
// Old versions should NOT be protected
|
||||
expect(preg_match($pattern, 'ghcr.io/coollabsio/coolify-helper:1.0.11'))->toBe(0);
|
||||
expect(preg_match($pattern, 'docker.io/coollabsio/coolify-helper:1.0.11'))->toBe(0);
|
||||
|
||||
// Other images should not be protected
|
||||
expect(preg_match($pattern, 'nginx:alpine'))->toBe(0);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue