feat(backups): support scheduled backups for application storage targets

Add polymorphic volume backup scheduling for persistent volumes and directories, expose schedule management via API, and reorganize backup configuration and execution views.
This commit is contained in:
Andras Bacsai committed 2026-07-15 17:34:22 +02:00
1 parent 995ec5fbb6
commit d7385ad0c4
60 files changed
+3570 -985

No files matched your search

@@ -1,23 +1,65 @@
<div>
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex flex-col h-full gap-2 md:gap-8 md:flex-row">
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
@if ($backup)
<div class="sub-menu-wrapper">
<a class="sub-menu-item" {{ wireNavigate() }}
href="{{ route('project.service.database.backups', $parameters) }}">
<span class="menu-item-label">Back</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'general']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.show', $backupParameters) }}">
<span class="menu-item-label">General</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 's3']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.s3', $backupParameters) }}">
<span class="menu-item-label">S3</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'retention']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.retention', $backupParameters) }}">
<span class="menu-item-label">Retention</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'executions']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.executions', $backupParameters) }}">
<span class="menu-item-label">Executions</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'danger']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.danger', $backupParameters) }}">
<span class="menu-item-label">Danger Zone</span>
</a>
</div>
@else
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase"
:isImportSupported="$isImportSupported" />
@endif
<div class="w-full">
<x-slot:title>
{{ data_get_str($service, 'name')->limit(10) }} >
{{ data_get_str($serviceDatabase, 'name')->limit(10) }} > Backups | Coolify
</x-slot>
<div class="flex gap-2">
<h2 class="pb-4">Scheduled Backups</h2>
@if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated)
@can('update', $serviceDatabase)
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
</x-modal-input>
@endcan
@if ($backup)
@if ($section === 'executions')
<livewire:project.database.backup-executions :backup="$backup" :database="$serviceDatabase" />
@else
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s"
:status="data_get($serviceDatabase, 'status')" :section="$section"
wire:key="service-database-backup-{{ $backup->uuid }}-{{ $section }}" />
@endif
</div>
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
@else
<div class="flex gap-2">
<h2 class="pb-4">Scheduled Backups</h2>
@if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated)
@can('update', $serviceDatabase)
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
</x-modal-input>
@endcan
@endif
</div>
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
@endif
</div>
</div>
</div>
@@ -19,7 +19,15 @@
@endif
<div class="flex flex-col justify-center text-sm select-text">
<div class="flex gap-2 md:flex-row flex-col">
<x-forms.input label="Source Path" :value="$fileStorage->fs_path" readonly />
<x-forms.input label="Source Path" :value="$fileStorage->fs_path" readonly>
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
<x-forms.input label="Destination Path" :value="$fileStorage->mount_path" readonly />
</div>
</div>
@@ -51,6 +59,14 @@
confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" />
@if ($resource instanceof \App\Models\Application)
<x-modal-input buttonTitle="Configure Backup" title="Configure Directory Backup"
:wireIgnore="false">
<livewire:project.application.backup.create :application="$resource"
:selected-target-key="'directory:' . $fileStorage->id"
wire:key="configure-directory-backup-{{ $fileStorage->id }}" />
</x-modal-input>
@endif
<x-modal-confirmation :ignoreWire="false" title="Confirm Directory Deletion?" buttonTitle="Delete"
isErrorButton submitAction="delete" :checkboxes="$directoryDeletionCheckboxes" :actions="[
'The selected directory and all its contents will be permanently deleted from the container.',
@@ -130,5 +146,16 @@
@endif
@endif
</form>
@if ($isReadOnly && $fileStorage->is_directory && $resource instanceof \App\Models\Application)
@can('update', $resource)
<div>
<x-modal-input buttonTitle="Configure Backup" title="Configure Directory Backup" :wireIgnore="false">
<livewire:project.application.backup.create :application="$resource"
:selected-target-key="'directory:' . $fileStorage->id"
wire:key="configure-readonly-directory-backup-{{ $fileStorage->id }}" />
</x-modal-input>
</div>
@endcan
@endif
</div>
</div>
@@ -23,7 +23,7 @@
['label' => 'Back', 'route' => 'project.service.configuration', 'active' => false, 'parameters' => [...$parameters, 'stack_service_uuid' => null]],
['label' => 'General', 'route' => 'project.service.index', 'active' => request()->routeIs('project.service.index')],
['label' => 'Advanced', 'route' => 'project.service.index.advanced', 'active' => request()->routeIs('project.service.index.advanced')],
['label' => 'Backups', 'route' => 'project.service.database.backups', 'active' => request()->routeIs('project.service.database.backups')],
['label' => 'Backups', 'route' => 'project.service.database.backups', 'active' => request()->routeIs('project.service.database.backup*')],
['label' => 'Import Backup', 'route' => 'project.service.database.import', 'active' => request()->routeIs('project.service.database.import')],
];
}