Merge pull request #3545 from peaklabs-dev/improve-cleanup
Feat: Improve Docker cleanup
This commit is contained in:
@@ -12,28 +12,29 @@ class CleanupDocker
|
|||||||
|
|
||||||
public function handle(Server $server)
|
public function handle(Server $server)
|
||||||
{
|
{
|
||||||
|
$settings = InstanceSettings::get();
|
||||||
|
$helperImageVersion = data_get($settings, 'helper_version');
|
||||||
|
$helperImage = config('coolify.helper_image');
|
||||||
|
$helperImageWithVersion = "$helperImage:$helperImageVersion";
|
||||||
|
|
||||||
$commands = $this->getCommands();
|
$commands = [
|
||||||
|
'docker container prune -f --filter "label=coolify.managed=true"',
|
||||||
|
'docker image prune -af --filter "label!=coolify.managed=true"',
|
||||||
|
'docker builder prune -af',
|
||||||
|
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
|
||||||
|
];
|
||||||
|
|
||||||
|
$serverSettings = $server->settings;
|
||||||
|
if ($serverSettings->delete_unused_volumes) {
|
||||||
|
$commands[] = 'docker volume prune -af';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($serverSettings->delete_unused_networks) {
|
||||||
|
$commands[] = 'docker network prune -f';
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($commands as $command) {
|
foreach ($commands as $command) {
|
||||||
instant_remote_process([$command], $server, false);
|
instant_remote_process([$command], $server, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCommands(): array
|
|
||||||
{
|
|
||||||
$settings = InstanceSettings::get();
|
|
||||||
$helperImageVersion = data_get($settings, 'helper_version');
|
|
||||||
$helperImage = config('coolify.helper_image');
|
|
||||||
$helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion;
|
|
||||||
|
|
||||||
$commonCommands = [
|
|
||||||
'docker container prune -f --filter "label=coolify.managed=true"',
|
|
||||||
'docker image prune -af --filter "label!=coolify.managed=true"',
|
|
||||||
'docker builder prune -af',
|
|
||||||
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi",
|
|
||||||
];
|
|
||||||
|
|
||||||
return $commonCommands;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public ?string $usageBefore = null;
|
public ?string $usageBefore = null;
|
||||||
|
|
||||||
public function __construct(public Server $server) {}
|
public function __construct(public Server $server, public bool $manualCleanup = false) {}
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
@@ -31,10 +31,10 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
if (! $this->server->isFunctional()) {
|
if (! $this->server->isFunctional()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->server->settings->force_docker_cleanup) {
|
|
||||||
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
|
|
||||||
CleanupDocker::run(server: $this->server);
|
|
||||||
|
|
||||||
|
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
|
||||||
|
Log::info('DockerCleanupJob ' . ($this->manualCleanup ? 'manual' : 'force') . ' cleanup on ' . $this->server->name);
|
||||||
|
CleanupDocker::run(server: $this->server);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Actions\Server\StopSentinel;
|
|||||||
use App\Jobs\PullSentinelImageJob;
|
use App\Jobs\PullSentinelImageJob;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
use App\Jobs\DockerCleanupJob;
|
||||||
|
|
||||||
class Form extends Component
|
class Form extends Component
|
||||||
{
|
{
|
||||||
@@ -24,6 +25,9 @@ class Form extends Component
|
|||||||
|
|
||||||
public $timezones;
|
public $timezones;
|
||||||
|
|
||||||
|
public $delete_unused_volumes = false;
|
||||||
|
public $delete_unused_networks = false;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
$teamId = auth()->user()->currentTeam()->id;
|
$teamId = auth()->user()->currentTeam()->id;
|
||||||
@@ -58,6 +62,8 @@ class Form extends Component
|
|||||||
'server.settings.force_docker_cleanup' => 'required|boolean',
|
'server.settings.force_docker_cleanup' => 'required|boolean',
|
||||||
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
|
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
|
||||||
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
|
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
|
||||||
|
'server.settings.delete_unused_volumes' => 'boolean',
|
||||||
|
'server.settings.delete_unused_networks' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
@@ -79,6 +85,8 @@ class Form extends Component
|
|||||||
'server.settings.metrics_history_days' => 'Metrics History',
|
'server.settings.metrics_history_days' => 'Metrics History',
|
||||||
'server.settings.is_server_api_enabled' => 'Server API',
|
'server.settings.is_server_api_enabled' => 'Server API',
|
||||||
'server.settings.server_timezone' => 'Server Timezone',
|
'server.settings.server_timezone' => 'Server Timezone',
|
||||||
|
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
|
||||||
|
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount(Server $server)
|
public function mount(Server $server)
|
||||||
@@ -88,6 +96,8 @@ class Form extends Component
|
|||||||
$this->wildcard_domain = $this->server->settings->wildcard_domain;
|
$this->wildcard_domain = $this->server->settings->wildcard_domain;
|
||||||
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
|
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
|
||||||
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
|
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
|
||||||
|
$this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
|
||||||
|
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updated($field)
|
public function updated($field)
|
||||||
@@ -137,6 +147,7 @@ class Form extends Component
|
|||||||
try {
|
try {
|
||||||
refresh_server_connection($this->server->privateKey);
|
refresh_server_connection($this->server->privateKey);
|
||||||
$this->validateServer(false);
|
$this->validateServer(false);
|
||||||
|
|
||||||
$this->server->settings->save();
|
$this->server->settings->save();
|
||||||
$this->server->save();
|
$this->server->save();
|
||||||
$this->dispatch('success', 'Server updated.');
|
$this->dispatch('success', 'Server updated.');
|
||||||
@@ -154,6 +165,7 @@ class Form extends Component
|
|||||||
ray('Sentinel is not enabled');
|
ray('Sentinel is not enabled');
|
||||||
StopSentinel::dispatch($this->server);
|
StopSentinel::dispatch($this->server);
|
||||||
}
|
}
|
||||||
|
$this->server->settings->save();
|
||||||
// $this->checkPortForServerApi();
|
// $this->checkPortForServerApi();
|
||||||
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -234,9 +246,9 @@ class Form extends Component
|
|||||||
$this->server->settings->server_timezone = $newTimezone;
|
$this->server->settings->server_timezone = $newTimezone;
|
||||||
$this->server->settings->save();
|
$this->server->settings->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->server->settings->save();
|
$this->server->settings->save();
|
||||||
$this->server->save();
|
$this->server->save();
|
||||||
|
|
||||||
$this->dispatch('success', 'Server updated.');
|
$this->dispatch('success', 'Server updated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
@@ -250,6 +262,15 @@ class Form extends Component
|
|||||||
$this->dispatch('success', 'Server timezone updated.');
|
$this->dispatch('success', 'Server timezone updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function manualCleanup()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DockerCleanupJob::dispatch($this->server, true);
|
||||||
|
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
public function manualCloudflareConfig()
|
public function manualCloudflareConfig()
|
||||||
{
|
{
|
||||||
$this->server->settings->is_cloudflare_tunnel = true;
|
$this->server->settings->is_cloudflare_tunnel = true;
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ use Symfony\Component\Yaml\Yaml;
|
|||||||
'validation_logs' => ['type' => 'string'],
|
'validation_logs' => ['type' => 'string'],
|
||||||
'log_drain_notification_sent' => ['type' => 'boolean'],
|
'log_drain_notification_sent' => ['type' => 'boolean'],
|
||||||
'swarm_cluster' => ['type' => 'string'],
|
'swarm_cluster' => ['type' => 'string'],
|
||||||
|
'delete_unused_volumes' => ['type' => 'boolean'],
|
||||||
|
'delete_unused_networks' => ['type' => 'boolean'],
|
||||||
]
|
]
|
||||||
)]
|
)]
|
||||||
|
|
||||||
@@ -105,6 +107,8 @@ class Server extends BaseModel
|
|||||||
'proxy' => SchemalessAttributes::class,
|
'proxy' => SchemalessAttributes::class,
|
||||||
'logdrain_axiom_api_key' => 'encrypted',
|
'logdrain_axiom_api_key' => 'encrypted',
|
||||||
'logdrain_newrelic_license_key' => 'encrypted',
|
'logdrain_newrelic_license_key' => 'encrypted',
|
||||||
|
'delete_unused_volumes' => 'boolean',
|
||||||
|
'delete_unused_networks' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $schemalessAttributes = [
|
protected $schemalessAttributes = [
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('server_settings', function (Blueprint $table) {
|
||||||
|
$table->boolean('delete_unused_volumes')->default(false);
|
||||||
|
$table->boolean('delete_unused_networks')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('server_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('delete_unused_volumes');
|
||||||
|
$table->dropColumn('delete_unused_networks');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -194,14 +194,39 @@
|
|||||||
|
|
||||||
@if ($server->isFunctional())
|
@if ($server->isFunctional())
|
||||||
<h3 class="pt-4">Settings</h3>
|
<h3 class="pt-4">Settings</h3>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-col flex-wrap gap-2 sm:flex-nowrap">
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
<div class="w-64">
|
<div class="w-64">
|
||||||
<x-forms.checkbox
|
<x-forms.checkbox
|
||||||
helper="Enable force Docker Cleanup. This will cleanup build caches / unused images / etc."
|
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
|
||||||
|
<ul class='list-disc pl-4 mt-2'>
|
||||||
|
<li>Removes stopped containers manged by Coolify (as containers are none persistent, no data will be lost).</li>
|
||||||
|
<li>Deletes unused images.</li>
|
||||||
|
<li>Clears build cache.</li>
|
||||||
|
<li>Removes old versions of the Coolify helper image.</li>
|
||||||
|
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
|
||||||
|
<li>Optionally remove unused networks (if enabled in advanced options).</li>
|
||||||
|
</ul>"
|
||||||
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" />
|
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" />
|
||||||
</div>
|
</div>
|
||||||
|
<x-modal-confirmation
|
||||||
|
title="Confirm Docker Cleanup?"
|
||||||
|
buttonTitle="Trigger Docker Cleanup"
|
||||||
|
submitAction="manualCleanup"
|
||||||
|
:actions="[
|
||||||
|
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
|
||||||
|
'Permanently deletes all unused images',
|
||||||
|
'Clears build cache',
|
||||||
|
'Removes old versions of the Coolify helper image',
|
||||||
|
'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
|
||||||
|
'Optionally permanently deletes all unused networks (if enabled in advanced options).'
|
||||||
|
]"
|
||||||
|
:confirmWithText="false"
|
||||||
|
:confirmWithPassword="false"
|
||||||
|
step2ButtonText="Trigger Docker Cleanup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@if ($server->settings->force_docker_cleanup)
|
@if ($server->settings->force_docker_cleanup)
|
||||||
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
|
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
|
||||||
label="Docker cleanup frequency" required
|
label="Docker cleanup frequency" required
|
||||||
@@ -211,9 +236,36 @@
|
|||||||
label="Docker cleanup threshold (%)" required
|
label="Docker cleanup threshold (%)" required
|
||||||
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
|
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
|
||||||
@endif
|
@endif
|
||||||
|
<div x-data="{ open: false }" class="mt-4 max-w-md">
|
||||||
|
<button @click="open = !open" type="button" class="flex items-center justify-between w-full text-left text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100">
|
||||||
|
<span>Advanced Options</span>
|
||||||
|
<svg :class="{'rotate-180': open}" class="w-5 h-5 transition-transform duration-200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" class="mt-2 space-y-2">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2"><strong>Warning: Enable these options only if you fully understand their implications and consequences!</strong><br>Improper use will result in data loss and could cause functional issues.</p>
|
||||||
|
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes" label="Delete Unused Volumes"
|
||||||
|
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
|
||||||
|
<ul class='list-disc pl-4 mt-2'>
|
||||||
|
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
|
||||||
|
<li>Data from stopped containers volumes will be permanently lost.</li>
|
||||||
|
<li>No way to recover deleted volume data.</li>
|
||||||
|
</ul>"
|
||||||
|
/>
|
||||||
|
<x-forms.checkbox instantSave id="server.settings.delete_unused_networks" label="Delete Unused Networks"
|
||||||
|
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
|
||||||
|
<ul class='list-disc pl-4 mt-2'>
|
||||||
|
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
|
||||||
|
<li>Custom networks for stopped containers will be permanently deleted.</li>
|
||||||
|
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
|
||||||
|
</ul>"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap">
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-4 sm:flex-nowrap">
|
||||||
<x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required
|
<x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required
|
||||||
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
|
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
|
||||||
<x-forms.input id="server.settings.dynamic_timeout" label="Deployment timeout (seconds)" required
|
<x-forms.input id="server.settings.dynamic_timeout" label="Deployment timeout (seconds)" required
|
||||||
|
|||||||
Reference in New Issue
Block a user