From a5dedc07cd72d58a3f0a843fcee7d120f9000d7c Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 17 Aug 2026 16:19:41 +0200
Subject: [PATCH] feat(storage): persist local images and unify clipboard
copying
Add a private images disk with persistent container mounts and installation/upgrade setup, while routing avatar and project icon storage through it. Reuse the copy button component for API tokens and support clipboard copying through the shared helper.
---
app/Services/AvatarStorageService.php | 2 +-
config/filesystems.php | 7 +++++
docker-compose.prod.yml | 1 +
docker-compose.windows.yml | 1 +
other/nightly/docker-compose.prod.yml | 1 +
.../components/forms/copy-button.blade.php | 9 ++----
.../livewire/security/api-tokens.blade.php | 13 +-------
scripts/install.sh | 1 +
scripts/upgrade.sh | 4 +++
tests/Feature/PersistentImageStorageTest.php | 31 +++++++++++++++++++
tests/Feature/ProfileAvatarTest.php | 22 ++++++-------
tests/Feature/ProjectIconTest.php | 14 ++++-----
.../Feature/ResourceDetailsVisibilityTest.php | 13 ++++++--
13 files changed, 79 insertions(+), 40 deletions(-)
create mode 100644 tests/Feature/PersistentImageStorageTest.php
diff --git a/app/Services/AvatarStorageService.php b/app/Services/AvatarStorageService.php
index d1446ea37e..983b144b94 100644
--- a/app/Services/AvatarStorageService.php
+++ b/app/Services/AvatarStorageService.php
@@ -71,7 +71,7 @@ class AvatarStorageService
protected function disk(string $storageType, ?int $s3StorageId): FilesystemAdapter
{
if ($storageType !== 's3') {
- return Storage::disk('local');
+ return Storage::disk('images');
}
$storage = S3Storage::query()->whereKey($s3StorageId)->where('is_usable', true)->first();
diff --git a/config/filesystems.php b/config/filesystems.php
index ba0921a794..966cd0d5a8 100644
--- a/config/filesystems.php
+++ b/config/filesystems.php
@@ -35,6 +35,13 @@ return [
'throw' => false,
],
+ 'images' => [
+ 'driver' => 'local',
+ 'root' => storage_path('app/images'),
+ 'visibility' => 'private',
+ 'throw' => false,
+ ],
+
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 9edc00e702..0d7caceb95 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -11,6 +11,7 @@ services:
- /data/coolify/databases:/var/www/html/storage/app/databases
- /data/coolify/services:/var/www/html/storage/app/services
- /data/coolify/backups:/var/www/html/storage/app/backups
+ - /data/coolify/images:/var/www/html/storage/app/images
environment:
- APP_ENV=${APP_ENV:-production}
- PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M}
diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml
index 43f6f0d0e9..33709873f2 100644
--- a/docker-compose.windows.yml
+++ b/docker-compose.windows.yml
@@ -25,6 +25,7 @@ services:
- ./databases:/var/www/html/storage/app/databases
- ./services:/var/www/html/storage/app/services
- ./backups:/var/www/html/storage/app/backups
+ - ./images:/var/www/html/storage/app/images
env_file:
- .env
environment:
diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml
index 9edc00e702..0d7caceb95 100644
--- a/other/nightly/docker-compose.prod.yml
+++ b/other/nightly/docker-compose.prod.yml
@@ -11,6 +11,7 @@ services:
- /data/coolify/databases:/var/www/html/storage/app/databases
- /data/coolify/services:/var/www/html/storage/app/services
- /data/coolify/backups:/var/www/html/storage/app/backups
+ - /data/coolify/images:/var/www/html/storage/app/images
environment:
- APP_ENV=${APP_ENV:-production}
- PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M}
diff --git a/resources/views/components/forms/copy-button.blade.php b/resources/views/components/forms/copy-button.blade.php
index 7a12cdfe43..e299610eb2 100644
--- a/resources/views/components/forms/copy-button.blade.php
+++ b/resources/views/components/forms/copy-button.blade.php
@@ -1,21 +1,18 @@
@props(['text', 'label' => null])
-
+
@if ($label)
@endif