diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 2ce4374a0..8a61e3c38 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -104,7 +104,8 @@ public function convertToDirectory()
public function loadStorageOnServer()
{
try {
- $this->authorize('update', $this->resource);
+ // Loading content is a read operation, so we use 'view' permission
+ $this->authorize('view', $this->resource);
$this->fileStorage->loadStorageOnServer();
$this->syncData();
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index 96170dbd6..dda5de194 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -239,22 +239,32 @@ public function isReadOnlyVolume(): bool
$volumes = $compose['services'][$serviceName]['volumes'];
// Check each volume to find a match
+ // Note: We match on mount_path (container path) only, since fs_path gets transformed
+ // from relative (./file) to absolute (/data/coolify/services/uuid/file) during parsing
foreach ($volumes as $volume) {
// Volume can be string like "host:container:ro" or "host:container"
if (is_string($volume)) {
$parts = explode(':', $volume);
- // Check if this volume matches our fs_path and mount_path
+ // Check if this volume matches our mount_path
if (count($parts) >= 2) {
- $hostPath = $parts[0];
$containerPath = $parts[1];
$options = $parts[2] ?? null;
- // Match based on fs_path and mount_path
- if ($hostPath === $this->fs_path && $containerPath === $this->mount_path) {
+ // Match based on mount_path (container path)
+ if ($containerPath === $this->mount_path) {
return $options === 'ro';
}
}
+ } elseif (is_array($volume)) {
+ // Long-form syntax: { type: bind, source: ..., target: ..., read_only: true }
+ $containerPath = data_get($volume, 'target');
+ $readOnly = data_get($volume, 'read_only', false);
+
+ // Match based on mount_path (container path)
+ if ($containerPath === $this->mount_path) {
+ return $readOnly === true;
+ }
}
}
diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php
index e7862478b..26e9b3e85 100644
--- a/app/Models/LocalPersistentVolume.php
+++ b/app/Models/LocalPersistentVolume.php
@@ -85,6 +85,7 @@ public function isReadOnlyVolume(): bool
$volumes = $compose['services'][$serviceName]['volumes'];
// Check each volume to find a match
+ // Note: We match on mount_path (container path) only, since host paths get transformed
foreach ($volumes as $volume) {
// Volume can be string like "host:container:ro" or "host:container"
if (is_string($volume)) {
@@ -104,6 +105,19 @@ public function isReadOnlyVolume(): bool
return $options === 'ro';
}
}
+ } elseif (is_array($volume)) {
+ // Long-form syntax: { type: bind/volume, source: ..., target: ..., read_only: true }
+ $containerPath = data_get($volume, 'target');
+ $readOnly = data_get($volume, 'read_only', false);
+
+ // Match based on mount_path
+ // Remove leading slash from mount_path if present for comparison
+ $mountPath = str($this->mount_path)->ltrim('/')->toString();
+ $containerPathClean = str($containerPath)->ltrim('/')->toString();
+
+ if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
+ return $readOnly === true;
+ }
}
}
diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php
index 4ab966ec3..1dd58fe17 100644
--- a/resources/views/livewire/project/service/file-storage.blade.php
+++ b/resources/views/livewire/project/service/file-storage.blade.php
@@ -65,6 +65,7 @@
@endif