fix(docker-cleanup): clean after a resource stop only when the disk needs it

A stop or delete no longer queues a full cleanup each time. It skips the
cleanup when one finished on the server in the last hour, and the queued
run only cleans when disk usage is at or above the server cleanup
threshold. The docker_cleanup API flag keeps its default of true.
This commit is contained in:
Andras Bacsai
2026-10-03 21:25:17 +02:00
parent 7956be8833
commit 3575234b38
10 changed files with 90 additions and 22 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ class StopApplication
}
if ($dockerCleanup) {
CleanupDocker::dispatch($server, false, false);
CleanupDocker::dispatchAfterStop($server);
}
} catch (\Exception $e) {
$errors[] = $e->getMessage();
+1 -1
View File
@@ -41,7 +41,7 @@ class StopDatabase
}
if ($dockerCleanup) {
CleanupDocker::dispatch($server, false, false);
CleanupDocker::dispatchAfterStop($server);
}
if ($database->is_public) {
+35 -1
View File
@@ -7,6 +7,7 @@ use App\Support\Actions\UniqueUntilProcessingJobDecorator;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Decorators\JobDecorator;
use Lorisleiva\Actions\Decorators\UniqueJobDecorator;
@@ -34,6 +35,12 @@ class CleanupDocker implements ShouldBeUnique
*/
public const QUEUED_DELAY = 60;
/**
* A resource stop does not queue a cleanup when a cleanup finished on the server in this
* window. The scheduled cleanup still runs.
*/
public const STOP_CLEANUP_COOLDOWN = 3600;
public int $jobTries = 1;
/**
@@ -71,6 +78,24 @@ class CleanupDocker implements ShouldBeUnique
return new UniqueUntilProcessingJobDecorator(static::class, ...$arguments);
}
public static function lastRunCacheKey(Server $server): string
{
return 'docker-cleanup-last-run-'.$server->uuid;
}
/**
* Cleanup after a resource stop: skipped when a cleanup ran recently, and the queued run
* only cleans when the disk usage is at or above the server cleanup threshold.
*/
public static function dispatchAfterStop(Server $server): void
{
if (Cache::has(self::lastRunCacheKey($server))) {
return;
}
static::dispatch($server, false, false, true);
}
public function getJobUniqueId(Server $server): string
{
return $server->uuid;
@@ -95,9 +120,16 @@ class CleanupDocker implements ShouldBeUnique
];
}
public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $deleteUnusedNetworks = false)
public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $deleteUnusedNetworks = false, bool $onlyAboveThreshold = false)
{
$this->deadline = now()->getTimestamp() + self::REMOTE_COMMANDS_DEADLINE;
if ($onlyAboveThreshold) {
$diskUsage = $server->getDiskUsage();
if (is_numeric($diskUsage) && (int) $diskUsage < $server->settings->docker_cleanup_threshold) {
return [];
}
}
$helperImageVersion = getHelperVersion();
$helperImage = coolifyHelperImage();
$helperImageWithVersion = "$helperImage:$helperImageVersion";
@@ -155,6 +187,8 @@ class CleanupDocker implements ShouldBeUnique
}
}
Cache::put(self::lastRunCacheKey($server), true, self::STOP_CLEANUP_COOLDOWN);
return $cleanupLog;
}
+1 -1
View File
@@ -62,7 +62,7 @@ class StopService
$service->deleteConnectedNetworks();
}
if ($dockerCleanup) {
CleanupDocker::dispatch($server, false, false);
CleanupDocker::dispatchAfterStop($server);
}
} catch (\Exception $e) {
return $e->getMessage();
@@ -2926,7 +2926,7 @@ class ApplicationsController extends Controller
),
new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
@@ -4553,7 +4553,7 @@ class ApplicationsController extends Controller
new OA\Parameter(
name: 'docker_cleanup',
in: 'query',
description: 'Perform docker cleanup (prune networks, volumes, etc.).',
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.',
schema: new OA\Schema(
type: 'boolean',
default: true,
@@ -2738,7 +2738,7 @@ class DatabasesController extends Controller
),
new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
@@ -3428,7 +3428,7 @@ class DatabasesController extends Controller
new OA\Parameter(
name: 'docker_cleanup',
in: 'query',
description: 'Perform docker cleanup (prune networks, volumes, etc.).',
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.',
schema: new OA\Schema(
type: 'boolean',
default: true,
@@ -1005,7 +1005,7 @@ class ServicesController extends Controller
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_from_coolify_only', in: 'query', required: false, description: 'Remove only Coolify metadata without deleting Docker resources.', schema: new OA\Schema(type: 'boolean', default: false)),
],
@@ -2184,7 +2184,7 @@ class ServicesController extends Controller
new OA\Parameter(
name: 'docker_cleanup',
in: 'query',
description: 'Perform docker cleanup (prune networks, volumes, etc.).',
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.',
schema: new OA\Schema(
type: 'boolean',
default: true,
+6 -6
View File
@@ -2919,7 +2919,7 @@
{
"name": "docker_cleanup",
"in": "query",
"description": "Run docker cleanup.",
"description": "Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.",
"required": false,
"schema": {
"type": "boolean",
@@ -4359,7 +4359,7 @@
{
"name": "docker_cleanup",
"in": "query",
"description": "Perform docker cleanup (prune networks, volumes, etc.).",
"description": "Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.",
"schema": {
"type": "boolean",
"default": true
@@ -6512,7 +6512,7 @@
{
"name": "docker_cleanup",
"in": "query",
"description": "Run docker cleanup.",
"description": "Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.",
"required": false,
"schema": {
"type": "boolean",
@@ -8768,7 +8768,7 @@
{
"name": "docker_cleanup",
"in": "query",
"description": "Perform docker cleanup (prune networks, volumes, etc.).",
"description": "Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.",
"schema": {
"type": "boolean",
"default": true
@@ -18942,7 +18942,7 @@
{
"name": "docker_cleanup",
"in": "query",
"description": "Run docker cleanup.",
"description": "Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.",
"required": false,
"schema": {
"type": "boolean",
@@ -19895,7 +19895,7 @@
{
"name": "docker_cleanup",
"in": "query",
"description": "Perform docker cleanup (prune networks, volumes, etc.).",
"description": "Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.",
"schema": {
"type": "boolean",
"default": true
+6 -6
View File
@@ -1931,7 +1931,7 @@ paths:
-
name: docker_cleanup
in: query
description: 'Run docker cleanup.'
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.'
required: false
schema:
type: boolean
@@ -2841,7 +2841,7 @@ paths:
-
name: docker_cleanup
in: query
description: 'Perform docker cleanup (prune networks, volumes, etc.).'
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.'
schema:
type: boolean
default: true
@@ -4203,7 +4203,7 @@ paths:
-
name: docker_cleanup
in: query
description: 'Run docker cleanup.'
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.'
required: false
schema:
type: boolean
@@ -5765,7 +5765,7 @@ paths:
-
name: docker_cleanup
in: query
description: 'Perform docker cleanup (prune networks, volumes, etc.).'
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.'
schema:
type: boolean
default: true
@@ -11971,7 +11971,7 @@ paths:
-
name: docker_cleanup
in: query
description: 'Run docker cleanup.'
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.'
required: false
schema:
type: boolean
@@ -12547,7 +12547,7 @@ paths:
-
name: docker_cleanup
in: query
description: 'Perform docker cleanup (prune networks, volumes, etc.).'
description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.'
schema:
type: boolean
default: true
+34
View File
@@ -292,6 +292,40 @@ it('accepts a new queued cleanup once the waiting one is lost', function () {
Queue::assertPushed(UniqueUntilProcessingJobDecorator::class, 2);
});
it('does not queue a cleanup after a stop when a cleanup ran on the server recently', function () {
Queue::fake();
$cleanedServer = dockerCleanupReachableServer();
$otherServer = dockerCleanupReachableServer();
Cache::put(CleanupDocker::lastRunCacheKey($cleanedServer), true, CleanupDocker::STOP_CLEANUP_COOLDOWN);
CleanupDocker::dispatchAfterStop($cleanedServer);
CleanupDocker::dispatchAfterStop($otherServer);
Queue::assertPushed(UniqueUntilProcessingJobDecorator::class, 1);
Queue::assertPushed(UniqueUntilProcessingJobDecorator::class, fn (JobDecorator $job) => $job->getParameters()[0]->is($otherServer));
});
it('cleans after a stop only when the disk usage reaches the server threshold', function (string $diskUsage, bool $cleans) {
$server = dockerCleanupReachableServer();
$server->settings()->update(['docker_cleanup_threshold' => 80]);
$commands = collect();
Process::fake(function ($process) use ($commands, $diskUsage) {
$command = is_array($process->command) ? implode(' ', $process->command) : $process->command;
$commands->push($command);
return Process::result(output: str_contains($command, 'df / --output=pcent') ? $diskUsage : '');
});
CleanupDocker::run($server->fresh(), false, false, true);
expect($commands->contains(fn (string $command) => str_contains($command, 'docker builder prune')))->toBe($cleans)
->and(Cache::has(CleanupDocker::lastRunCacheKey($server)))->toBe($cleans);
})->with([
'below threshold' => ['79', false],
'at threshold' => ['80', true],
'unknown usage' => ['', true],
]);
it('scans application images with a fixed number of remote commands and keeps the retention rules', function () {
$server = dockerCleanupReachableServer();
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();