From 0f54c194d7a3ec63c41d707202f53b57e4abe7d7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:46:57 +0100 Subject: [PATCH] Add Garage as a one-click service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for deploying Garage (S3-compatible object storage) as a one-click service in Coolify. Includes service template with TOML config, automatic URL generation for S3, Web, and Admin endpoints with reverse proxy configuration, and UI fields for credentials and access tokens. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Models/Service.php | 78 +++++++++++++++++++++ bootstrap/helpers/constants.php | 1 + bootstrap/helpers/docker.php | 30 ++++++++ public/svgs/garage.svg | 118 ++++++++++++++++++++++++++++++++ templates/compose/garage.yaml | 60 ++++++++++++++++ 5 files changed, 287 insertions(+) create mode 100644 public/svgs/garage.svg create mode 100644 templates/compose/garage.yaml diff --git a/app/Models/Service.php b/app/Models/Service.php index 2f8a64464..2cea4c805 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -712,6 +712,84 @@ public function extraFields() $fields->put('MinIO', $data->toArray()); break; + case $image->contains('garage'): + $data = collect([]); + $s3_api_url = $this->environment_variables()->where('key', 'GARAGE_S3_API_URL')->first(); + $web_url = $this->environment_variables()->where('key', 'GARAGE_WEB_URL')->first(); + $admin_url = $this->environment_variables()->where('key', 'GARAGE_ADMIN_URL')->first(); + $admin_token = $this->environment_variables()->where('key', 'GARAGE_ADMIN_TOKEN')->first(); + if (is_null($admin_token)) { + $admin_token = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GARAGE')->first(); + } + $rpc_secret = $this->environment_variables()->where('key', 'GARAGE_RPC_SECRET')->first(); + if (is_null($rpc_secret)) { + $rpc_secret = $this->environment_variables()->where('key', 'SERVICE_HEX_32_RPCSECRET')->first(); + } + $metrics_token = $this->environment_variables()->where('key', 'GARAGE_METRICS_TOKEN')->first(); + if (is_null($metrics_token)) { + $metrics_token = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GARAGEMETRICS')->first(); + } + + if ($s3_api_url) { + $data = $data->merge([ + 'S3 API URL' => [ + 'key' => data_get($s3_api_url, 'key'), + 'value' => data_get($s3_api_url, 'value'), + 'rules' => 'required|url', + ], + ]); + } + if ($web_url) { + $data = $data->merge([ + 'Web URL' => [ + 'key' => data_get($web_url, 'key'), + 'value' => data_get($web_url, 'value'), + 'rules' => 'required|url', + ], + ]); + } + if ($admin_url) { + $data = $data->merge([ + 'Admin URL' => [ + 'key' => data_get($admin_url, 'key'), + 'value' => data_get($admin_url, 'value'), + 'rules' => 'required|url', + ], + ]); + } + if ($admin_token) { + $data = $data->merge([ + 'Admin Token' => [ + 'key' => data_get($admin_token, 'key'), + 'value' => data_get($admin_token, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + if ($rpc_secret) { + $data = $data->merge([ + 'RPC Secret' => [ + 'key' => data_get($rpc_secret, 'key'), + 'value' => data_get($rpc_secret, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + if ($metrics_token) { + $data = $data->merge([ + 'Metrics Token' => [ + 'key' => data_get($metrics_token, 'key'), + 'value' => data_get($metrics_token, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + + $fields->put('Garage', $data->toArray()); + break; case $image->contains('weblate'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first(); diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 9196f9fb8..114c4bb98 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -58,6 +58,7 @@ 'ghcr.io/coollabsio/minio', 'coollabsio/minio', 'svhd/logto', + 'dxflrs/garage', ]; // Based on /etc/os-release diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 759d345b0..a0f810480 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -312,6 +312,36 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) $LOGTO_ADMIN_ENDPOINT->value.':3002', ]); break; + case $type?->contains('garage'): + $GARAGE_S3_API_URL = $variables->where('key', 'GARAGE_S3_API_URL')->first(); + $GARAGE_WEB_URL = $variables->where('key', 'GARAGE_WEB_URL')->first(); + $GARAGE_ADMIN_URL = $variables->where('key', 'GARAGE_ADMIN_URL')->first(); + + if (is_null($GARAGE_S3_API_URL) || is_null($GARAGE_WEB_URL) || is_null($GARAGE_ADMIN_URL)) { + return collect([]); + } + + if (str($GARAGE_S3_API_URL->value ?? '')->isEmpty()) { + $GARAGE_S3_API_URL->update([ + 'value' => generateUrl(server: $server, random: 's3-'.$uuid, forceHttps: true), + ]); + } + if (str($GARAGE_WEB_URL->value ?? '')->isEmpty()) { + $GARAGE_WEB_URL->update([ + 'value' => generateUrl(server: $server, random: 'web-'.$uuid, forceHttps: true), + ]); + } + if (str($GARAGE_ADMIN_URL->value ?? '')->isEmpty()) { + $GARAGE_ADMIN_URL->update([ + 'value' => generateUrl(server: $server, random: 'admin-'.$uuid, forceHttps: true), + ]); + } + $payload = collect([ + $GARAGE_S3_API_URL->value.':3900', + $GARAGE_WEB_URL->value.':3902', + $GARAGE_ADMIN_URL->value.':3903', + ]); + break; } return $payload; diff --git a/public/svgs/garage.svg b/public/svgs/garage.svg new file mode 100644 index 000000000..18aedeaaf --- /dev/null +++ b/public/svgs/garage.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/compose/garage.yaml b/templates/compose/garage.yaml new file mode 100644 index 000000000..865a9c89a --- /dev/null +++ b/templates/compose/garage.yaml @@ -0,0 +1,60 @@ +# ignore: true +# documentation: https://garagehq.deuxfleurs.fr/documentation/ +# slogan: Garage is an S3-compatible distributed object storage service designed for self-hosting. +# category: storage +# tags: object, storage, server, s3, api, distributed +# logo: svgs/garage.svg +# port: 3900 + +services: + garage: + image: dxflrs/garage:v2.1.0 + environment: + - GARAGE_S3_API_URL=$GARAGE_S3_API_URL + - GARAGE_WEB_URL=$GARAGE_WEB_URL + - GARAGE_ADMIN_URL=$GARAGE_ADMIN_URL + - GARAGE_RPC_SECRET=${SERVICE_HEX_32_RPCSECRET} + - GARAGE_ADMIN_TOKEN=$SERVICE_PASSWORD_GARAGE + - GARAGE_METRICS_TOKEN=$SERVICE_PASSWORD_GARAGEMETRICS + - GARAGE_ALLOW_WORLD_READABLE_SECRETS=true + - RUST_LOG=${RUST_LOG:-garage=info} + volumes: + - garage-meta:/var/lib/garage/meta + - garage-data:/var/lib/garage/data + - type: bind + source: ./garage.toml + target: /etc/garage.toml + content: | + metadata_dir = "/var/lib/garage/meta" + data_dir = "/var/lib/garage/data" + db_engine = "lmdb" + + replication_factor = 1 + consistency_mode = "consistent" + + compression_level = 1 + block_size = "1M" + + [rpc] + bind_addr = "[::]:3901" + rpc_secret_file = "env:GARAGE_RPC_SECRET" + bootstrap_peers = [] + + [s3_api] + s3_region = "garage" + api_bind_addr = "[::]:3900" + root_domain = ".s3.garage.localhost" + + [s3_web] + bind_addr = "[::]:3902" + root_domain = ".web.garage.localhost" + + [admin] + api_bind_addr = "[::]:3903" + admin_token_file = "env:GARAGE_ADMIN_TOKEN" + metrics_token_file = "env:GARAGE_METRICS_TOKEN" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3903/health"] + interval: 10s + timeout: 5s + retries: 5