diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index a585baa69..ec995cafb 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -660,7 +660,16 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$escapedSecret = escapeshellarg($secret);
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set temporary {$escapedEndpoint} {$escapedKey} {$escapedSecret}";
- $commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
+
+ // Build S3 path with optional prefix
+ $s3Path = $bucket;
+ if (filled($this->s3->path)) {
+ $pathPrefix = ltrim($this->s3->path, '/');
+ $s3Path .= '/'.$pathPrefix;
+ }
+ $s3Path .= $this->backup_dir.'/';
+
+ $commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp $this->backup_location temporary/{$s3Path}";
instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
$this->s3_uploaded = true;
diff --git a/app/Livewire/Storage/Form.php b/app/Livewire/Storage/Form.php
index d101d7b58..330aabaa4 100644
--- a/app/Livewire/Storage/Form.php
+++ b/app/Livewire/Storage/Form.php
@@ -29,6 +29,8 @@ class Form extends Component
public string $secret;
+ public ?string $path = null;
+
public ?bool $isUsable = null;
protected function rules(): array
@@ -42,6 +44,7 @@ class Form extends Component
'secret' => 'required|max:255',
'bucket' => 'required|max:255',
'endpoint' => 'required|url|max:255',
+ 'path' => 'nullable|max:255|regex:/^[a-zA-Z0-9\/\-_\.]*$/',
];
}
@@ -63,6 +66,8 @@ class Form extends Component
'endpoint.required' => 'The Endpoint field is required.',
'endpoint.url' => 'The Endpoint must be a valid URL.',
'endpoint.max' => 'The Endpoint may not be greater than 255 characters.',
+ 'path.max' => 'The Path Prefix may not be greater than 255 characters.',
+ 'path.regex' => 'The Path Prefix may only contain letters, numbers, slashes (/), dashes (-), underscores (_), and dots (.).',
]
);
}
@@ -76,6 +81,7 @@ class Form extends Component
'secret' => 'Secret',
'bucket' => 'Bucket',
'endpoint' => 'Endpoint',
+ 'path' => 'Path Prefix',
];
/**
@@ -94,6 +100,7 @@ class Form extends Component
$this->storage->region = $this->region;
$this->storage->key = $this->key;
$this->storage->secret = $this->secret;
+ $this->storage->path = $this->path;
$this->storage->is_usable = $this->isUsable;
} else {
// Sync FROM model (on load/refresh)
@@ -104,6 +111,7 @@ class Form extends Component
$this->region = $this->storage->region;
$this->key = $this->storage->key;
$this->secret = $this->storage->secret;
+ $this->path = $this->storage->path;
$this->isUsable = $this->storage->is_usable;
}
}
diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php
index 5df36db33..77b5c5f5b 100644
--- a/bootstrap/helpers/databases.php
+++ b/bootstrap/helpers/databases.php
@@ -209,6 +209,18 @@ function deleteBackupsS3(string|array|null $filenames, S3Storage $s3): void
$filenames = [$filenames];
}
+ // Apply path prefix if configured
+ if (filled($s3->path)) {
+ $pathPrefix = ltrim($s3->path, '/');
+ $filenames = array_map(function ($filename) use ($pathPrefix) {
+ // The filename is the local path (e.g., /var/lib/coolify/backups/databases/...)
+ // We need to prepend the path prefix to match the S3 key structure
+ $cleanFilename = ltrim($filename, '/');
+
+ return $pathPrefix.'/'.$cleanFilename;
+ }, $filenames);
+ }
+
$disk = Storage::build([
'driver' => 's3',
'key' => $s3->key,
diff --git a/database/migrations/2025_12_26_000001_add_path_to_s3_storages_table.php b/database/migrations/2025_12_26_000001_add_path_to_s3_storages_table.php
new file mode 100644
index 000000000..92aedb977
--- /dev/null
+++ b/database/migrations/2025_12_26_000001_add_path_to_s3_storages_table.php
@@ -0,0 +1,28 @@
+string('path')->nullable()->after('endpoint');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('s3_storages', function (Blueprint $table) {
+ $table->dropColumn('path');
+ });
+ }
+};
diff --git a/resources/views/livewire/storage/form.blade.php b/resources/views/livewire/storage/form.blade.php
index 850d7735f..6278adf93 100644
--- a/resources/views/livewire/storage/form.blade.php
+++ b/resources/views/livewire/storage/form.blade.php
@@ -40,6 +40,11 @@