From 68fb562fd3961638836faca2c8317509244635a8 Mon Sep 17 00:00:00 2001 From: Io Levanter Date: Wed, 18 Feb 2026 03:18:21 -0800 Subject: [PATCH] feat: enable database detection and backup support for Docker Compose deployments via GitHub App - Add nullable application_id FK to service_databases table so a ServiceDatabase can belong to either a Service or an Application - Create ServiceDatabase records in the Application model parsing path of parseDockerComposeFile() when isDatabaseImage() detects a database - Add helper methods (getServer, getOwnerUuid, getNetwork, isApplicationOwned) to ServiceDatabase for owner-agnostic resolution - Update all code paths that resolve server/network/uuid through ServiceDatabase to use the new helpers instead of hardcoded ->service-> chains - Add composeDatabases() relationship to Application model with cleanup on build_pack change and force delete - Add Backups tab to Application configuration UI for dockercompose apps - Create ComposeBackups Livewire component with database selector Files modified: - database/migrations: new migration for application_id column - app/Models/ServiceDatabase.php: application() relationship + helpers - app/Models/Application.php: composeDatabases() relationship + cleanup - bootstrap/helpers/shared.php: ServiceDatabase creation in App path - app/Jobs/DatabaseBackupJob.php: owner-agnostic server/uuid resolution - app/Actions/Database/Start/StopDatabaseProxy.php: same - app/Livewire/Project/Database/*: same - app/Models/ScheduledDatabaseBackup.php: same - app/Models/LocalFileVolume.php: same - bootstrap/helpers/databases.php: same - routes/web.php: new route + updated download handler - resources/views: Backups tab + compose-backups view Resolves #7528 --- app/Actions/Database/StartDatabaseProxy.php | 6 +- app/Actions/Database/StopDatabaseProxy.php | 2 +- app/Jobs/DatabaseBackupJob.php | 9 +- .../Project/Application/ComposeBackups.php | 64 ++++++++++++++ app/Livewire/Project/Database/BackupEdit.php | 10 ++- .../Project/Database/BackupExecutions.php | 9 +- app/Livewire/Project/Database/Import.php | 6 +- app/Models/Application.php | 13 +++ app/Models/LocalFileVolume.php | 9 ++ app/Models/ScheduledDatabaseBackup.php | 4 +- app/Models/ServiceDatabase.php | 86 +++++++++++++++++-- bootstrap/helpers/databases.php | 2 +- bootstrap/helpers/shared.php | 43 +++++++++- ...lication_id_to_service_databases_table.php | 38 ++++++++ .../application/compose-backups.blade.php | 51 +++++++++++ .../application/configuration.blade.php | 6 ++ routes/web.php | 3 +- 17 files changed, 331 insertions(+), 30 deletions(-) create mode 100644 app/Livewire/Project/Application/ComposeBackups.php create mode 100644 database/migrations/2026_02_18_000000_add_application_id_to_service_databases_table.php create mode 100644 resources/views/livewire/project/application/compose-backups.blade.php diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index c634f14bac..e8c7fb7bf3 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -31,9 +31,9 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { $databaseType = $database->databaseType(); - $network = $database->service->uuid; - $server = data_get($database, 'service.destination.server'); - $containerName = "{$database->name}-{$database->service->uuid}"; + $network = $database->getNetwork(); + $server = $database->getServer(); + $containerName = "{$database->name}-{$database->getOwnerUuid()}"; } $internalPort = match ($databaseType) { 'standalone-mariadb', 'standalone-mysql' => 3306, diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 96a1097662..51b73eda31 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -25,7 +25,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $server = data_get($database, 'destination.server'); $uuid = $database->uuid; if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { - $server = data_get($database, 'service.server'); + $server = $database->getServer(); } instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index a585baa690..91948deba2 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -93,7 +93,7 @@ public function handle(): void } if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $this->database = data_get($this->backup, 'database'); - $this->server = $this->database->service->server; + $this->server = $this->database->getServer(); $this->s3 = $this->backup->s3; } else { $this->database = data_get($this->backup, 'database'); @@ -115,8 +115,9 @@ public function handle(): void } if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $databaseType = $this->database->databaseType(); - $serviceUuid = $this->database->service->uuid; - $serviceName = str($this->database->service->name)->slug(); + $serviceUuid = $this->database->getOwnerUuid(); + $owner = $this->database->getOwner(); + $serviceName = str($owner->name)->slug(); if (str($databaseType)->contains('postgres')) { $this->container_name = "{$this->database->name}-$serviceUuid"; $this->directory_name = $serviceName.'-'.$this->container_name; @@ -630,7 +631,7 @@ private function upload_to_s3(): void $endpoint = $this->s3->endpoint; $this->s3->testConnection(shouldSave: true); if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { - $network = $this->database->service->destination->network; + $network = $this->database->getNetwork(); } else { $network = $this->database->destination->network; } diff --git a/app/Livewire/Project/Application/ComposeBackups.php b/app/Livewire/Project/Application/ComposeBackups.php new file mode 100644 index 0000000000..57bb10fc22 --- /dev/null +++ b/app/Livewire/Project/Application/ComposeBackups.php @@ -0,0 +1,64 @@ + '$refresh']; + + public function mount() + { + try { + $this->parameters = get_route_parameters(); + $this->application = Application::whereUuid($this->parameters['application_uuid'])->first(); + if (! $this->application || $this->application->build_pack !== 'dockercompose') { + return redirect()->route('dashboard'); + } + $this->authorize('view', $this->application); + + // Auto-select first database if available + $firstDb = $this->application->composeDatabases()->first(); + if ($firstDb) { + $this->selectDatabase($firstDb->uuid); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function selectDatabase(string $uuid) + { + $this->selectedDatabaseUuid = $uuid; + $this->selectedDatabase = $this->application->composeDatabases()->whereUuid($uuid)->first(); + + if ($this->selectedDatabase) { + $dbType = $this->selectedDatabase->databaseType(); + $supportedTypes = ['mysql', 'mariadb', 'postgres', 'mongo']; + $this->isImportSupported = collect($supportedTypes)->contains(fn ($type) => str_contains($dbType, $type)); + } + } + + public function render() + { + return view('livewire.project.application.compose-backups', [ + 'composeDatabases' => $this->application->composeDatabases()->get(), + ]); + } +} diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 35262d7b0b..4a62b0567c 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -157,7 +157,7 @@ public function delete($password) try { $server = null; if ($this->backup->database instanceof \App\Models\ServiceDatabase) { - $server = $this->backup->database->service->destination->server; + $server = $this->backup->database->getServer(); } elseif ($this->backup->database->destination && $this->backup->database->destination->server) { $server = $this->backup->database->destination->server; } @@ -185,6 +185,14 @@ public function delete($password) if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $serviceDatabase = $this->backup->database; + if ($serviceDatabase->isApplicationOwned()) { + return redirect()->route('project.application.compose-backups', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'environment_uuid' => $this->parameters['environment_uuid'], + 'application_uuid' => $serviceDatabase->application->uuid, + ]); + } + return redirect()->route('project.service.database.backups', [ 'project_uuid' => $this->parameters['project_uuid'], 'environment_uuid' => $this->parameters['environment_uuid'], diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 44f903fcc1..2b7d8c80a1 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -78,9 +78,10 @@ public function deleteBackup($executionId, $password) return; } - $server = $execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class - ? $execution->scheduledDatabaseBackup->database->service->destination->server - : $execution->scheduledDatabaseBackup->database->destination->server; + $db = $execution->scheduledDatabaseBackup->database; + $server = $db->getMorphClass() === \App\Models\ServiceDatabase::class + ? $db->getServer() + : $db->destination->server; try { if ($execution->filename) { @@ -182,7 +183,7 @@ public function server() $server = null; if ($this->database instanceof \App\Models\ServiceDatabase) { - $server = $this->database->service->destination->server; + $server = $this->database->getServer(); } elseif ($this->database->destination && $this->database->destination->server) { $server = $this->database->destination->server; } diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index 7d37bd473d..fa02bb7f0d 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -314,12 +314,12 @@ public function getContainers() // Handle ServiceDatabase server access differently if ($resource->getMorphClass() === \App\Models\ServiceDatabase::class) { - $server = $resource->service?->server; + $server = $resource->getServer(); if (! $server) { abort(404, 'Server not found for this service database.'); } $this->serverId = $server->id; - $this->container = $resource->name.'-'.$resource->service->uuid; + $this->container = $resource->name.'-'.$resource->getOwnerUuid(); $this->resourceUuid = $resource->uuid; // Use ServiceDatabase's own UUID // Determine database type for ServiceDatabase @@ -633,7 +633,7 @@ public function restoreFromS3() // Get the database destination network if ($this->resource->getMorphClass() === \App\Models\ServiceDatabase::class) { - $destinationNetwork = $this->resource->service->destination->network ?? 'coolify'; + $destinationNetwork = $this->resource->getNetwork() ?? 'coolify'; } else { $destinationNetwork = $this->resource->destination->network ?? 'coolify'; } diff --git a/app/Models/Application.php b/app/Models/Application.php index d6c222a97a..32500d17d1 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -187,6 +187,11 @@ protected static function booted() $application->docker_compose_domains = null; $application->docker_compose_raw = null; + // Remove compose databases and their backups + $application->composeDatabases()->each(function ($db) { + $db->delete(); + }); + // Remove SERVICE_FQDN_* and SERVICE_URL_* environment variables $application->environment_variables() ->where(function ($q) { @@ -235,6 +240,9 @@ protected static function booted() }); static::forceDeleting(function ($application) { $application->update(['fqdn' => null]); + $application->composeDatabases()->each(function ($db) { + $db->delete(); + }); $application->settings()->delete(); $application->persistentStorages()->delete(); $application->environment_variables()->delete(); @@ -490,6 +498,11 @@ public function settings() return $this->hasOne(ApplicationSetting::class); } + public function composeDatabases() + { + return $this->hasMany(ServiceDatabase::class); + } + public function persistentStorages() { return $this->morphMany(LocalPersistentVolume::class, 'resource'); diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index 9d7095cb5d..51a44b7dea 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -51,6 +51,9 @@ public function loadStorageOnServer() if ($isService) { $workdir = $this->resource->service->workdir(); $server = $this->resource->service->server; + } elseif ($this->resource instanceof \App\Models\ServiceDatabase && $this->resource->isApplicationOwned()) { + $workdir = $this->resource->workdir(); + $server = $this->resource->getServer(); } else { $workdir = $this->resource->workdir(); $server = $this->resource->destination->server; @@ -86,6 +89,9 @@ public function deleteStorageOnServer() if ($isService) { $workdir = $this->resource->service->workdir(); $server = $this->resource->service->server; + } elseif ($this->resource instanceof \App\Models\ServiceDatabase && $this->resource->isApplicationOwned()) { + $workdir = $this->resource->workdir(); + $server = $this->resource->getServer(); } else { $workdir = $this->resource->workdir(); $server = $this->resource->destination->server; @@ -123,6 +129,9 @@ public function saveStorageOnServer() if ($isService) { $workdir = $this->resource->service->workdir(); $server = $this->resource->service->server; + } elseif ($this->resource instanceof \App\Models\ServiceDatabase && $this->resource->isApplicationOwned()) { + $workdir = $this->resource->workdir(); + $server = $this->resource->getServer(); } else { $workdir = $this->resource->workdir(); $server = $this->resource->destination->server; diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 3ade21df8b..7a8c12ba2f 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -67,8 +67,7 @@ public function server() { if ($this->database) { if ($this->database instanceof ServiceDatabase) { - $destination = data_get($this->database->service, 'destination'); - $server = data_get($destination, 'server'); + $server = $this->database->getServer(); } else { $destination = data_get($this->database, 'destination'); $server = data_get($destination, 'server'); @@ -80,4 +79,5 @@ public function server() return null; } + } diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index f6a39cfe48..d15338abe3 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -27,7 +27,10 @@ protected static function booted() public static function ownedByCurrentTeamAPI(int $teamId) { - return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + return ServiceDatabase::where(function ($query) use ($teamId) { + $query->whereRelation('service.environment.project.team', 'id', $teamId) + ->orWhereRelation('application.environment.project.team', 'id', $teamId); + })->orderBy('name'); } /** @@ -36,7 +39,12 @@ public static function ownedByCurrentTeamAPI(int $teamId) */ public static function ownedByCurrentTeam() { - return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + $teamId = currentTeam()->id; + + return ServiceDatabase::where(function ($query) use ($teamId) { + $query->whereRelation('service.environment.project.team', 'id', $teamId) + ->orWhereRelation('application.environment.project.team', 'id', $teamId); + })->orderBy('name'); } /** @@ -49,10 +57,60 @@ public static function ownedByCurrentTeamCached() }); } + /** + * Check if this ServiceDatabase is owned by an Application (Docker Compose via GitHub App). + */ + public function isApplicationOwned(): bool + { + return filled($this->application_id) && is_null($this->service_id); + } + + /** + * Get the owning resource (Service or Application). + */ + public function getOwner(): Service|Application|null + { + return $this->service ?? $this->application; + } + + /** + * Get the UUID of the owning resource. + */ + public function getOwnerUuid(): ?string + { + return $this->getOwner()?->uuid; + } + + /** + * Get the server for this database, resolving through either Service or Application. + */ + public function getServer(): ?Server + { + if ($this->service_id) { + return $this->service?->server; + } + + return $this->application?->destination?->server; + } + + /** + * Get the network for this database. + */ + public function getNetwork(): ?string + { + if ($this->service_id) { + return $this->service?->uuid; + } + + return $this->application?->uuid; + } + public function restart() { - $container_id = $this->name.'-'.$this->service->uuid; - remote_process(["docker restart {$container_id}"], $this->service->server); + $ownerUuid = $this->getOwnerUuid(); + $server = $this->getServer(); + $container_id = $this->name.'-'.$ownerUuid; + remote_process(["docker restart {$container_id}"], $server); } public function isRunning() @@ -114,8 +172,9 @@ public function databaseType() public function getServiceDatabaseUrl() { $port = $this->public_port; - $realIp = $this->service->server->ip; - if ($this->service->server->isLocalhost() || isDev()) { + $server = $this->getServer(); + $realIp = $server->ip; + if ($server->isLocalhost() || isDev()) { $realIp = base_ip(); } @@ -124,12 +183,18 @@ public function getServiceDatabaseUrl() public function team() { - return data_get($this, 'environment.project.team'); + if ($this->service_id) { + return data_get($this, 'service.environment.project.team'); + } + + return data_get($this, 'application.environment.project.team'); } public function workdir() { - return service_configuration_dir()."/{$this->service->uuid}"; + $ownerUuid = $this->getOwnerUuid(); + + return service_configuration_dir()."/{$ownerUuid}"; } public function service() @@ -137,6 +202,11 @@ public function service() return $this->belongsTo(Service::class); } + public function application() + { + return $this->belongsTo(Application::class); + } + public function persistentStorages() { return $this->morphMany(LocalPersistentVolume::class, 'resource'); diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 5df36db336..1b615028f1 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -346,7 +346,7 @@ function deleteOldBackupsLocally($backup): Collection $server = null; if ($backup->database_type === \App\Models\ServiceDatabase::class) { - $server = $backup->database->service->server; + $server = $backup->database->getServer(); } else { $server = $backup->database->destination->server; } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 2173e76199..6008d376ab 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -555,9 +555,12 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } - // ServiceDatabase has a different relationship path: service->environment->project->team_id + // ServiceDatabase has a different relationship path depending on ownership if ($resource instanceof \App\Models\ServiceDatabase) { - if ($resource->service?->environment?->project?->team_id === $teamId) { + if ($resource->service_id && $resource->service?->environment?->project?->team_id === $teamId) { + return $resource; + } + if ($resource->application_id && $resource->application?->environment?->project?->team_id === $teamId) { return $resource; } @@ -2418,6 +2421,32 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $isDatabase = isDatabaseImage($image, $service); data_set($service, 'is_database', $isDatabase); + // Create/update ServiceDatabase records for detected databases (skip preview deployments) + if ($isDatabase && $pull_request_id === 0) { + if ($isNew) { + ServiceDatabase::create([ + 'name' => $serviceName, + 'image' => $image, + 'application_id' => $resource->id, + ]); + } else { + $existingDb = ServiceDatabase::where([ + 'name' => $serviceName, + 'application_id' => $resource->id, + ])->first(); + if (is_null($existingDb)) { + ServiceDatabase::create([ + 'name' => $serviceName, + 'image' => $image, + 'application_id' => $resource->id, + ]); + } elseif ($existingDb->image !== $image) { + $existingDb->image = $image; + $existingDb->save(); + } + } + } + // Collect/create/update networks if ($serviceNetworks->count() > 0) { foreach ($serviceNetworks as $networkName => $networkDetails) { @@ -2795,6 +2824,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $service; }); + // Clean up stale ServiceDatabase records for services no longer in the compose file + if ($pull_request_id === 0) { + $currentServiceNames = $services->keys()->toArray(); + ServiceDatabase::where('application_id', $resource->id) + ->whereNotIn('name', $currentServiceNames) + ->each(function ($staleDb) { + $staleDb->delete(); + }); + } + if ($pull_request_id !== 0) { $services->each(function ($service, $serviceName) use ($pull_request_id, $services) { $services[addPreviewDeploymentSuffix($serviceName, $pull_request_id)] = $service; diff --git a/database/migrations/2026_02_18_000000_add_application_id_to_service_databases_table.php b/database/migrations/2026_02_18_000000_add_application_id_to_service_databases_table.php new file mode 100644 index 0000000000..1ad15cb280 --- /dev/null +++ b/database/migrations/2026_02_18_000000_add_application_id_to_service_databases_table.php @@ -0,0 +1,38 @@ +foreignId('application_id')->nullable()->after('service_id')->constrained()->cascadeOnDelete(); + }); + + // Make service_id nullable so Application-owned databases don't need it + Schema::table('service_databases', function (Blueprint $table) { + $table->unsignedBigInteger('service_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('service_databases', function (Blueprint $table) { + $table->dropForeign(['application_id']); + $table->dropColumn('application_id'); + }); + + Schema::table('service_databases', function (Blueprint $table) { + $table->unsignedBigInteger('service_id')->nullable(false)->change(); + }); + } +}; diff --git a/resources/views/livewire/project/application/compose-backups.blade.php b/resources/views/livewire/project/application/compose-backups.blade.php new file mode 100644 index 0000000000..65633bfa20 --- /dev/null +++ b/resources/views/livewire/project/application/compose-backups.blade.php @@ -0,0 +1,51 @@ +
+

Docker Compose Database Backups

+ + @if ($composeDatabases->isEmpty()) +
+ No database services detected in your Docker Compose file. +
+ Database images (PostgreSQL, MySQL, MariaDB, MongoDB, etc.) are automatically detected when deploying. +
+ @else +
+ {{-- Database selector --}} +
+ @foreach ($composeDatabases as $db) + + @endforeach +
+ + {{-- Backup management for selected database --}} + @if ($selectedDatabase) + @if ($selectedDatabase->isBackupSolutionAvailable()) +
+
+

Scheduled Backups for {{ $selectedDatabase->name }}

+ @can('update', $selectedDatabase) + + + + @endcan +
+ +
+ @else +
+ Backups are not available for this database type ({{ $selectedDatabase->databaseType() }}). +
+ Supported database types: PostgreSQL, MySQL, MariaDB, MongoDB. +
+ @endif + @endif +
+ @endif +
diff --git a/resources/views/livewire/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php index 34c859a185..e612045965 100644 --- a/resources/views/livewire/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -50,6 +50,10 @@ Preview Deployments @endif + @if ($application->build_pack === 'dockercompose' && $application->composeDatabases()->count() > 0) + Backups + @endif @if ($application->build_pack !== 'dockercompose') Healthcheck @@ -102,6 +106,8 @@ @elseif ($currentRoute === 'project.application.danger') + @elseif ($currentRoute === 'project.application.compose-backups') + @endif diff --git a/routes/web.php b/routes/web.php index e8c738b71e..790cf618a7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -208,6 +208,7 @@ Route::get('/metrics', ApplicationConfiguration::class)->name('project.application.metrics'); Route::get('/tags', ApplicationConfiguration::class)->name('project.application.tags'); Route::get('/danger', ApplicationConfiguration::class)->name('project.application.danger'); + Route::get('/compose-backups', ApplicationConfiguration::class)->name('project.application.compose-backups'); Route::get('/deployment', DeploymentIndex::class)->name('project.application.deployment.index'); Route::get('/deployment/{deployment_uuid}', DeploymentShow::class)->name('project.application.deployment.show'); @@ -328,7 +329,7 @@ } $filename = data_get($execution, 'filename'); if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { - $server = $execution->scheduledDatabaseBackup->database->service->destination->server; + $server = $execution->scheduledDatabaseBackup->database->getServer(); } else { $server = $execution->scheduledDatabaseBackup->database->destination->server; }