From dd597d152722cb8f141dfa76b4de694fd4283618 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:10:23 +0200 Subject: [PATCH 1/3] fix(backups): support long-running volume backups --- .../Api/VolumeBackupsController.php | 4 +- app/Jobs/VolumeBackupJob.php | 4 +- .../Project/Shared/Storages/VolumeBackups.php | 2 +- app/Models/ScheduledVolumeBackup.php | 2 + config/horizon.php | 6 ++- ...increase_default_volume_backup_timeout.php | 27 ++++++++++++++ tests/Feature/VolumeBackupTest.php | 37 +++++++++++++++++++ 7 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php diff --git a/app/Http/Controllers/Api/VolumeBackupsController.php b/app/Http/Controllers/Api/VolumeBackupsController.php index e51bf31f8..a5225f5dd 100644 --- a/app/Http/Controllers/Api/VolumeBackupsController.php +++ b/app/Http/Controllers/Api/VolumeBackupsController.php @@ -34,7 +34,7 @@ new OA\Property(property: 'retention_amount_s3', type: 'integer', default: 7, minimum: 0, maximum: 10000), new OA\Property(property: 'retention_days_s3', type: 'integer', default: 0, maximum: 2147483647, minimum: 0), new OA\Property(property: 'retention_max_storage_s3', type: 'number', format: 'float', default: 0, maximum: 9999999999, minimum: 0), - new OA\Property(property: 'timeout', type: 'integer', default: 3600, minimum: 60, maximum: 36000), + new OA\Property(property: 'timeout', type: 'integer', default: ScheduledVolumeBackup::DEFAULT_TIMEOUT, minimum: 60, maximum: 36000), ], type: 'object', additionalProperties: false, @@ -275,7 +275,7 @@ private function persistSchedule( 'retention_amount_s3' => $request->integer('retention_amount_s3', 7), 'retention_days_s3' => $request->integer('retention_days_s3'), 'retention_max_storage_s3' => $request->float('retention_max_storage_s3'), - 'timeout' => $request->integer('timeout', 3600), + 'timeout' => $request->integer('timeout', ScheduledVolumeBackup::DEFAULT_TIMEOUT), ]); $created = $backup->wasRecentlyCreated; diff --git a/app/Jobs/VolumeBackupJob.php b/app/Jobs/VolumeBackupJob.php index 39998a1f6..b567a71b7 100644 --- a/app/Jobs/VolumeBackupJob.php +++ b/app/Jobs/VolumeBackupJob.php @@ -28,14 +28,14 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue public int $maxExceptions = 1; - public int $timeout = 3600; + public int $timeout = ScheduledVolumeBackup::DEFAULT_TIMEOUT; private ?ScheduledVolumeBackupExecution $execution = null; public function __construct(public ScheduledVolumeBackup $backup) { $this->onQueue(crons_queue()); - $this->timeout = $backup->timeout ?? 3600; + $this->timeout = $backup->timeout ?? ScheduledVolumeBackup::DEFAULT_TIMEOUT; } public function middleware(): array diff --git a/app/Livewire/Project/Shared/Storages/VolumeBackups.php b/app/Livewire/Project/Shared/Storages/VolumeBackups.php index a03820b4b..a10eb5ad0 100644 --- a/app/Livewire/Project/Shared/Storages/VolumeBackups.php +++ b/app/Livewire/Project/Shared/Storages/VolumeBackups.php @@ -56,7 +56,7 @@ class VolumeBackups extends Component public string $timezone = ''; - public int $timeout = 3600; + public int $timeout = ScheduledVolumeBackup::DEFAULT_TIMEOUT; public int $perPage = 10; diff --git a/app/Models/ScheduledVolumeBackup.php b/app/Models/ScheduledVolumeBackup.php index a33368142..7f33fd92b 100644 --- a/app/Models/ScheduledVolumeBackup.php +++ b/app/Models/ScheduledVolumeBackup.php @@ -11,6 +11,8 @@ class ScheduledVolumeBackup extends BaseModel { + public const int DEFAULT_TIMEOUT = 36000; + protected $fillable = [ 'uuid', 'backupable_type', diff --git a/config/horizon.php b/config/horizon.php index d86c52aff..fe35734c2 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -1,5 +1,6 @@ 1, 'nice' => 0, 'sleep' => 3, - 'timeout' => env('HORIZON_TIMEOUT', 36000), + 'timeout' => min( + max((int) env('HORIZON_TIMEOUT', 39600), ScheduledVolumeBackup::DEFAULT_TIMEOUT + 600), + 85800, + ), ], ], diff --git a/database/migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php b/database/migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php new file mode 100644 index 000000000..f895c7837 --- /dev/null +++ b/database/migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php @@ -0,0 +1,27 @@ +where('timeout', 3600) + ->update(['timeout' => 36000]); + + Schema::table('scheduled_volume_backups', function (Blueprint $table) { + $table->unsignedInteger('timeout')->default(36000)->change(); + }); + } + + public function down(): void + { + Schema::table('scheduled_volume_backups', function (Blueprint $table) { + $table->unsignedInteger('timeout')->default(3600)->change(); + }); + } +}; diff --git a/tests/Feature/VolumeBackupTest.php b/tests/Feature/VolumeBackupTest.php index 496894591..e18f1475f 100644 --- a/tests/Feature/VolumeBackupTest.php +++ b/tests/Feature/VolumeBackupTest.php @@ -51,6 +51,43 @@ ->and(method_exists(LocalFileVolume::class, 'scheduledBackups'))->toBeTrue(); }); +it('allows large volume backups to run for ten hours by default', function () { + $backup = new ScheduledVolumeBackup; + $job = new VolumeBackupJob($backup); + + expect($job->timeout)->toBe(36000) + ->and((new VolumeBackups)->timeout)->toBe(36000) + ->and(config('horizon.defaults.s6.timeout'))->toBeGreaterThan($job->timeout) + ->and(config('queue.connections.redis.retry_after'))->toBeGreaterThan(config('horizon.defaults.s6.timeout')); +}); + +it('upgrades existing default volume backup timeouts without changing custom timeouts', function () { + $team = Team::factory()->create(); + [$application, $defaultVolume] = createVolumeBackupApplication($team); + $customVolume = LocalPersistentVolume::create([ + 'name' => 'custom-timeout-data', + 'mount_path' => '/custom-data', + 'resource_id' => $application->id, + 'resource_type' => $application->getMorphClass(), + ]); + $defaultBackup = $defaultVolume->scheduledBackups()->create([ + 'team_id' => $team->id, + 'frequency' => 'daily', + 'timeout' => 3600, + ]); + $customBackup = $customVolume->scheduledBackups()->create([ + 'team_id' => $team->id, + 'frequency' => 'daily', + 'timeout' => 7200, + ]); + + $migration = require database_path('migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php'); + $migration->up(); + + expect($defaultBackup->fresh()->timeout)->toBe(36000) + ->and($customBackup->fresh()->timeout)->toBe(7200); +}); + it('includes parallel gzip support in the Coolify helper image', function () { $dockerfile = file_get_contents(base_path('docker/coolify-helper/Dockerfile')); From b075693274341f78f894783e5ca18c6712eab6d1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:30:59 +0200 Subject: [PATCH 2/3] fix(backups): preserve existing volume backup timeouts Change only the default timeout for new volume backups without modifying existing schedules. --- ...6_08_15_000000_increase_default_volume_backup_timeout.php | 5 ----- tests/Feature/VolumeBackupTest.php | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/database/migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php b/database/migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php index f895c7837..32eccce96 100644 --- a/database/migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php +++ b/database/migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php @@ -2,17 +2,12 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - DB::table('scheduled_volume_backups') - ->where('timeout', 3600) - ->update(['timeout' => 36000]); - Schema::table('scheduled_volume_backups', function (Blueprint $table) { $table->unsignedInteger('timeout')->default(36000)->change(); }); diff --git a/tests/Feature/VolumeBackupTest.php b/tests/Feature/VolumeBackupTest.php index e18f1475f..b4f78d9e0 100644 --- a/tests/Feature/VolumeBackupTest.php +++ b/tests/Feature/VolumeBackupTest.php @@ -61,7 +61,7 @@ ->and(config('queue.connections.redis.retry_after'))->toBeGreaterThan(config('horizon.defaults.s6.timeout')); }); -it('upgrades existing default volume backup timeouts without changing custom timeouts', function () { +it('changes the default volume backup timeout without changing existing timeouts', function () { $team = Team::factory()->create(); [$application, $defaultVolume] = createVolumeBackupApplication($team); $customVolume = LocalPersistentVolume::create([ @@ -84,7 +84,7 @@ $migration = require database_path('migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php'); $migration->up(); - expect($defaultBackup->fresh()->timeout)->toBe(36000) + expect($defaultBackup->fresh()->timeout)->toBe(3600) ->and($customBackup->fresh()->timeout)->toBe(7200); }); From a3e968d03e38807a676fc573770f366b0e581be9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:41:31 +0200 Subject: [PATCH 3/3] fix(backups): preserve existing volume backup timeouts Only update timeout when the API request explicitly provides one, with coverage for custom timeout values. --- app/Http/Controllers/Api/VolumeBackupsController.php | 10 +++++++--- tests/Feature/Api/VolumeBackupScheduleApiTest.php | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/VolumeBackupsController.php b/app/Http/Controllers/Api/VolumeBackupsController.php index a5225f5dd..26ff938a1 100644 --- a/app/Http/Controllers/Api/VolumeBackupsController.php +++ b/app/Http/Controllers/Api/VolumeBackupsController.php @@ -261,7 +261,7 @@ private function persistSchedule( string $resourceType, Model $resource, ): JsonResponse { - $backup = $storage->scheduledBackups()->updateOrCreate([], [ + $attributes = [ 'team_id' => $teamId, 'frequency' => $request->string('frequency')->toString(), 'enabled' => $request->boolean('enabled', true), @@ -275,8 +275,12 @@ private function persistSchedule( 'retention_amount_s3' => $request->integer('retention_amount_s3', 7), 'retention_days_s3' => $request->integer('retention_days_s3'), 'retention_max_storage_s3' => $request->float('retention_max_storage_s3'), - 'timeout' => $request->integer('timeout', ScheduledVolumeBackup::DEFAULT_TIMEOUT), - ]); + ]; + if ($request->has('timeout')) { + $attributes['timeout'] = $request->integer('timeout'); + } + + $backup = $storage->scheduledBackups()->updateOrCreate([], $attributes); $created = $backup->wasRecentlyCreated; auditLog('api.volume_backup.schedule_set', [ diff --git a/tests/Feature/Api/VolumeBackupScheduleApiTest.php b/tests/Feature/Api/VolumeBackupScheduleApiTest.php index 4f4bc395b..e60f2919d 100644 --- a/tests/Feature/Api/VolumeBackupScheduleApiTest.php +++ b/tests/Feature/Api/VolumeBackupScheduleApiTest.php @@ -130,6 +130,7 @@ function createVolumeBackupApiToken($context, User $user, array $abilities): str 'save_s3' => true, 'disable_local_backup' => true, 's3_storage_id' => $this->s3Storage->id, + 'timeout' => 7200, ]); $response = $this->withHeaders($this->headers) @@ -146,7 +147,8 @@ function createVolumeBackupApiToken($context, User $user, array $abilities): str ->and($backup->enabled)->toBeFalse() ->and($backup->save_s3)->toBeFalse() ->and($backup->disable_local_backup)->toBeFalse() - ->and($backup->s3_storage_id)->toBeNull(); + ->and($backup->s3_storage_id)->toBeNull() + ->and($backup->timeout)->toBe(7200); }); it('sets a directory backup schedule through the API', function () {