From 856735613d70ec8cd9c00c3468d11b18554a4c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=94=EF=B8=8F=20Peak?= <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:59:14 +0200 Subject: [PATCH] feat(backups): back up the coolify database before an update (#12093) --- app/Actions/Server/UpdateCoolify.php | 20 ++++++ app/Livewire/SettingsBackup.php | 24 +++++++ app/Models/InstanceSettings.php | 2 + ...ate_enabled_to_instance_settings_table.php | 28 ++++++++ .../views/livewire/settings-backup.blade.php | 15 +++- .../SettingsBackupBeforeUpdateTest.php | 47 +++++++++++++ tests/Unit/UpdateCoolifyTest.php | 70 +++++++++++++++++++ 7 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2026_10_12_002632_add_is_backup_before_update_enabled_to_instance_settings_table.php create mode 100644 tests/Feature/SettingsBackupBeforeUpdateTest.php diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 3c60c34379..ddfc6578e4 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -2,7 +2,9 @@ namespace App\Actions\Server; +use App\Jobs\DatabaseBackupJob; use App\Models\Server; +use App\Models\StandalonePostgresql; use App\Services\CoolifyUpgradeStatus; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -22,6 +24,7 @@ class UpdateCoolify public function handle($manual_update = false) { if (isDev()) { + $this->backupDatabase(); Sleep::for(10)->seconds(); return; @@ -110,11 +113,28 @@ class UpdateCoolify ); } + $this->backupDatabase(); $this->update(); $settings->new_version_available = false; $settings->save(); } + private function backupDatabase(): void + { + $backup = StandalonePostgresql::find(0)?->scheduledBackups()->first(); + if (! $backup || ! instanceSettings()->is_backup_before_update_enabled) { + return; + } + + // Runs inline so the update waits for the result. dispatchSync() would go through the job's + // WithoutOverlapping middleware, which silently drops the run while the scheduled backup is running. + $job = new DatabaseBackupJob($backup); + $job->handle(); + if ($job->backup_log?->status !== 'success') { + throw new \Exception('The database backup failed, so the update was not started.'); + } + } + private function update() { $this->ensureNoUpgradeIsRunning(); diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index a7e8b89488..0f92b2ad34 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -46,6 +46,8 @@ class SettingsBackup extends Component #[Validate(['required'])] public string $postgres_password; + public bool $is_backup_before_update_enabled; + public function mount() { if (! isInstanceAdmin()) { @@ -75,6 +77,7 @@ class SettingsBackup extends Component $this->executions = $this->backup?->executions ?? []; } $this->settings = $settings; + $this->is_backup_before_update_enabled = $settings->is_backup_before_update_enabled; $this->s3s = $s3s; } @@ -136,6 +139,27 @@ class SettingsBackup extends Component } } + public function instantSave() + { + try { + $this->authorize('update', $this->settings); + $this->settings->is_backup_before_update_enabled = $this->is_backup_before_update_enabled; + $changedFields = auditChangedFields($this->settings); + $this->settings->save(); + if ($changedFields !== []) { + auditLog('ui.instance.settings.updated', [ + 'team_id' => null, + 'resource' => 'instance', + 'section' => 'backup', + 'changed_fields' => $changedFields, + ]); + } + $this->dispatch('success', 'Backup updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 16a1d7ea30..87a7356920 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -21,6 +21,7 @@ class InstanceSettings extends Model 'public_port_max', 'do_not_track', 'is_auto_update_enabled', + 'is_backup_before_update_enabled', 'is_registration_enabled', 'disable_registration_when_oauth_enabled', 'next_channel', @@ -90,6 +91,7 @@ class InstanceSettings extends Model 'allowed_ip_ranges' => 'array', 'is_auto_update_enabled' => 'boolean', + 'is_backup_before_update_enabled' => 'boolean', 'is_registration_enabled' => 'boolean', 'disable_registration_when_oauth_enabled' => 'boolean', 'auto_update_frequency' => 'string', diff --git a/database/migrations/2026_10_12_002632_add_is_backup_before_update_enabled_to_instance_settings_table.php b/database/migrations/2026_10_12_002632_add_is_backup_before_update_enabled_to_instance_settings_table.php new file mode 100644 index 0000000000..ff6cc2509e --- /dev/null +++ b/database/migrations/2026_10_12_002632_add_is_backup_before_update_enabled_to_instance_settings_table.php @@ -0,0 +1,28 @@ +boolean('is_backup_before_update_enabled')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('is_backup_before_update_enabled'); + }); + } +}; diff --git a/resources/views/livewire/settings-backup.blade.php b/resources/views/livewire/settings-backup.blade.php index c89bf148cf..05673e9816 100644 --- a/resources/views/livewire/settings-backup.blade.php +++ b/resources/views/livewire/settings-backup.blade.php @@ -8,7 +8,8 @@ @if ($server->isFunctional()) @if (isset($database) && isset($backup))