From 5b424f1f0e44a0783406d4652e7a410c8ae309bc Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 16 Mar 2026 13:33:58 +0100
Subject: [PATCH 1/7] fix(preview): exclude bind mounts from preview deployment
suffix
Bind mount volumes reference files at the repository's original path and
should not receive the -pr-N suffix. Only named Docker volumes require
the suffix for isolation between preview deployments.
Adds PreviewDeploymentBindMountTest to verify the correct behavior.
Fixes #7802
---
bootstrap/helpers/parsers.php | 3 --
coolify-examples-1 | 1 +
tests/Unit/PreviewDeploymentBindMountTest.php | 41 +++++++++++++++++++
3 files changed, 42 insertions(+), 3 deletions(-)
create mode 160000 coolify-examples-1
create mode 100644 tests/Unit/PreviewDeploymentBindMountTest.php
diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php
index e84df55f9..95f5fa9c0 100644
--- a/bootstrap/helpers/parsers.php
+++ b/bootstrap/helpers/parsers.php
@@ -789,9 +789,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
}
$source = replaceLocalSource($source, $mainDirectory);
- if ($isPullRequest) {
- $source = addPreviewDeploymentSuffix($source, $pull_request_id);
- }
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
diff --git a/coolify-examples-1 b/coolify-examples-1
new file mode 160000
index 000000000..a5964d89a
--- /dev/null
+++ b/coolify-examples-1
@@ -0,0 +1 @@
+Subproject commit a5964d89a9d361dea373ddabaec265916266c4fc
diff --git a/tests/Unit/PreviewDeploymentBindMountTest.php b/tests/Unit/PreviewDeploymentBindMountTest.php
new file mode 100644
index 000000000..acc560e68
--- /dev/null
+++ b/tests/Unit/PreviewDeploymentBindMountTest.php
@@ -0,0 +1,41 @@
+value() === 'bind')");
+ $volumeBlockStart = strpos($parsersFile, "} elseif (\$type->value() === 'volume')");
+ $bindBlock = substr($parsersFile, $bindBlockStart, $volumeBlockStart - $bindBlockStart);
+
+ // Bind mount paths should NOT be suffixed with -pr-N
+ expect($bindBlock)->not->toContain('addPreviewDeploymentSuffix');
+});
+
+it('still applies preview deployment suffix to named volume paths', function () {
+ // Read the applicationParser volume handling in parsers.php
+ $parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
+
+ // Find the named volume handling block (type === 'volume')
+ $volumeBlockStart = strpos($parsersFile, "} elseif (\$type->value() === 'volume')");
+ $volumeBlock = substr($parsersFile, $volumeBlockStart, 1000);
+
+ // Named volumes SHOULD still get the -pr-N suffix for isolation
+ expect($volumeBlock)->toContain('addPreviewDeploymentSuffix');
+});
+
+it('confirms addPreviewDeploymentSuffix works correctly', function () {
+ $result = addPreviewDeploymentSuffix('myvolume', 3);
+ expect($result)->toBe('myvolume-pr-3');
+
+ $result = addPreviewDeploymentSuffix('myvolume', 0);
+ expect($result)->toBe('myvolume');
+});
From 1b484a56b0698993335bdcd92cfe186e72246701 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 16 Mar 2026 13:37:14 +0100
Subject: [PATCH 2/7] chore: remove coolify-examples-1 submodule
---
coolify-examples-1 | 1 -
1 file changed, 1 deletion(-)
delete mode 160000 coolify-examples-1
diff --git a/coolify-examples-1 b/coolify-examples-1
deleted file mode 160000
index a5964d89a..000000000
--- a/coolify-examples-1
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a5964d89a9d361dea373ddabaec265916266c4fc
From add16853a8180896864769c8904e72a0777383b8 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 16 Mar 2026 14:54:22 +0100
Subject: [PATCH 3/7] feat(preview): add configurable PR suffix toggle for
volumes
Add `is_preview_suffix_enabled` flag to `local_file_volumes` and
`local_persistent_volumes` tables, allowing per-volume control over
whether a `-pr-N` suffix is appended during preview deployments.
Defaults to `true` to preserve existing behavior. Users can disable
it for volumes containing shared config or repository scripts that
should not be isolated per PR.
---
app/Jobs/ApplicationDeploymentJob.php | 6 +-
app/Livewire/Project/Service/FileStorage.php | 6 ++
app/Livewire/Project/Shared/Storages/Show.php | 14 ++++
app/Models/LocalFileVolume.php | 1 +
app/Models/LocalPersistentVolume.php | 4 +
bootstrap/helpers/parsers.php | 26 +++---
...review_suffix_enabled_to_volume_tables.php | 30 +++++++
.../project/service/file-storage.blade.php | 7 ++
.../project/shared/storages/show.blade.php | 14 ++++
tests/Unit/PreviewDeploymentBindMountTest.php | 84 +++++++++++++++++--
10 files changed, 172 insertions(+), 20 deletions(-)
create mode 100644 database/migrations/2026_03_16_000000_add_is_preview_suffix_enabled_to_volume_tables.php
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index f84cdceb9..a0cc7aaa0 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -2745,7 +2745,8 @@ private function generate_local_persistent_volumes()
} else {
$volume_name = $persistentStorage->name;
}
- if ($this->pull_request_id !== 0) {
+ $isPreviewSuffixEnabled = (bool) data_get($persistentStorage, 'is_preview_suffix_enabled', true);
+ if ($this->pull_request_id !== 0 && $isPreviewSuffixEnabled) {
$volume_name = addPreviewDeploymentSuffix($volume_name, $this->pull_request_id);
}
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
@@ -2763,7 +2764,8 @@ private function generate_local_persistent_volumes_only_volume_names()
}
$name = $persistentStorage->name;
- if ($this->pull_request_id !== 0) {
+ $isPreviewSuffixEnabled = (bool) data_get($persistentStorage, 'is_preview_suffix_enabled', true);
+ if ($this->pull_request_id !== 0 && $isPreviewSuffixEnabled) {
$name = addPreviewDeploymentSuffix($name, $this->pull_request_id);
}
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 5d948bffd..33b32989a 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -40,12 +40,16 @@ class FileStorage extends Component
#[Validate(['required', 'boolean'])]
public bool $isBasedOnGit = false;
+ #[Validate(['required', 'boolean'])]
+ public bool $isPreviewSuffixEnabled = true;
+
protected $rules = [
'fileStorage.is_directory' => 'required',
'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required',
'content' => 'nullable',
'isBasedOnGit' => 'required|boolean',
+ 'isPreviewSuffixEnabled' => 'required|boolean',
];
public function mount()
@@ -71,12 +75,14 @@ public function syncData(bool $toModel = false): void
// Sync to model
$this->fileStorage->content = $this->content;
$this->fileStorage->is_based_on_git = $this->isBasedOnGit;
+ $this->fileStorage->is_preview_suffix_enabled = $this->isPreviewSuffixEnabled;
$this->fileStorage->save();
} else {
// Sync from model
$this->content = $this->fileStorage->content;
$this->isBasedOnGit = $this->fileStorage->is_based_on_git;
+ $this->isPreviewSuffixEnabled = $this->fileStorage->is_preview_suffix_enabled ?? true;
}
}
diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php
index 69395a591..72b330845 100644
--- a/app/Livewire/Project/Shared/Storages/Show.php
+++ b/app/Livewire/Project/Shared/Storages/Show.php
@@ -29,10 +29,13 @@ class Show extends Component
public ?string $hostPath = null;
+ public bool $isPreviewSuffixEnabled = true;
+
protected $rules = [
'name' => 'required|string',
'mountPath' => 'required|string',
'hostPath' => 'string|nullable',
+ 'isPreviewSuffixEnabled' => 'required|boolean',
];
protected $validationAttributes = [
@@ -53,11 +56,13 @@ private function syncData(bool $toModel = false): void
$this->storage->name = $this->name;
$this->storage->mount_path = $this->mountPath;
$this->storage->host_path = $this->hostPath;
+ $this->storage->is_preview_suffix_enabled = $this->isPreviewSuffixEnabled;
} else {
// Sync FROM model (on load/refresh)
$this->name = $this->storage->name;
$this->mountPath = $this->storage->mount_path;
$this->hostPath = $this->storage->host_path;
+ $this->isPreviewSuffixEnabled = $this->storage->is_preview_suffix_enabled ?? true;
}
}
@@ -67,6 +72,15 @@ public function mount()
$this->isReadOnly = $this->storage->shouldBeReadOnlyInUI();
}
+ public function instantSave()
+ {
+ $this->authorize('update', $this->resource);
+
+ $this->syncData(true);
+ $this->storage->save();
+ $this->dispatch('success', 'Storage updated successfully');
+ }
+
public function submit()
{
$this->authorize('update', $this->resource);
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index 9d7095cb5..da58ed2f9 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -14,6 +14,7 @@ class LocalFileVolume extends BaseModel
// 'mount_path' => 'encrypted',
'content' => 'encrypted',
'is_directory' => 'boolean',
+ 'is_preview_suffix_enabled' => 'boolean',
];
use HasFactory;
diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php
index 7126253ea..1721f4afe 100644
--- a/app/Models/LocalPersistentVolume.php
+++ b/app/Models/LocalPersistentVolume.php
@@ -10,6 +10,10 @@ class LocalPersistentVolume extends Model
{
protected $guarded = [];
+ protected $casts = [
+ 'is_preview_suffix_enabled' => 'boolean',
+ ];
+
public function resource()
{
return $this->morphTo('resource');
diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php
index 95f5fa9c0..cd4928d63 100644
--- a/bootstrap/helpers/parsers.php
+++ b/bootstrap/helpers/parsers.php
@@ -789,6 +789,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
}
$source = replaceLocalSource($source, $mainDirectory);
+ $isPreviewSuffixEnabled = $foundConfig
+ ? (bool) data_get($foundConfig, 'is_preview_suffix_enabled', true)
+ : true;
+ if ($isPullRequest && $isPreviewSuffixEnabled) {
+ $source = addPreviewDeploymentSuffix($source, $pull_request_id);
+ }
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
@@ -1312,19 +1318,19 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
}
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
$shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels;
- $uuid = $resource->uuid;
- $network = data_get($resource, 'destination.network');
+ $labelUuid = $resource->uuid;
+ $labelNetwork = data_get($resource, 'destination.network');
if ($isPullRequest) {
- $uuid = "{$resource->uuid}-{$pullRequestId}";
+ $labelUuid = "{$resource->uuid}-{$pullRequestId}";
}
if ($isPullRequest) {
- $network = "{$resource->destination->network}-{$pullRequestId}";
+ $labelNetwork = "{$resource->destination->network}-{$pullRequestId}";
}
if ($shouldGenerateLabelsExactly) {
switch ($server->proxyType()) {
case ProxyTypes::TRAEFIK->value:
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
- uuid: $uuid,
+ uuid: $labelUuid,
domains: $fqdns,
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
serviceLabels: $serviceLabels,
@@ -1336,8 +1342,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
break;
case ProxyTypes::CADDY->value:
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
- network: $network,
- uuid: $uuid,
+ network: $labelNetwork,
+ uuid: $labelUuid,
domains: $fqdns,
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
serviceLabels: $serviceLabels,
@@ -1351,7 +1357,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
}
} else {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
- uuid: $uuid,
+ uuid: $labelUuid,
domains: $fqdns,
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
serviceLabels: $serviceLabels,
@@ -1361,8 +1367,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
image: $image
));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
- network: $network,
- uuid: $uuid,
+ network: $labelNetwork,
+ uuid: $labelUuid,
domains: $fqdns,
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
serviceLabels: $serviceLabels,
diff --git a/database/migrations/2026_03_16_000000_add_is_preview_suffix_enabled_to_volume_tables.php b/database/migrations/2026_03_16_000000_add_is_preview_suffix_enabled_to_volume_tables.php
new file mode 100644
index 000000000..a1f1d9ea1
--- /dev/null
+++ b/database/migrations/2026_03_16_000000_add_is_preview_suffix_enabled_to_volume_tables.php
@@ -0,0 +1,30 @@
+boolean('is_preview_suffix_enabled')->default(true)->after('is_based_on_git');
+ });
+
+ Schema::table('local_persistent_volumes', function (Blueprint $table) {
+ $table->boolean('is_preview_suffix_enabled')->default(true)->after('host_path');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('local_file_volumes', function (Blueprint $table) {
+ $table->dropColumn('is_preview_suffix_enabled');
+ });
+
+ Schema::table('local_persistent_volumes', function (Blueprint $table) {
+ $table->dropColumn('is_preview_suffix_enabled');
+ });
+ }
+};
diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php
index 1dd58fe17..24612098b 100644
--- a/resources/views/livewire/project/service/file-storage.blade.php
+++ b/resources/views/livewire/project/service/file-storage.blade.php
@@ -15,6 +15,13 @@