From 307ea4d8d86d27ccfa19c6b4bb2fe36b996fb462 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 2 Feb 2026 22:31:28 +0100
Subject: [PATCH 01/11] feat(backup): mark existing backups as legacy
---
.../ScheduledDatabaseBackupExecution.php | 1 +
...gacy_column_and_cleanup_backup_columns.php | 22 +++++++++++++++++++
2 files changed, 23 insertions(+)
create mode 100644 database/migrations/2026_02_02_220629_add_legacy_column_and_cleanup_backup_columns.php
diff --git a/app/Models/ScheduledDatabaseBackupExecution.php b/app/Models/ScheduledDatabaseBackupExecution.php
index c0298ecc8..1cd4f1aba 100644
--- a/app/Models/ScheduledDatabaseBackupExecution.php
+++ b/app/Models/ScheduledDatabaseBackupExecution.php
@@ -14,6 +14,7 @@ class ScheduledDatabaseBackupExecution extends BaseModel
's3_uploaded' => 'boolean',
'local_storage_deleted' => 'boolean',
's3_storage_deleted' => 'boolean',
+ 'legacy' => 'boolean',
];
}
diff --git a/database/migrations/2026_02_02_220629_add_legacy_column_and_cleanup_backup_columns.php b/database/migrations/2026_02_02_220629_add_legacy_column_and_cleanup_backup_columns.php
new file mode 100644
index 000000000..3efe4778b
--- /dev/null
+++ b/database/migrations/2026_02_02_220629_add_legacy_column_and_cleanup_backup_columns.php
@@ -0,0 +1,22 @@
+boolean('legacy')->default(false)->after('filename');
+ });
+
+ // Mark all existing backup executions as legacy
+ DB::table('scheduled_database_backup_executions')->update(['legacy' => true]);
+
+ // @todo: In a future update or v5 we no longer need "databases_to_backup", "dump_all" in "scheduled_database_backups" - keeping them for now if we need to rollback.
+ // @todo: In a future update or v5 we no longer need "database_name" from "scheduled_database_backup_executions" - keeping them for now if we need to rollback.
+ }
+};
From b92aa0a3203ef771f9bdec0f3e6f868bb0503f30 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 2 Feb 2026 22:32:08 +0100
Subject: [PATCH 02/11] feat(ui): add legacy badge to ui and remove database
- we now backup all databases all the time so no need to specify the database anymore
---
.../livewire/project/database/backup-executions.blade.php | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/resources/views/livewire/project/database/backup-executions.blade.php b/resources/views/livewire/project/database/backup-executions.blade.php
index b6d88a2fd..ba0e1eaf1 100644
--- a/resources/views/livewire/project/database/backup-executions.blade.php
+++ b/resources/views/livewire/project/database/backup-executions.blade.php
@@ -66,6 +66,11 @@
@endphp
{{ $statusText }}
+ @if (data_get($execution, 'legacy', false))
+
+ Legacy
+
+ @endif
@if (data_get($execution, 'status') === 'running')
@@ -79,7 +84,6 @@
• {{ \Carbon\Carbon::parse(data_get($execution, 'finished_at'))->format('M j, H:i') }}
@endif
- • Database: {{ data_get($execution, 'database_name', 'N/A') }}
@if(data_get($execution, 'size'))
• Size: {{ formatBytes(data_get($execution, 'size')) }}
@endif
From 268dcc9d043b3414dd1dc4dc673a5b2b4b81a9c8 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 2 Feb 2026 22:33:26 +0100
Subject: [PATCH 03/11] feat(backup): remove overcomplicated backup settings
- remove databasesToBackup and dumpAll settings
---
app/Livewire/Project/Database/BackupEdit.php | 29 ----------------
.../Database/CreateScheduledBackup.php | 8 -----
.../project/database/backup-edit.blade.php | 34 -------------------
3 files changed, 71 deletions(-)
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index 35262d7b0..dfd32f2cf 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -69,12 +69,6 @@ class BackupEdit extends Component
#[Validate(['nullable', 'integer'])]
public ?int $s3StorageId = 1;
- #[Validate(['nullable', 'string'])]
- public ?string $databasesToBackup = null;
-
- #[Validate(['required', 'boolean'])]
- public bool $dumpAll = false;
-
#[Validate(['required', 'int', 'min:60', 'max:36000'])]
public int $timeout = 3600;
@@ -103,27 +97,6 @@ class BackupEdit extends Component
$this->backup->save_s3 = $this->saveS3;
$this->backup->disable_local_backup = $this->disableLocalBackup;
$this->backup->s3_storage_id = $this->s3StorageId;
-
- // Validate databases_to_backup to prevent command injection
- if (filled($this->databasesToBackup)) {
- $databases = str($this->databasesToBackup)->explode(',');
- foreach ($databases as $index => $db) {
- $dbName = trim($db);
- try {
- validateShellSafePath($dbName, 'database name');
- } catch (\Exception $e) {
- // Provide specific error message indicating which database failed validation
- $position = $index + 1;
- throw new \Exception(
- "Database #{$position} ('{$dbName}') validation failed: ".
- $e->getMessage()
- );
- }
- }
- }
-
- $this->backup->databases_to_backup = $this->databasesToBackup;
- $this->backup->dump_all = $this->dumpAll;
$this->backup->timeout = $this->timeout;
$this->customValidate();
$this->backup->save();
@@ -140,8 +113,6 @@ class BackupEdit extends Component
$this->saveS3 = $this->backup->save_s3;
$this->disableLocalBackup = $this->backup->disable_local_backup ?? false;
$this->s3StorageId = $this->backup->s3_storage_id;
- $this->databasesToBackup = $this->backup->databases_to_backup;
- $this->dumpAll = $this->backup->dump_all;
$this->timeout = $this->backup->timeout;
}
}
diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php
index 7f807afe2..238e51ecd 100644
--- a/app/Livewire/Project/Database/CreateScheduledBackup.php
+++ b/app/Livewire/Project/Database/CreateScheduledBackup.php
@@ -65,14 +65,6 @@ class CreateScheduledBackup extends Component
'team_id' => currentTeam()->id,
];
- if ($this->database->type() === 'standalone-postgresql') {
- $payload['databases_to_backup'] = $this->database->postgres_db;
- } elseif ($this->database->type() === 'standalone-mysql') {
- $payload['databases_to_backup'] = $this->database->mysql_database;
- } elseif ($this->database->type() === 'standalone-mariadb') {
- $payload['databases_to_backup'] = $this->database->mariadb_database;
- }
-
$databaseBackup = ScheduledDatabaseBackup::create($payload);
if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$this->dispatch('refreshScheduledBackups', $databaseBackup->id);
diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php
index bb5dcfc4d..4afb116f2 100644
--- a/resources/views/livewire/project/database/backup-edit.blade.php
+++ b/resources/views/livewire/project/database/backup-edit.blade.php
@@ -46,40 +46,6 @@
@endif
Settings
-
- @if ($backup->database_type === 'App\Models\StandalonePostgresql' && $backup->database_id !== 0)
-
-
-
- @if (!$backup->dump_all)
-
- @endif
- @elseif($backup->database_type === 'App\Models\StandaloneMongodb')
-
- @elseif($backup->database_type === 'App\Models\StandaloneMysql')
-
-
-
- @if (!$backup->dump_all)
-
- @endif
- @elseif($backup->database_type === 'App\Models\StandaloneMariadb')
-
-
-
- @if (!$backup->dump_all)
-
- @endif
- @endif
-
Date: Mon, 2 Feb 2026 22:34:27 +0100
Subject: [PATCH 04/11] feat(api): remove dump_all and databases_to_backup
---
.../Controllers/Api/DatabasesController.php | 23 ++-----------------
openapi.json | 17 --------------
openapi.yaml | 13 -----------
3 files changed, 2 insertions(+), 51 deletions(-)
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 15d182db2..4c24af2a9 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -627,8 +627,6 @@ class DatabasesController extends Controller
'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled', 'default' => true],
'save_s3' => ['type' => 'boolean', 'description' => 'Whether to save backups to S3', 'default' => false],
's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID (required if save_s3 is true)'],
- 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'],
- 'dump_all' => ['type' => 'boolean', 'description' => 'Whether to dump all databases', 'default' => false],
'backup_now' => ['type' => 'boolean', 'description' => 'Whether to trigger backup immediately after creation'],
'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Number of backups to retain locally'],
'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Number of days to retain backups locally'],
@@ -672,7 +670,7 @@ class DatabasesController extends Controller
)]
public function create_backup(Request $request)
{
- $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
+ $backupConfigFields = ['save_s3', 'enabled', 'frequency', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -689,10 +687,8 @@ class DatabasesController extends Controller
'frequency' => 'required|string',
'enabled' => 'boolean',
'save_s3' => 'boolean',
- 'dump_all' => 'boolean',
'backup_now' => 'boolean|nullable',
's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable',
- 'databases_to_backup' => 'string|nullable',
'database_backup_retention_amount_locally' => 'integer|min:0',
'database_backup_retention_days_locally' => 'integer|min:0',
'database_backup_retention_max_storage_locally' => 'integer|min:0',
@@ -777,17 +773,6 @@ class DatabasesController extends Controller
unset($backupData['s3_storage_uuid']);
}
- // Set default databases_to_backup based on database type if not provided
- if (! isset($backupData['databases_to_backup']) || empty($backupData['databases_to_backup'])) {
- if ($database->type() === 'standalone-postgresql') {
- $backupData['databases_to_backup'] = $database->postgres_db;
- } elseif ($database->type() === 'standalone-mysql') {
- $backupData['databases_to_backup'] = $database->mysql_database;
- } elseif ($database->type() === 'standalone-mariadb') {
- $backupData['databases_to_backup'] = $database->mariadb_database;
- }
- }
-
// Add required fields
$backupData['database_id'] = $database->id;
$backupData['database_type'] = $database->getMorphClass();
@@ -852,8 +837,6 @@ class DatabasesController extends Controller
's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID'],
'backup_now' => ['type' => 'boolean', 'description' => 'Whether to take a backup now or not'],
'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled or not'],
- 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'],
- 'dump_all' => ['type' => 'boolean', 'description' => 'Whether all databases are dumped or not'],
'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'],
'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'],
'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'],
@@ -890,7 +873,7 @@ class DatabasesController extends Controller
)]
public function update_backup(Request $request)
{
- $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
+ $backupConfigFields = ['save_s3', 'enabled', 'frequency', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -905,9 +888,7 @@ class DatabasesController extends Controller
'save_s3' => 'boolean',
'backup_now' => 'boolean|nullable',
'enabled' => 'boolean',
- 'dump_all' => 'boolean',
's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable',
- 'databases_to_backup' => 'string|nullable',
'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly',
'database_backup_retention_amount_locally' => 'integer|min:0',
'database_backup_retention_days_locally' => 'integer|min:0',
diff --git a/openapi.json b/openapi.json
index bd502865a..a08db6d3c 100644
--- a/openapi.json
+++ b/openapi.json
@@ -3946,15 +3946,6 @@
"type": "string",
"description": "S3 storage UUID (required if save_s3 is true)"
},
- "databases_to_backup": {
- "type": "string",
- "description": "Comma separated list of databases to backup"
- },
- "dump_all": {
- "type": "boolean",
- "description": "Whether to dump all databases",
- "default": false
- },
"backup_now": {
"type": "boolean",
"description": "Whether to trigger backup immediately after creation"
@@ -4508,14 +4499,6 @@
"type": "boolean",
"description": "Whether the backup is enabled or not"
},
- "databases_to_backup": {
- "type": "string",
- "description": "Comma separated list of databases to backup"
- },
- "dump_all": {
- "type": "boolean",
- "description": "Whether all databases are dumped or not"
- },
"frequency": {
"type": "string",
"description": "Frequency of the backup"
diff --git a/openapi.yaml b/openapi.yaml
index 11148f43b..c4ec408cd 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -2473,13 +2473,6 @@ paths:
s3_storage_uuid:
type: string
description: 'S3 storage UUID (required if save_s3 is true)'
- databases_to_backup:
- type: string
- description: 'Comma separated list of databases to backup'
- dump_all:
- type: boolean
- description: 'Whether to dump all databases'
- default: false
backup_now:
type: boolean
description: 'Whether to trigger backup immediately after creation'
@@ -2862,12 +2855,6 @@ paths:
enabled:
type: boolean
description: 'Whether the backup is enabled or not'
- databases_to_backup:
- type: string
- description: 'Comma separated list of databases to backup'
- dump_all:
- type: boolean
- description: 'Whether all databases are dumped or not'
frequency:
type: string
description: 'Frequency of the backup'
From 4d50511b1db151e41988bc37e2aa79471354a535 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 2 Feb 2026 22:38:50 +0100
Subject: [PATCH 05/11] chore(test): remove restore test for now
---
.../Unit/Livewire/Database/S3RestoreTest.php | 79 -------------------
1 file changed, 79 deletions(-)
delete mode 100644 tests/Unit/Livewire/Database/S3RestoreTest.php
diff --git a/tests/Unit/Livewire/Database/S3RestoreTest.php b/tests/Unit/Livewire/Database/S3RestoreTest.php
deleted file mode 100644
index 18837b466..000000000
--- a/tests/Unit/Livewire/Database/S3RestoreTest.php
+++ /dev/null
@@ -1,79 +0,0 @@
-dumpAll = false;
- $component->postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
-
- $database = Mockery::mock('App\Models\StandalonePostgresql');
- $database->shouldReceive('getMorphClass')->andReturn('App\Models\StandalonePostgresql');
- $component->resource = $database;
-
- $result = $component->buildRestoreCommand('/tmp/test.dump');
-
- expect($result)->toContain('pg_restore');
- expect($result)->toContain('/tmp/test.dump');
-});
-
-test('buildRestoreCommand handles PostgreSQL with dumpAll', function () {
- $component = new Import;
- $component->dumpAll = true;
- // This is the full dump-all command prefix that would be set in the updatedDumpAll method
- $component->postgresqlRestoreCommand = 'psql -U $POSTGRES_USER -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid()" && psql -U $POSTGRES_USER -t -c "SELECT datname FROM pg_database WHERE NOT datistemplate" | xargs -I {} dropdb -U $POSTGRES_USER --if-exists {} && createdb -U $POSTGRES_USER postgres';
-
- $database = Mockery::mock('App\Models\StandalonePostgresql');
- $database->shouldReceive('getMorphClass')->andReturn('App\Models\StandalonePostgresql');
- $component->resource = $database;
-
- $result = $component->buildRestoreCommand('/tmp/test.dump');
-
- expect($result)->toContain('gunzip -cf /tmp/test.dump');
- expect($result)->toContain('psql -U $POSTGRES_USER postgres');
-});
-
-test('buildRestoreCommand handles MySQL without dumpAll', function () {
- $component = new Import;
- $component->dumpAll = false;
- $component->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
-
- $database = Mockery::mock('App\Models\StandaloneMysql');
- $database->shouldReceive('getMorphClass')->andReturn('App\Models\StandaloneMysql');
- $component->resource = $database;
-
- $result = $component->buildRestoreCommand('/tmp/test.dump');
-
- expect($result)->toContain('mysql -u $MYSQL_USER');
- expect($result)->toContain('< /tmp/test.dump');
-});
-
-test('buildRestoreCommand handles MariaDB without dumpAll', function () {
- $component = new Import;
- $component->dumpAll = false;
- $component->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
-
- $database = Mockery::mock('App\Models\StandaloneMariadb');
- $database->shouldReceive('getMorphClass')->andReturn('App\Models\StandaloneMariadb');
- $component->resource = $database;
-
- $result = $component->buildRestoreCommand('/tmp/test.dump');
-
- expect($result)->toContain('mariadb -u $MARIADB_USER');
- expect($result)->toContain('< /tmp/test.dump');
-});
-
-test('buildRestoreCommand handles MongoDB', function () {
- $component = new Import;
- $component->dumpAll = false;
- $component->mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive=';
-
- $database = Mockery::mock('App\Models\StandaloneMongodb');
- $database->shouldReceive('getMorphClass')->andReturn('App\Models\StandaloneMongodb');
- $component->resource = $database;
-
- $result = $component->buildRestoreCommand('/tmp/test.dump');
-
- expect($result)->toContain('mongorestore');
- expect($result)->toContain('/tmp/test.dump');
-});
From 2cfc9d6afc90ed65b2fbc8cc21917188ba490ea0 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 2 Feb 2026 22:44:26 +0100
Subject: [PATCH 06/11] test: remove database_name from
StartupExecutionCleanupTest
---
tests/Feature/StartupExecutionCleanupTest.php | 3 ---
1 file changed, 3 deletions(-)
diff --git a/tests/Feature/StartupExecutionCleanupTest.php b/tests/Feature/StartupExecutionCleanupTest.php
index 3a6b00208..0b03ca647 100644
--- a/tests/Feature/StartupExecutionCleanupTest.php
+++ b/tests/Feature/StartupExecutionCleanupTest.php
@@ -101,20 +101,17 @@ test('app:init marks stuck database backup executions as failed', function () {
$runningBackup1 = ScheduledDatabaseBackupExecution::create([
'scheduled_database_backup_id' => $scheduledBackup->id,
'status' => 'running',
- 'database_name' => 'test_db',
]);
$runningBackup2 = ScheduledDatabaseBackupExecution::create([
'scheduled_database_backup_id' => $scheduledBackup->id,
'status' => 'running',
- 'database_name' => 'test_db_2',
]);
// Create a successful backup (should not be affected)
$successfulBackup = ScheduledDatabaseBackupExecution::create([
'scheduled_database_backup_id' => $scheduledBackup->id,
'status' => 'success',
- 'database_name' => 'test_db_3',
'finished_at' => Carbon::now()->subMinutes(20),
]);
From dc04b786317346e2b9adc1d7881c6c5a596bfcc4 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 3 Feb 2026 00:23:44 +0100
Subject: [PATCH 07/11] feat(backup): always backup all database with the
backup job
---
app/Jobs/DatabaseBackupJob.php | 410 +++++++++++----------------------
1 file changed, 130 insertions(+), 280 deletions(-)
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index a585baa69..dae8e96ef 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -83,8 +83,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
- $databasesToBackup = null;
-
$this->team = Team::find($this->backup->team_id);
if (! $this->team) {
$this->backup->delete();
@@ -133,15 +131,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->database->postgres_user = 'postgres';
}
- $db = $envs->filter(function ($env) {
- return str($env)->startsWith('POSTGRES_DB=');
- })->first();
-
- if ($db) {
- $databasesToBackup = str($db)->after('POSTGRES_DB=')->value();
- } else {
- $databasesToBackup = $this->database->postgres_user;
- }
$this->postgres_password = $envs->filter(function ($env) {
return str($env)->startsWith('POSTGRES_PASSWORD=');
})->first();
@@ -161,16 +150,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($rootPassword) {
$this->database->mysql_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
}
-
- $db = $envs->filter(function ($env) {
- return str($env)->startsWith('MYSQL_DATABASE=');
- })->first();
-
- if ($db) {
- $databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
- } else {
- throw new \Exception('MYSQL_DATABASE not found');
- }
} elseif (str($databaseType)->contains('mariadb')) {
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName.'-'.$this->container_name;
@@ -190,26 +169,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->database->mariadb_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
}
}
-
- $db = $envs->filter(function ($env) {
- return str($env)->startsWith('MARIADB_DATABASE=');
- })->first();
-
- if ($db) {
- $databasesToBackup = str($db)->after('MARIADB_DATABASE=')->value();
- } else {
- $db = $envs->filter(function ($env) {
- return str($env)->startsWith('MYSQL_DATABASE=');
- })->first();
-
- if ($db) {
- $databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
- } else {
- throw new \Exception('MARIADB_DATABASE or MYSQL_DATABASE not found');
- }
- }
} elseif (str($databaseType)->contains('mongo')) {
- $databasesToBackup = ['*'];
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName.'-'.$this->container_name;
@@ -244,44 +204,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->container_name = $this->database->uuid;
$this->directory_name = $databaseName.'-'.$this->container_name;
$databaseType = $this->database->type();
- $databasesToBackup = data_get($this->backup, 'databases_to_backup');
- }
- if (blank($databasesToBackup)) {
- if (str($databaseType)->contains('postgres')) {
- $databasesToBackup = [$this->database->postgres_db];
- } elseif (str($databaseType)->contains('mongo')) {
- $databasesToBackup = ['*'];
- } elseif (str($databaseType)->contains('mysql')) {
- $databasesToBackup = [$this->database->mysql_database];
- } elseif (str($databaseType)->contains('mariadb')) {
- $databasesToBackup = [$this->database->mariadb_database];
- } else {
- return;
- }
- } else {
- if (str($databaseType)->contains('postgres')) {
- // Format: db1,db2,db3
- $databasesToBackup = explode(',', $databasesToBackup);
- $databasesToBackup = array_map('trim', $databasesToBackup);
- } elseif (str($databaseType)->contains('mongo')) {
- // Format: db1:collection1,collection2|db2:collection3,collection4
- // Only explode if it's a string, not if it's already an array
- if (is_string($databasesToBackup)) {
- $databasesToBackup = explode('|', $databasesToBackup);
- $databasesToBackup = array_map('trim', $databasesToBackup);
- }
- } elseif (str($databaseType)->contains('mysql')) {
- // Format: db1,db2,db3
- $databasesToBackup = explode(',', $databasesToBackup);
- $databasesToBackup = array_map('trim', $databasesToBackup);
- } elseif (str($databaseType)->contains('mariadb')) {
- // Format: db1,db2,db3
- $databasesToBackup = explode(',', $databasesToBackup);
- $databasesToBackup = array_map('trim', $databasesToBackup);
- } else {
- return;
- }
}
+
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
@@ -289,156 +213,131 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir().'/coolify'."/coolify-db-$ip";
}
- foreach ($databasesToBackup as $database) {
- // Generate unique UUID for each database backup execution
- $attempts = 0;
- do {
- $this->backup_log_uuid = (string) new Cuid2;
- $exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists();
- $attempts++;
- if ($attempts >= 3 && $exists) {
- throw new \Exception('Unable to generate unique UUID for backup execution after 3 attempts');
- }
- } while ($exists);
- $size = 0;
- $localBackupSucceeded = false;
- $s3UploadError = null;
+ // Generate unique UUID for backup execution
+ $attempts = 0;
+ do {
+ $this->backup_log_uuid = (string) new Cuid2;
+ $exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists();
+ $attempts++;
+ if ($attempts >= 3 && $exists) {
+ throw new \Exception('Unable to generate unique UUID for backup execution after 3 attempts');
+ }
+ } while ($exists);
- // Step 1: Create local backup
+ $size = 0;
+ $localBackupSucceeded = false;
+ $s3UploadError = null;
+
+ // Step 1: Create local backup (always dump all databases)
+ try {
+ if (str($databaseType)->contains('postgres')) {
+ $this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.sql.gz';
+ $this->backup_location = $this->backup_dir.$this->backup_file;
+ $this->backup_log = ScheduledDatabaseBackupExecution::create([
+ 'uuid' => $this->backup_log_uuid,
+ 'filename' => $this->backup_location,
+ 'scheduled_database_backup_id' => $this->backup->id,
+ 'local_storage_deleted' => false,
+ ]);
+ $this->backup_standalone_postgresql();
+ } elseif (str($databaseType)->contains('mongo')) {
+ $this->backup_file = '/mongo-dump-all-'.Carbon::now()->timestamp.'.tar.gz';
+ $this->backup_location = $this->backup_dir.$this->backup_file;
+ $this->backup_log = ScheduledDatabaseBackupExecution::create([
+ 'uuid' => $this->backup_log_uuid,
+ 'filename' => $this->backup_location,
+ 'scheduled_database_backup_id' => $this->backup->id,
+ 'local_storage_deleted' => false,
+ ]);
+ $this->backup_standalone_mongodb();
+ } elseif (str($databaseType)->contains('mysql')) {
+ $this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.sql.gz';
+ $this->backup_location = $this->backup_dir.$this->backup_file;
+ $this->backup_log = ScheduledDatabaseBackupExecution::create([
+ 'uuid' => $this->backup_log_uuid,
+ 'filename' => $this->backup_location,
+ 'scheduled_database_backup_id' => $this->backup->id,
+ 'local_storage_deleted' => false,
+ ]);
+ $this->backup_standalone_mysql();
+ } elseif (str($databaseType)->contains('mariadb')) {
+ $this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.sql.gz';
+ $this->backup_location = $this->backup_dir.$this->backup_file;
+ $this->backup_log = ScheduledDatabaseBackupExecution::create([
+ 'uuid' => $this->backup_log_uuid,
+ 'filename' => $this->backup_location,
+ 'scheduled_database_backup_id' => $this->backup->id,
+ 'local_storage_deleted' => false,
+ ]);
+ $this->backup_standalone_mariadb();
+ } else {
+ throw new \Exception('Unsupported database type');
+ }
+
+ $size = $this->calculate_size();
+
+ // Verify local backup succeeded
+ if ($size > 0) {
+ $localBackupSucceeded = true;
+ } else {
+ throw new \Exception('Local backup file is empty or was not created');
+ }
+ } catch (\Throwable $e) {
+ // Local backup failed
+ if ($this->backup_log) {
+ $this->backup_log->update([
+ 'status' => 'failed',
+ 'message' => $this->error_output ?? $this->backup_output ?? $e->getMessage(),
+ 'size' => $size,
+ 'filename' => null,
+ 's3_uploaded' => null,
+ ]);
+ }
+ $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->error_output ?? $this->backup_output ?? $e->getMessage()));
+ throw $e;
+ }
+
+ // Step 2: Upload to S3 if enabled (independent of local backup)
+ $localStorageDeleted = false;
+ if ($this->backup->save_s3 && $localBackupSucceeded) {
try {
- if (str($databaseType)->contains('postgres')) {
- $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp';
- if ($this->backup->dump_all) {
- $this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz';
- }
- $this->backup_location = $this->backup_dir.$this->backup_file;
- $this->backup_log = ScheduledDatabaseBackupExecution::create([
- 'uuid' => $this->backup_log_uuid,
- 'database_name' => $database,
- 'filename' => $this->backup_location,
- 'scheduled_database_backup_id' => $this->backup->id,
- 'local_storage_deleted' => false,
- ]);
- $this->backup_standalone_postgresql($database);
- } elseif (str($databaseType)->contains('mongo')) {
- if ($database === '*') {
- $database = 'all';
- $databaseName = 'all';
- } else {
- if (str($database)->contains(':')) {
- $databaseName = str($database)->before(':');
- } else {
- $databaseName = $database;
- }
- }
- $this->backup_file = "/mongo-dump-$databaseName-".Carbon::now()->timestamp.'.tar.gz';
- $this->backup_location = $this->backup_dir.$this->backup_file;
- $this->backup_log = ScheduledDatabaseBackupExecution::create([
- 'uuid' => $this->backup_log_uuid,
- 'database_name' => $databaseName,
- 'filename' => $this->backup_location,
- 'scheduled_database_backup_id' => $this->backup->id,
- 'local_storage_deleted' => false,
- ]);
- $this->backup_standalone_mongodb($database);
- } elseif (str($databaseType)->contains('mysql')) {
- $this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
- if ($this->backup->dump_all) {
- $this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz';
- }
- $this->backup_location = $this->backup_dir.$this->backup_file;
- $this->backup_log = ScheduledDatabaseBackupExecution::create([
- 'uuid' => $this->backup_log_uuid,
- 'database_name' => $database,
- 'filename' => $this->backup_location,
- 'scheduled_database_backup_id' => $this->backup->id,
- 'local_storage_deleted' => false,
- ]);
- $this->backup_standalone_mysql($database);
- } elseif (str($databaseType)->contains('mariadb')) {
- $this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
- if ($this->backup->dump_all) {
- $this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz';
- }
- $this->backup_location = $this->backup_dir.$this->backup_file;
- $this->backup_log = ScheduledDatabaseBackupExecution::create([
- 'uuid' => $this->backup_log_uuid,
- 'database_name' => $database,
- 'filename' => $this->backup_location,
- 'scheduled_database_backup_id' => $this->backup->id,
- 'local_storage_deleted' => false,
- ]);
- $this->backup_standalone_mariadb($database);
- } else {
- throw new \Exception('Unsupported database type');
- }
+ $this->upload_to_s3();
- $size = $this->calculate_size();
-
- // Verify local backup succeeded
- if ($size > 0) {
- $localBackupSucceeded = true;
- } else {
- throw new \Exception('Local backup file is empty or was not created');
+ // If local backup is disabled, delete the local file immediately after S3 upload
+ if ($this->backup->disable_local_backup) {
+ deleteBackupsLocally($this->backup_location, $this->server);
+ $localStorageDeleted = true;
}
} catch (\Throwable $e) {
- // Local backup failed
- if ($this->backup_log) {
- $this->backup_log->update([
- 'status' => 'failed',
- 'message' => $this->error_output ?? $this->backup_output ?? $e->getMessage(),
- 'size' => $size,
- 'filename' => null,
- 's3_uploaded' => null,
- ]);
- }
- $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->error_output ?? $this->backup_output ?? $e->getMessage(), $database));
+ // S3 upload failed but local backup succeeded
+ $s3UploadError = $e->getMessage();
+ }
+ }
- continue;
+ // Step 3: Update status and send notifications based on results
+ if ($localBackupSucceeded) {
+ $message = $this->backup_output;
+
+ if ($s3UploadError) {
+ $message = $message
+ ? $message."\n\nWarning: S3 upload failed: ".$s3UploadError
+ : 'Warning: S3 upload failed: '.$s3UploadError;
}
- // Step 2: Upload to S3 if enabled (independent of local backup)
- $localStorageDeleted = false;
- if ($this->backup->save_s3 && $localBackupSucceeded) {
- try {
- $this->upload_to_s3();
+ $this->backup_log->update([
+ 'status' => 'success',
+ 'message' => $message,
+ 'size' => $size,
+ 's3_uploaded' => $this->backup->save_s3 ? $this->s3_uploaded : null,
+ 'local_storage_deleted' => $localStorageDeleted,
+ ]);
- // If local backup is disabled, delete the local file immediately after S3 upload
- if ($this->backup->disable_local_backup) {
- deleteBackupsLocally($this->backup_location, $this->server);
- $localStorageDeleted = true;
- }
- } catch (\Throwable $e) {
- // S3 upload failed but local backup succeeded
- $s3UploadError = $e->getMessage();
- }
- }
-
- // Step 3: Update status and send notifications based on results
- if ($localBackupSucceeded) {
- $message = $this->backup_output;
-
- if ($s3UploadError) {
- $message = $message
- ? $message."\n\nWarning: S3 upload failed: ".$s3UploadError
- : 'Warning: S3 upload failed: '.$s3UploadError;
- }
-
- $this->backup_log->update([
- 'status' => 'success',
- 'message' => $message,
- 'size' => $size,
- 's3_uploaded' => $this->backup->save_s3 ? $this->s3_uploaded : null,
- 'local_storage_deleted' => $localStorageDeleted,
- ]);
-
- // Send appropriate notification
- if ($s3UploadError) {
- $this->team->notify(new BackupSuccessWithS3Warning($this->backup, $this->database, $database, $s3UploadError));
- } else {
- $this->team->notify(new BackupSuccess($this->backup, $this->database, $database));
- }
+ // Send appropriate notification
+ if ($s3UploadError) {
+ $this->team->notify(new BackupSuccessWithS3Warning($this->backup, $this->database, $s3UploadError));
+ } else {
+ $this->team->notify(new BackupSuccess($this->backup, $this->database));
}
}
if ($this->backup_log && $this->backup_log->status === 'success') {
@@ -458,7 +357,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
- private function backup_standalone_mongodb(string $databaseWithCollections): void
+ private function backup_standalone_mongodb(): void
{
try {
$url = $this->database->internal_db_url;
@@ -473,41 +372,14 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
\Log::info('MongoDB backup URL configured', ['has_url' => filled($url), 'using_env_vars' => blank($this->database->internal_db_url)]);
- if ($databaseWithCollections === 'all') {
- $commands[] = 'mkdir -p '.$this->backup_dir;
- if (str($this->database->image)->startsWith('mongo:4')) {
- $commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
- } else {
- $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --gzip --archive > $this->backup_location";
- }
+
+ $commands[] = 'mkdir -p '.$this->backup_dir;
+ if (str($this->database->image)->startsWith('mongo:4')) {
+ $commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
} else {
- if (str($databaseWithCollections)->contains(':')) {
- $databaseName = str($databaseWithCollections)->before(':');
- $collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
- } else {
- $databaseName = $databaseWithCollections;
- $collectionsToExclude = collect();
- }
- $commands[] = 'mkdir -p '.$this->backup_dir;
-
- // Validate and escape database name to prevent command injection
- validateShellSafePath($databaseName, 'database name');
- $escapedDatabaseName = escapeshellarg($databaseName);
-
- if ($collectionsToExclude->count() === 0) {
- if (str($this->database->image)->startsWith('mongo:4')) {
- $commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
- } else {
- $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --archive > $this->backup_location";
- }
- } else {
- if (str($this->database->image)->startsWith('mongo:4')) {
- $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
- } else {
- $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
- }
- }
+ $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --gzip --archive > $this->backup_location";
}
+
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -519,7 +391,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
- private function backup_standalone_postgresql(string $database): void
+ private function backup_standalone_postgresql(): void
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
@@ -527,14 +399,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=\"{$this->postgres_password}\"";
}
- if ($this->backup->dump_all) {
- $backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
- } else {
- // Validate and escape database name to prevent command injection
- validateShellSafePath($database, 'database name');
- $escapedDatabase = escapeshellarg($database);
- $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $escapedDatabase > $this->backup_location";
- }
+ $backupCommand .= " $this->container_name pg_dumpall --clean --if-exists --username {$this->database->postgres_user} | gzip > $this->backup_location";
$commands[] = $backupCommand;
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
@@ -548,18 +413,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
- private function backup_standalone_mysql(string $database): void
+ private function backup_standalone_mysql(): void
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
- if ($this->backup->dump_all) {
- $commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
- } else {
- // Validate and escape database name to prevent command injection
- validateShellSafePath($database, 'database name');
- $escapedDatabase = escapeshellarg($database);
- $commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $escapedDatabase > $this->backup_location";
- }
+ $commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" --all-databases --single-transaction --quick --lock-tables=false | gzip > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -571,18 +429,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
- private function backup_standalone_mariadb(string $database): void
+ private function backup_standalone_mariadb(): void
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
- if ($this->backup->dump_all) {
- $commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
- } else {
- // Validate and escape database name to prevent command injection
- validateShellSafePath($database, 'database name');
- $escapedDatabase = escapeshellarg($database);
- $commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $escapedDatabase > $this->backup_location";
- }
+ $commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" --all-databases --single-transaction --quick --lock-tables=false | gzip > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -709,9 +560,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
// Notify team about permanent failure
if ($this->team) {
- $databaseName = $log?->database_name ?? 'unknown';
$output = $this->backup_output ?? $exception?->getMessage() ?? 'Unknown error';
- $this->team->notify(new BackupFailed($this->backup, $this->database, $output, $databaseName));
+ $this->team->notify(new BackupFailed($this->backup, $this->database, $output));
}
}
}
From f9958d83dc37afdc07b188abf9f85ffb9661d467 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 3 Feb 2026 00:27:08 +0100
Subject: [PATCH 08/11] feat(notifcations): use just the coolify database name
in notifications
- no longer use the cluster database_name in backup notifications as we always backup all databases
---
app/Console/Commands/Emails.php | 3 +-
app/Notifications/Database/BackupFailed.php | 12 ++--
app/Notifications/Database/BackupSuccess.php | 12 ++--
.../Database/BackupSuccessWithS3Warning.php | 12 ++--
.../views/emails/backup-failed.blade.php | 2 +-
.../backup-success-with-s3-warning.blade.php | 2 +-
.../views/emails/backup-success.blade.php | 2 +-
.../project/database/import.blade.php | 66 +++++++++++--------
8 files changed, 57 insertions(+), 54 deletions(-)
diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php
index 43ba06804..60626ced8 100644
--- a/app/Console/Commands/Emails.php
+++ b/app/Console/Commands/Emails.php
@@ -54,7 +54,6 @@ class Emails extends Command
options: [
'updates' => 'Send Update Email to all users',
'emails-test' => 'Test',
- 'database-backup-statuses-daily' => 'Database - Backup Statuses (Daily)',
'application-deployment-success-daily' => 'Application - Deployment Success (Daily)',
'application-deployment-success' => 'Application - Deployment Success',
'application-deployment-failed' => 'Application - Deployment Failed',
@@ -167,7 +166,7 @@ class Emails extends Command
]);
}
$output = 'Because of an error, the backup of the database '.$db->name.' failed.';
- $this->mail = (new BackupFailed($backup, $db, $output, $backup->database_name ?? 'unknown'))->toMail();
+ $this->mail = (new BackupFailed($backup, $db, $output))->toMail();
$this->sendEmail();
break;
case 'backup-success':
diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php
index c2b21b1d5..0040d7ace 100644
--- a/app/Notifications/Database/BackupFailed.php
+++ b/app/Notifications/Database/BackupFailed.php
@@ -15,7 +15,7 @@ class BackupFailed extends CustomEmailNotification
public string $frequency;
- public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name)
+ public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output)
{
$this->onQueue('high');
$this->name = $database->name;
@@ -33,7 +33,6 @@ class BackupFailed extends CustomEmailNotification
$mail->subject("Coolify: [ACTION REQUIRED] Database Backup FAILED for {$this->database->name}");
$mail->view('emails.backup-failed', [
'name' => $this->name,
- 'database_name' => $this->database_name,
'frequency' => $this->frequency,
'output' => $this->output,
]);
@@ -45,7 +44,7 @@ class BackupFailed extends CustomEmailNotification
{
$message = new DiscordMessage(
title: ':cross_mark: Database backup failed',
- description: "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.",
+ description: "Database backup for {$this->name} has FAILED.",
color: DiscordMessage::errorColor(),
isCritical: true,
);
@@ -58,7 +57,7 @@ class BackupFailed extends CustomEmailNotification
public function toTelegram(): array
{
- $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
+ $message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
return [
'message' => $message,
@@ -70,14 +69,14 @@ class BackupFailed extends CustomEmailNotification
return new PushoverMessage(
title: 'Database backup failed',
level: 'error',
- message: "Database backup for {$this->name} (db:{$this->database_name}) was FAILED
Frequency: {$this->frequency} .
Reason: {$this->output}",
+ message: "Database backup for {$this->name} was FAILED
Frequency: {$this->frequency} .
Reason: {$this->output}",
);
}
public function toSlack(): SlackMessage
{
$title = 'Database backup failed';
- $description = "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.";
+ $description = "Database backup for {$this->name} has FAILED.";
$description .= "\n\n*Frequency:* {$this->frequency}";
$description .= "\n\n*Error Output:* {$this->output}";
@@ -99,7 +98,6 @@ class BackupFailed extends CustomEmailNotification
'event' => 'backup_failed',
'database_name' => $this->name,
'database_uuid' => $this->database->uuid,
- 'database_type' => $this->database_name,
'frequency' => $this->frequency,
'error_output' => $this->output,
'url' => $url,
diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php
index 3d2d8ece3..4624fcef8 100644
--- a/app/Notifications/Database/BackupSuccess.php
+++ b/app/Notifications/Database/BackupSuccess.php
@@ -15,7 +15,7 @@ class BackupSuccess extends CustomEmailNotification
public string $frequency;
- public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name)
+ public function __construct(ScheduledDatabaseBackup $backup, public $database)
{
$this->onQueue('high');
@@ -34,7 +34,6 @@ class BackupSuccess extends CustomEmailNotification
$mail->subject("Coolify: Backup successfully done for {$this->database->name}");
$mail->view('emails.backup-success', [
'name' => $this->name,
- 'database_name' => $this->database_name,
'frequency' => $this->frequency,
]);
@@ -45,7 +44,7 @@ class BackupSuccess extends CustomEmailNotification
{
$message = new DiscordMessage(
title: ':white_check_mark: Database backup successful',
- description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.",
+ description: "Database backup for {$this->name} was successful.",
color: DiscordMessage::successColor(),
);
@@ -56,7 +55,7 @@ class BackupSuccess extends CustomEmailNotification
public function toTelegram(): array
{
- $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
+ $message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
return [
'message' => $message,
@@ -68,14 +67,14 @@ class BackupSuccess extends CustomEmailNotification
return new PushoverMessage(
title: 'Database backup successful',
level: 'success',
- message: "Database backup for {$this->name} (db:{$this->database_name}) was successful.
Frequency: {$this->frequency}.",
+ message: "Database backup for {$this->name} was successful.
Frequency: {$this->frequency}.",
);
}
public function toSlack(): SlackMessage
{
$title = 'Database backup successful';
- $description = "Database backup for {$this->name} (db:{$this->database_name}) was successful.";
+ $description = "Database backup for {$this->name} was successful.";
$description .= "\n\n*Frequency:* {$this->frequency}";
@@ -96,7 +95,6 @@ class BackupSuccess extends CustomEmailNotification
'event' => 'backup_success',
'database_name' => $this->name,
'database_uuid' => $this->database->uuid,
- 'database_type' => $this->database_name,
'frequency' => $this->frequency,
'url' => $url,
];
diff --git a/app/Notifications/Database/BackupSuccessWithS3Warning.php b/app/Notifications/Database/BackupSuccessWithS3Warning.php
index ee24ef17d..d8ec01731 100644
--- a/app/Notifications/Database/BackupSuccessWithS3Warning.php
+++ b/app/Notifications/Database/BackupSuccessWithS3Warning.php
@@ -17,7 +17,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
public ?string $s3_storage_url = null;
- public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name, public $s3_error)
+ public function __construct(ScheduledDatabaseBackup $backup, public $database, public $s3_error)
{
$this->onQueue('high');
@@ -40,7 +40,6 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
$mail->subject("Coolify: Backup succeeded locally but S3 upload failed for {$this->database->name}");
$mail->view('emails.backup-success-with-s3-warning', [
'name' => $this->name,
- 'database_name' => $this->database_name,
'frequency' => $this->frequency,
's3_error' => $this->s3_error,
's3_storage_url' => $this->s3_storage_url,
@@ -53,7 +52,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
{
$message = new DiscordMessage(
title: ':warning: Database backup succeeded locally, S3 upload failed',
- description: "Database backup for {$this->name} (db:{$this->database_name}) was created successfully on local storage, but failed to upload to S3.",
+ description: "Database backup for {$this->name} was created successfully on local storage, but failed to upload to S3.",
color: DiscordMessage::warningColor(),
);
@@ -69,7 +68,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
public function toTelegram(): array
{
- $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} succeeded locally but failed to upload to S3.\n\nS3 Error:\n{$this->s3_error}";
+ $message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} succeeded locally but failed to upload to S3.\n\nS3 Error:\n{$this->s3_error}";
if ($this->s3_storage_url) {
$message .= "\n\nCheck S3 Configuration: {$this->s3_storage_url}";
@@ -82,7 +81,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
public function toPushover(): PushoverMessage
{
- $message = "Database backup for {$this->name} (db:{$this->database_name}) was created successfully on local storage, but failed to upload to S3.
Frequency: {$this->frequency}.
S3 Error: {$this->s3_error}";
+ $message = "Database backup for {$this->name} was created successfully on local storage, but failed to upload to S3.
Frequency: {$this->frequency}.
S3 Error: {$this->s3_error}";
if ($this->s3_storage_url) {
$message .= "
s3_storage_url}\">Check S3 Configuration";
@@ -98,7 +97,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
public function toSlack(): SlackMessage
{
$title = 'Database backup succeeded locally, S3 upload failed';
- $description = "Database backup for {$this->name} (db:{$this->database_name}) was created successfully on local storage, but failed to upload to S3.";
+ $description = "Database backup for {$this->name} was created successfully on local storage, but failed to upload to S3.";
$description .= "\n\n*Frequency:* {$this->frequency}";
$description .= "\n\n*S3 Error:* {$this->s3_error}";
@@ -124,7 +123,6 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
'event' => 'backup_success_with_s3_warning',
'database_name' => $this->name,
'database_uuid' => $this->database->uuid,
- 'database_type' => $this->database_name,
'frequency' => $this->frequency,
's3_error' => $this->s3_error,
'url' => $url,
diff --git a/resources/views/emails/backup-failed.blade.php b/resources/views/emails/backup-failed.blade.php
index 013c8bc98..de6a7c981 100644
--- a/resources/views/emails/backup-failed.blade.php
+++ b/resources/views/emails/backup-failed.blade.php
@@ -1,5 +1,5 @@
-Database backup for {{ $name }} @if($database_name)(db:{{ $database_name }})@endif with frequency of {{ $frequency }} was FAILED.
+Database backup for {{ $name }} with frequency of {{ $frequency }} was FAILED.
### Reason
diff --git a/resources/views/emails/backup-success-with-s3-warning.blade.php b/resources/views/emails/backup-success-with-s3-warning.blade.php
index 5d2f25851..1cc2e4549 100644
--- a/resources/views/emails/backup-success-with-s3-warning.blade.php
+++ b/resources/views/emails/backup-success-with-s3-warning.blade.php
@@ -1,5 +1,5 @@
-Database backup for {{ $name }} @if($database_name)(db:{{ $database_name }})@endif with frequency of {{ $frequency }} succeeded locally but failed to upload to S3.
+Database backup for {{ $name }} with frequency of {{ $frequency }} succeeded locally but failed to upload to S3.
S3 Error: {{ $s3_error }}
diff --git a/resources/views/emails/backup-success.blade.php b/resources/views/emails/backup-success.blade.php
index d06bca6ce..0d54a254c 100644
--- a/resources/views/emails/backup-success.blade.php
+++ b/resources/views/emails/backup-success.blade.php
@@ -1,3 +1,3 @@
-Database backup for {{ $name }} @if($database_name)(db:{{ $database_name }})@endif with frequency of {{ $frequency }} was successful.
+Database backup for {{ $name }} with frequency of {{ $frequency }} was successful.
diff --git a/resources/views/livewire/project/database/import.blade.php b/resources/views/livewire/project/database/import.blade.php
index 666abb3b3..fa360e35b 100644
--- a/resources/views/livewire/project/database/import.blade.php
+++ b/resources/views/livewire/project/database/import.blade.php
@@ -61,39 +61,49 @@
@if (str($resourceStatus)->startsWith('running'))
{{-- Restore Command Configuration --}}
@if ($resourceDbType === 'standalone-postgresql')
- @if ($dumpAll)
-
- @else
-
-
-
You can add "--clean" to drop objects before creating them, avoiding
- conflicts.
-
You can add "--verbose" to log more things.
+
+
Import Options
+
+
+
- @endif
-
-
+
+ @if ($restoreCommandText)
+
{{ $restoreCommandText }}
+ @endif
@elseif ($resourceDbType === 'standalone-mysql')
- @if ($dumpAll)
-
- @else
-
- @endif
-
-
+
+
Import Options
+
+
+
+
+
+ @if ($restoreCommandText)
+
{{ $restoreCommandText }}
+ @endif
@elseif ($resourceDbType === 'standalone-mariadb')
- @if ($dumpAll)
-
- @else
-
- @endif
-
-
+
+
Import Options
+
+
+
+
+
+ @if ($restoreCommandText)
+
{{ $restoreCommandText }}
+ @endif
@endif
From fc5d7f31aeb17a68f7a4312aa014b7a0c91af2fc Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 3 Feb 2026 00:28:45 +0100
Subject: [PATCH 09/11] chore(notifications): remove unused email view
---
resources/views/emails/daily-backup.blade.php | 19 -------------------
1 file changed, 19 deletions(-)
delete mode 100644 resources/views/emails/daily-backup.blade.php
diff --git a/resources/views/emails/daily-backup.blade.php b/resources/views/emails/daily-backup.blade.php
deleted file mode 100644
index 25d887387..000000000
--- a/resources/views/emails/daily-backup.blade.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
-@foreach ($databases as $database_name => $databases)
-
-@if(data_get($databases,'failed_count') > 0)
-
-
-
-"{{ $database_name }}" backups: There were some failed backups. Please login and check the logs for more details.
-
-
-
-@else
-
-"{{ $database_name }}" backups: All backups were successful.
-
-@endif
-
-@endforeach
-
From 9f8b95777e639d849c4b9d2ec20e4df921bfc8ad Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 3 Feb 2026 00:46:32 +0100
Subject: [PATCH 10/11] feat(ui): add legacy import command options to the ui
---
.../project/database/import.blade.php | 24 +++++++++----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/resources/views/livewire/project/database/import.blade.php b/resources/views/livewire/project/database/import.blade.php
index fa360e35b..91efcd75b 100644
--- a/resources/views/livewire/project/database/import.blade.php
+++ b/resources/views/livewire/project/database/import.blade.php
@@ -64,10 +64,10 @@
Import Options
-
-
+
+
@@ -79,10 +79,10 @@
Import Options
-
-
+
+
@@ -94,10 +94,10 @@
Import Options
-
-
+
+
From 1095f248bfe6ddc30e48664a591706bda768e8cb Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 3 Feb 2026 00:49:25 +0100
Subject: [PATCH 11/11] feat(backup): add new restore functionality
---
app/Livewire/Project/Database/Import.php | 80 +++++++++++++-----------
1 file changed, 42 insertions(+), 38 deletions(-)
diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php
index 7d37bd473..23c077a34 100644
--- a/app/Livewire/Project/Database/Import.php
+++ b/app/Livewire/Project/Database/Import.php
@@ -139,7 +139,9 @@ class Import extends Component
public array $importCommands = [];
- public bool $dumpAll = false;
+ public bool $legacyDumpAll = false;
+
+ public bool $legacySingleDb = false;
public string $restoreCommandText = '';
@@ -147,11 +149,11 @@ class Import extends Component
public ?int $activityId = null;
- public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
+ public string $postgresqlRestoreCommand = 'gunzip -c $tmpPath | psql -X -U ${POSTGRES_USER} -d postgres';
- public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
+ public string $mysqlRestoreCommand = 'gunzip -c $tmpPath | mysql -u root -p$MYSQL_ROOT_PASSWORD';
- public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
+ public string $mariadbRestoreCommand = 'gunzip -c $tmpPath | mariadb -u root -p$MARIADB_ROOT_PASSWORD';
public string $mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive=';
@@ -206,7 +208,23 @@ class Import extends Component
$this->loadAvailableS3Storages();
}
- public function updatedDumpAll($value)
+ public function updatedLegacyDumpAll($value)
+ {
+ if ($value) {
+ $this->legacySingleDb = false;
+ }
+ $this->updateRestoreCommands();
+ }
+
+ public function updatedLegacySingleDb($value)
+ {
+ if ($value) {
+ $this->legacyDumpAll = false;
+ }
+ $this->updateRestoreCommands();
+ }
+
+ private function updateRestoreCommands()
{
$morphClass = $this->resource->getMorphClass();
@@ -225,7 +243,7 @@ class Import extends Component
switch ($morphClass) {
case \App\Models\StandaloneMariadb::class:
case 'mariadb':
- if ($value === true) {
+ if ($this->legacyDumpAll) {
$this->mariadbRestoreCommand = <<<'EOD'
for pid in $(mariadb -u root -p$MARIADB_ROOT_PASSWORD -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do
mariadb -u root -p$MARIADB_ROOT_PASSWORD -e "KILL $pid" 2>/dev/null || true
@@ -234,14 +252,15 @@ mariadb -u root -p$MARIADB_ROOT_PASSWORD -N -e "SELECT CONCAT('DROP DATABASE IF
mariadb -u root -p$MARIADB_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS \`${MARIADB_DATABASE:-default}\`;" && \
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | sed -e '/^CREATE DATABASE/d' -e '/^USE \`mysql\`/d' | mariadb -u root -p$MARIADB_ROOT_PASSWORD ${MARIADB_DATABASE:-default}
EOD;
- $this->restoreCommandText = $this->mariadbRestoreCommand.' && (gunzip -cf
2>/dev/null || cat ) | mariadb -u root -p$MARIADB_ROOT_PASSWORD ${MARIADB_DATABASE:-default}';
+ } elseif ($this->legacySingleDb) {
+ $this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE < $tmpPath';
} else {
- $this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
+ $this->mariadbRestoreCommand = 'gunzip -c $tmpPath | mariadb -u root -p$MARIADB_ROOT_PASSWORD';
}
break;
case \App\Models\StandaloneMysql::class:
case 'mysql':
- if ($value === true) {
+ if ($this->legacyDumpAll) {
$this->mysqlRestoreCommand = <<<'EOD'
for pid in $(mysql -u root -p$MYSQL_ROOT_PASSWORD -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "KILL $pid" 2>/dev/null || true
@@ -250,26 +269,29 @@ mysql -u root -p$MYSQL_ROOT_PASSWORD -N -e "SELECT CONCAT('DROP DATABASE IF EXIS
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS \`${MYSQL_DATABASE:-default}\`;" && \
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | sed -e '/^CREATE DATABASE/d' -e '/^USE \`mysql\`/d' | mysql -u root -p$MYSQL_ROOT_PASSWORD ${MYSQL_DATABASE:-default}
EOD;
- $this->restoreCommandText = $this->mysqlRestoreCommand.' && (gunzip -cf 2>/dev/null || cat ) | mysql -u root -p$MYSQL_ROOT_PASSWORD ${MYSQL_DATABASE:-default}';
+ } elseif ($this->legacySingleDb) {
+ $this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < $tmpPath';
} else {
- $this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
+ $this->mysqlRestoreCommand = 'gunzip -c $tmpPath | mysql -u root -p$MYSQL_ROOT_PASSWORD';
}
break;
case \App\Models\StandalonePostgresql::class:
case 'postgresql':
- if ($value === true) {
+ if ($this->legacyDumpAll) {
$this->postgresqlRestoreCommand = <<<'EOD'
psql -U ${POSTGRES_USER} -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid()" && \
psql -U ${POSTGRES_USER} -t -c "SELECT datname FROM pg_database WHERE NOT datistemplate" | xargs -I {} dropdb -U ${POSTGRES_USER} --if-exists {} && \
-createdb -U ${POSTGRES_USER} ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}
+createdb -U ${POSTGRES_USER} ${POSTGRES_DB:-${POSTGRES_USER:-postgres}} && \
+(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}
EOD;
- $this->restoreCommandText = $this->postgresqlRestoreCommand.' && (gunzip -cf 2>/dev/null || cat ) | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
+ } elseif ($this->legacySingleDb) {
+ $this->postgresqlRestoreCommand = 'pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}} $tmpPath';
} else {
- $this->postgresqlRestoreCommand = 'pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
+ $this->postgresqlRestoreCommand = 'gunzip -c $tmpPath | psql -X -U ${POSTGRES_USER} -d postgres';
+ $this->restoreCommandText = 'Default: Restore from pg_dumpall file (gzipped SQL)';
}
break;
}
-
}
public function getContainers()
@@ -737,37 +759,19 @@ EOD;
switch ($morphClass) {
case \App\Models\StandaloneMariadb::class:
case 'mariadb':
- $restoreCommand = $this->mariadbRestoreCommand;
- if ($this->dumpAll) {
- $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mariadb -u root -p\$MARIADB_ROOT_PASSWORD \${MARIADB_DATABASE:-default}";
- } else {
- $restoreCommand .= " < {$tmpPath}";
- }
+ $restoreCommand = str_replace('$tmpPath', $tmpPath, $this->mariadbRestoreCommand);
break;
case \App\Models\StandaloneMysql::class:
case 'mysql':
- $restoreCommand = $this->mysqlRestoreCommand;
- if ($this->dumpAll) {
- $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mysql -u root -p\$MYSQL_ROOT_PASSWORD \${MYSQL_DATABASE:-default}";
- } else {
- $restoreCommand .= " < {$tmpPath}";
- }
+ $restoreCommand = str_replace('$tmpPath', $tmpPath, $this->mysqlRestoreCommand);
break;
case \App\Models\StandalonePostgresql::class:
case 'postgresql':
- $restoreCommand = $this->postgresqlRestoreCommand;
- if ($this->dumpAll) {
- $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | psql -U \${POSTGRES_USER} -d \${POSTGRES_DB:-\${POSTGRES_USER:-postgres}}";
- } else {
- $restoreCommand .= " {$tmpPath}";
- }
+ $restoreCommand = str_replace('$tmpPath', $tmpPath, $this->postgresqlRestoreCommand);
break;
case \App\Models\StandaloneMongodb::class:
case 'mongodb':
- $restoreCommand = $this->mongodbRestoreCommand;
- if ($this->dumpAll === false) {
- $restoreCommand .= "{$tmpPath}";
- }
+ $restoreCommand = $this->mongodbRestoreCommand.$tmpPath;
break;
default:
$restoreCommand = '';