From 93d924bde9e645bb1105af5619e62c2d90dacf09 Mon Sep 17 00:00:00 2001 From: henmohr Date: Wed, 27 May 2026 22:36:43 -0300 Subject: [PATCH 1/2] Add local Garage S3 stack --- .docker/garages3.config.php | 27 ++++ .env.example | 13 ++ Makefile | 27 ++++ README.md | 33 +++++ docker-compose-garages3.yml | 121 +++++++++++++++++ docs/README_ptBR.md | 33 +++++ garage/.gitignore | 5 + garage/data/.gitkeep | 1 + garage/garage.toml | 22 +++ garage/meta/.gitkeep | 1 + local/.env.dist | 15 ++- local/Makefile | 46 +++++++ local/README.md | 29 ++++ local/docker-compose-garages3.yml | 85 ++++++++++++ scripts/bootstrap-garages3.sh | 215 ++++++++++++++++++++++++++++++ 15 files changed, 672 insertions(+), 1 deletion(-) create mode 100644 .docker/garages3.config.php create mode 100644 Makefile create mode 100644 docker-compose-garages3.yml create mode 100644 garage/.gitignore create mode 100644 garage/data/.gitkeep create mode 100644 garage/garage.toml create mode 100644 garage/meta/.gitkeep create mode 100644 local/docker-compose-garages3.yml create mode 100755 scripts/bootstrap-garages3.sh diff --git a/.docker/garages3.config.php b/.docker/garages3.config.php new file mode 100644 index 0000000..4950f1e --- /dev/null +++ b/.docker/garages3.config.php @@ -0,0 +1,27 @@ + [ + 'class' => '\\OC\\Files\\ObjectStore\\S3', + 'arguments' => [ + 'bucket' => $bucket, + 'hostname' => getenv('GARAGES3_HOSTNAME') ?: 'host.docker.internal', + 'port' => (int) (getenv('GARAGES3_PORT') ?: 3900), + 'region' => getenv('GARAGES3_REGION') ?: 'garage', + 'key' => $key, + 'secret' => $secret, + 'use_ssl' => filter_var(getenv('GARAGES3_USE_SSL') ?: 'false', FILTER_VALIDATE_BOOLEAN), + 'use_path_style' => filter_var(getenv('GARAGES3_USE_PATH_STYLE') ?: 'true', FILTER_VALIDATE_BOOLEAN), + 'autocreate' => filter_var(getenv('GARAGES3_AUTOCREATE') ?: 'true', FILTER_VALIDATE_BOOLEAN), + 'verify_bucket_exists' => filter_var(getenv('GARAGES3_VERIFY_BUCKET_EXISTS') ?: 'true', FILTER_VALIDATE_BOOLEAN), + ], + ], +]; diff --git a/.env.example b/.env.example index 943bb21..3cd1bd4 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,16 @@ NEXTCLOUD_ADMIN_USER= NEXTCLOUD_ADMIN_PASSWORD= NEXTCLOUD_TRUSTED_DOMAINS= + +# Garage S3 primary storage (used by docker-compose-garages3.yml) +GARAGES3_BUCKET=nextcloud +GARAGES3_KEY= +GARAGES3_KEY_ID= +GARAGES3_SECRET= +GARAGES3_HOSTNAME=host.docker.internal +GARAGES3_PORT=3900 +GARAGES3_REGION=garage +GARAGES3_USE_SSL=false +GARAGES3_USE_PATH_STYLE=true +GARAGES3_AUTOCREATE=true +GARAGES3_VERIFY_BUCKET_EXISTS=true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..430bacb --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +COMPOSE ?= docker compose +GARAGES3_COMPOSE_FILE ?= docker-compose-garages3.yml + +.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 + +up-garages3: + $(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) up -d garage + +down-garages3: + $(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) down + +garage-status-garages3: + $(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) exec -T garage /garage status + +bootstrap-garages3: + ./scripts/bootstrap-garages3.sh + +start-garages3: + $(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) up -d db app web cron + +wait-nextcloud-garages3: + @until $(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) exec --user www-data app php occ status --output=json 2>/dev/null | grep -q '"installed":true'; do echo "Awaiting Nextcloud"; sleep 10; done + +setup-garages3: + $(MAKE) bootstrap-garages3 + $(MAKE) start-garages3 + $(MAKE) wait-nextcloud-garages3 diff --git a/README.md b/README.md index 0324123..8b27719 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,39 @@ docker compose exec -u www-data app ./occ db:convert-filecache-bigint You can do this using environments and creating a file called `docker-compose.override.yml` to add new services. +### Garage S3 primary storage + +Use `docker-compose-garages3.yml` when you want Nextcloud to store files in a Garage S3 bucket instead of the local `data/` directory. + +The stack expects these values in `.env`: + +- `GARAGES3_BUCKET` +- `GARAGES3_KEY` +- `GARAGES3_KEY_ID` +- `GARAGES3_SECRET` +- `GARAGES3_HOSTNAME`, defaulting to `host.docker.internal` +- `GARAGES3_PORT`, defaulting to `3900` +- `GARAGES3_REGION`, defaulting to `garage` + +Create the bucket and access key in Garage before starting Nextcloud with this compose file. `GARAGES3_KEY_ID` must contain the Garage access key ID used by Nextcloud. + +The Garage service uses `garage/garage.toml`. Update `rpc_secret` before using it in a real environment. + +Use `make up-garages3` to start Garage, and `make bootstrap-garages3` to create the Garage bucket and access key. +The bootstrap updates `.env` in place with the generated Garage credentials. +Use `make setup-garages3` to run the full setup and wait for Nextcloud to report as installed. +The stack now uses PostgreSQL 16. If you already created the database volume with an older PostgreSQL major version, recreate or migrate that volume once before starting the updated compose file. + +For a clean local installation, use `make reset-garages3`. +Use `make setup-garages3` if you want to keep the existing local state and only rerun the setup steps. + +Basic flow: + +1. Copy `.env.example` to `.env` if needed. +2. Update `garage/garage.toml` and replace the placeholder `rpc_secret`. +3. Run `make setup-garages3`. +4. Open the Nextcloud URL and finish the initial admin setup if it is still pending. + ### PHP - Create your `.ini` file at `volumes/php/` folder. Example: `volumes/php/xdebug.ini` diff --git a/docker-compose-garages3.yml b/docker-compose-garages3.yml new file mode 100644 index 0000000..3c8e0f2 --- /dev/null +++ b/docker-compose-garages3.yml @@ -0,0 +1,121 @@ +networks: + reverse-proxy: + external: true + name: reverse-proxy + internal: + driver: bridge + +services: + db: + image: postgres:16 + restart: always + volumes: + - ./volumes/postgres/data:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB:-nextcloud} + - POSTGRES_USER=${POSTGRES_USER:-nextcloud} + networks: + - internal + + garage: + image: dxflrs/garage:v1.0.0 + restart: unless-stopped + network_mode: host + volumes: + - ./garage/garage.toml:/etc/garage.toml + - ./garage/meta:/var/lib/garage/meta + - ./garage/data:/var/lib/garage/data + + app: + build: + context: .docker/app + args: + NEXTCLOUD_VERSION: ${NEXTCLOUD_VERSION:-stable-fpm} + restart: unless-stopped + volumes: + - ./volumes/nextcloud:/var/www/html + - ./app-hooks/post-installation:/docker-entrypoint-hooks.d/post-installation + - ./.docker/garages3.config.php:/var/www/html/config/garages3.config.php:ro + environment: + - POSTGRES_DB=${POSTGRES_DB:-nextcloud} + - POSTGRES_USER=${POSTGRES_USER:-nextcloud} + - POSTGRES_PASSWORD + - POSTGRES_HOST=db + - NEXTCLOUD_ADMIN_USER + - NEXTCLOUD_ADMIN_PASSWORD + - NEXTCLOUD_ADMIN_EMAIL + - NEXTCLOUD_TRUSTED_DOMAINS + - SMTP_HOST + - SMTP_SECURE + - SMTP_PORT + - SMTP_AUTHTYPE + - SMTP_NAME + - SMTP_PASSWORD + - MAIL_FROM_ADDRESS + - MAIL_DOMAIN + - TZ + - GARAGES3_BUCKET + - GARAGES3_KEY_ID=${GARAGES3_KEY_ID:-${GARAGES3_KEY:-}} + - GARAGES3_SECRET=${GARAGES3_SECRET:-} + - GARAGES3_HOSTNAME=${GARAGES3_HOSTNAME:-host.docker.internal} + - GARAGES3_PORT=${GARAGES3_PORT:-3900} + - GARAGES3_REGION=${GARAGES3_REGION:-garage} + - GARAGES3_USE_SSL=${GARAGES3_USE_SSL:-0} + - GARAGES3_USE_PATH_STYLE=${GARAGES3_USE_PATH_STYLE:-1} + - GARAGES3_AUTOCREATE=${GARAGES3_AUTOCREATE:-1} + - GARAGES3_VERIFY_BUCKET_EXISTS=${GARAGES3_VERIFY_BUCKET_EXISTS:-1} + extra_hosts: + - host.docker.internal:host-gateway + depends_on: + - db + - garage + networks: + - internal + + web: + build: .docker/web + restart: unless-stopped + volumes: + - ./volumes/nextcloud:/var/www/html:ro + - ./volumes/nginx/includes:/etc/nginx/conf.d/includes:rw + environment: + - VIRTUAL_HOST + - LETSENCRYPT_HOST + - LETSENCRYPT_EMAIL + - TZ + depends_on: + - app + networks: + - internal + - reverse-proxy + + cron: + build: + context: .docker/app + args: + NEXTCLOUD_VERSION: ${NEXTCLOUD_VERSION:-stable-fpm} + restart: unless-stopped + environment: + - TZ + - GARAGES3_BUCKET + - GARAGES3_KEY_ID=${GARAGES3_KEY_ID:-${GARAGES3_KEY:-}} + - GARAGES3_SECRET=${GARAGES3_SECRET:-} + - GARAGES3_HOSTNAME=${GARAGES3_HOSTNAME:-host.docker.internal} + - GARAGES3_PORT=${GARAGES3_PORT:-3900} + - GARAGES3_REGION=${GARAGES3_REGION:-garage} + - GARAGES3_USE_SSL=${GARAGES3_USE_SSL:-0} + - GARAGES3_USE_PATH_STYLE=${GARAGES3_USE_PATH_STYLE:-1} + - GARAGES3_AUTOCREATE=${GARAGES3_AUTOCREATE:-1} + - GARAGES3_VERIFY_BUCKET_EXISTS=${GARAGES3_VERIFY_BUCKET_EXISTS:-1} + volumes: + - ./volumes/nextcloud:/var/www/html + - ./.docker/garages3.config.php:/var/www/html/config/garages3.config.php:ro + extra_hosts: + - host.docker.internal:host-gateway + depends_on: + - db + - garage + networks: + - internal + entrypoint: /cron.sh diff --git a/docs/README_ptBR.md b/docs/README_ptBR.md index 18c1d64..fb01964 100644 --- a/docs/README_ptBR.md +++ b/docs/README_ptBR.md @@ -77,6 +77,39 @@ docker compose exec -u www-data app ./occ db:convert-filecache-bigint Você pode fazer isso usando variáveis de ambiente e criando um arquivo chamado `docker-compose.override.yml` para adicionar novos serviços. +### Storage primário Garage S3 + +Use `docker-compose-garages3.yml` quando quiser que o Nextcloud grave os arquivos em um bucket Garage S3 em vez do diretório local `data/`. + +O stack espera estes valores em `.env`: + +- `GARAGES3_BUCKET` +- `GARAGES3_KEY` +- `GARAGES3_KEY_ID` +- `GARAGES3_SECRET` +- `GARAGES3_HOSTNAME`, com padrão `host.docker.internal` +- `GARAGES3_PORT`, com padrão `3900` +- `GARAGES3_REGION`, com padrão `garage` + +Crie o bucket e a chave de acesso no Garage antes de iniciar o Nextcloud com este compose. `GARAGES3_KEY_ID` deve conter o access key ID do Garage usado pelo Nextcloud. + +O serviço Garage usa `garage/garage.toml`. Atualize `rpc_secret` antes de usar em ambiente real. + +Use `make up-garages3` para subir o Garage e `make bootstrap-garages3` para criar o bucket e a chave de acesso no Garage. +O bootstrap atualiza `.env` com as credenciais geradas do Garage. +Use `make setup-garages3` para executar a configuração completa e aguardar o Nextcloud reportar como instalado. +O stack agora usa PostgreSQL 16. Se você já criou o volume do banco com uma versão major mais antiga, recrie ou migre esse volume uma vez antes de subir o compose atualizado. + +Para uma instalação local limpa, use `make reset-garages3`. +Use `make setup-garages3` se quiser manter o estado local existente e apenas repetir as etapas de setup. + +Fluxo básico: + +1. Copie `.env.example` para `.env`, se necessário. +2. Atualize `garage/garage.toml` e substitua o `rpc_secret` placeholder. +3. Execute `make setup-garages3`. +4. Abra a URL do Nextcloud e finalize a configuração inicial do admin, se ainda estiver pendente. + ### PHP - Crie seu arquivo `.ini` na pasta `volumes/php/`. Exemplo: `volumes/php/xdebug.ini` diff --git a/garage/.gitignore b/garage/.gitignore new file mode 100644 index 0000000..a8a5d72 --- /dev/null +++ b/garage/.gitignore @@ -0,0 +1,5 @@ +meta/* +data/* +!.gitignore +!meta/.gitkeep +!data/.gitkeep diff --git a/garage/data/.gitkeep b/garage/data/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/garage/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/garage/garage.toml b/garage/garage.toml new file mode 100644 index 0000000..28435e8 --- /dev/null +++ b/garage/garage.toml @@ -0,0 +1,22 @@ +metadata_dir = "/var/lib/garage/meta" +data_dir = "/var/lib/garage/data" +db_engine = "lmdb" +metadata_auto_snapshot_interval = "6h" + +replication_factor = 3 + +compression_level = 2 + +rpc_bind_addr = "[::]:3901" +rpc_public_addr = ":3901" +rpc_secret = "99eed281cbe8f1ae7bba15bc4091f57bdbacf913f0e2487ad53b65d88c4955fa" + +[s3_api] +s3_region = "garage" +api_bind_addr = "[::]:3900" +root_domain = ".s3.garage" + +[s3_web] +bind_addr = "[::]:3902" +root_domain = ".web.garage" +index = "index.html" diff --git a/garage/meta/.gitkeep b/garage/meta/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/garage/meta/.gitkeep @@ -0,0 +1 @@ + diff --git a/local/.env.dist b/local/.env.dist index aa8fedd..5ee16c5 100644 --- a/local/.env.dist +++ b/local/.env.dist @@ -3,5 +3,18 @@ POSTGRES_DB=nextcloud POSTGRES_USER=nextcloud NEXTCLOUD_ADMIN_USER=admin NEXTCLOUD_ADMIN_PASSWORD=admin -NEXTCLOUD_TRUSTED_DOMAINS=mydomain.coop +NEXTCLOUD_TRUSTED_DOMAINS=localhost,127.0.0.1,localhost:8080,127.0.0.1:8080 CA_STORE=/usr/local/share/ca-certificates +NEXTCLOUD_VERSION=stable-apache + +GARAGES3_BUCKET=nextcloud +GARAGES3_KEY= +GARAGES3_KEY_ID= +GARAGES3_SECRET= +GARAGES3_HOSTNAME=garage +GARAGES3_PORT=3900 +GARAGES3_REGION=garage +GARAGES3_USE_SSL=false +GARAGES3_USE_PATH_STYLE=true +GARAGES3_AUTOCREATE=true +GARAGES3_VERIFY_BUCKET_EXISTS=true diff --git a/local/Makefile b/local/Makefile index 24d7f1b..2e7f40b 100644 --- a/local/Makefile +++ b/local/Makefile @@ -24,3 +24,49 @@ set-config: init-cron: docker-compose up -d cron + +GARAGES3_COMPOSE_FILE=docker-compose-garages3.yml + +init-all-garages3: init-garages3-db init-garages3-bootstrap init-garages3-volume init-garages3-perms init-garages3-app set-locale-garages3 set-config-garages3 init-garages3-cron + +setup-garages3: init-all-garages3 + +reset-garages3: down-garages3 clean-garages3-state setup-garages3 + +down-garages3: + docker compose -f $(GARAGES3_COMPOSE_FILE) down + +clean-garages3-state: + docker run --rm -v $(CURDIR)/volumes/postgres/data:/data alpine:3.20 sh -lc 'rm -rf /data/* /data/.[!.]* /data/..?*' + docker run --rm -v $(CURDIR)/volumes/nextcloud:/data alpine:3.20 sh -lc 'rm -rf /data/* /data/.[!.]* /data/..?*' + docker run --rm -v $(CURDIR)/garage:/data alpine:3.20 sh -lc 'mkdir -p /data/meta /data/data && rm -rf /data/meta/* /data/data/*' + rm -f .env + +init-garages3-db: + docker compose -f $(GARAGES3_COMPOSE_FILE) up -d db + until docker compose -f $(GARAGES3_COMPOSE_FILE) exec db pg_isready; do echo "Awaiting for Postgres"; sleep 5; done + +init-garages3-bootstrap: + GARAGES3_COMPOSE_FILE=$(GARAGES3_COMPOSE_FILE) GARAGES3_GARAGE_TOML=garage/garage.toml GARAGES3_ENV_FILE=.env GARAGES3_HOSTNAME=garage GARAGES3_AUTO_LAYOUT=true GARAGES3_LAYOUT_ZONE=local GARAGES3_LAYOUT_CAPACITY=1TB ../scripts/bootstrap-garages3.sh + +init-garages3-volume: + docker compose -f $(GARAGES3_COMPOSE_FILE) run --rm --user root --entrypoint sh app -lc 'if [ ! -f /var/www/html/lib/versioncheck.php ]; then cd /usr/src/nextcloud && tar --exclude=./config/s3.config.php -cf - . | tar -xf - -C /var/www/html/; fi' + +init-garages3-perms: + docker compose -f $(GARAGES3_COMPOSE_FILE) run --rm --user root --entrypoint sh app -lc 'find /var/www/html \( -path /var/www/html/config/s3.config.php \) -prune -o -exec chown www-data:www-data {} +' + +init-garages3-app: + docker compose -f $(GARAGES3_COMPOSE_FILE) up -d app + if ! docker compose -f $(GARAGES3_COMPOSE_FILE) exec -w /var/www/html --user www-data app php occ status --output=json | grep -q "{\"installed\":true,"; then docker compose -f $(GARAGES3_COMPOSE_FILE) exec -w /var/www/html --user www-data app sh -lc 'php occ maintenance:install --database pgsql --database-host "$$POSTGRES_HOST" --database-name "$$POSTGRES_DB" --database-user "$$POSTGRES_USER" --database-pass "$$POSTGRES_PASSWORD" --admin-user "$$NEXTCLOUD_ADMIN_USER" --admin-pass "$$NEXTCLOUD_ADMIN_PASSWORD" --no-interaction'; fi + until docker compose -f $(GARAGES3_COMPOSE_FILE) exec -w /var/www/html --user www-data app php occ status --output=json | grep -e "{\"installed\":true,"; do echo "Awaiting for NextCloud installation"; sleep 10; done + +set-locale-garages3: + docker compose -f $(GARAGES3_COMPOSE_FILE) exec -w /var/www/html --user www-data app php occ config:system:set default_locale --value ${LANG} + docker compose -f $(GARAGES3_COMPOSE_FILE) exec -w /var/www/html --user www-data app php occ config:system:set default_language --value ${LANG} + docker compose -f $(GARAGES3_COMPOSE_FILE) exec -w /var/www/html --user www-data app sh -c "php occ user:setting \$$NEXTCLOUD_ADMIN_USER core lang ${LANG}" + +set-config-garages3: + docker compose -f $(GARAGES3_COMPOSE_FILE) exec -w /var/www/html --user www-data app php occ config:system:set skeletondirectory --value "" + +init-garages3-cron: + docker compose -f $(GARAGES3_COMPOSE_FILE) up -d cron diff --git a/local/README.md b/local/README.md index 3bf9164..73ea2d5 100644 --- a/local/README.md +++ b/local/README.md @@ -31,3 +31,32 @@ Também é necessário adicionar o dominio customizado de acordo com o que foi i ``` Realizado as etapas acima, basta rodar o Makefile, o que pode ser feito executando o comando `make` na raiz do projeto: + +## Stack local com Garage S3 + +Se você quiser testar o Nextcloud localmente usando o Garage como storage primário, use o compose dedicado: + +```bash +cp .env.dist .env +make reset-garages3 +``` + +Se você quiser reaproveitar o estado local existente em vez de apagar tudo, use: + +```bash +make setup-garages3 +``` + +Essa variante: + +- expõe o Nextcloud em `http://localhost:8080` +- sobe o Garage junto com o banco e o cron +- usa o Garage em modo single-node para funcionar localmente +- aplica automaticamente o layout do Garage para permitir bucket/key com um nó +- cria automaticamente o bucket e a key do Garage +- grava o access key ID em `GARAGES3_KEY_ID` e o secret em `GARAGES3_SECRET` +- usa o arquivo `../volumes/nextcloud/config/s3.config.php` para o object store +- `make reset-garages3` faz a limpeza e reinstala tudo do zero + +Se o `garage/garage.toml` ainda estiver com `rpc_secret` placeholder, o bootstrap substitui o valor antes de subir o daemon. +Se você já tiver iniciado esse stack com PostgreSQL 12, precisará recriar ou migrar o volume de banco uma vez para o PostgreSQL 16. diff --git a/local/docker-compose-garages3.yml b/local/docker-compose-garages3.yml new file mode 100644 index 0000000..a125769 --- /dev/null +++ b/local/docker-compose-garages3.yml @@ -0,0 +1,85 @@ +networks: + internal: + +services: + db: + image: postgres:16 + restart: always + volumes: + - ./volumes/postgres/data:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB:-nextcloud} + - POSTGRES_USER=${POSTGRES_USER:-nextcloud} + networks: + - internal + + garage: + image: dxflrs/garage:v1.0.0 + restart: unless-stopped + volumes: + - ./garage/garage.toml:/etc/garage.toml + - ./garage/meta:/var/lib/garage/meta + - ./garage/data:/var/lib/garage/data + networks: + - internal + + app: + image: nextcloud:${NEXTCLOUD_VERSION:-stable-apache} + restart: always + ports: + - "8080:80" + volumes: + - ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/custom-php.ini + - ./volumes/nextcloud:/var/www/html + - ../volumes/nextcloud/config/s3.config.php:/var/www/html/config/s3.config.php:ro + environment: + - POSTGRES_DB=${POSTGRES_DB:-nextcloud} + - POSTGRES_HOST=${POSTGRES_HOST:-db} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD} + - POSTGRES_USER=${POSTGRES_USER:-nextcloud} + - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin} + - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} + - NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS:-localhost,127.0.0.1,localhost:8080,127.0.0.1:8080} + - OBJECTSTORE_S3_BUCKET=${GARAGES3_BUCKET:-nextcloud} + - OBJECTSTORE_S3_REGION=${GARAGES3_REGION:-garage} + - OBJECTSTORE_S3_HOST=${GARAGES3_HOSTNAME:-garage} + - OBJECTSTORE_S3_PORT=${GARAGES3_PORT:-3900} + - OBJECTSTORE_S3_SSL=${GARAGES3_USE_SSL:-false} + - OBJECTSTORE_S3_USEPATH_STYLE=${GARAGES3_USE_PATH_STYLE:-true} + - OBJECTSTORE_S3_AUTOCREATE=${GARAGES3_AUTOCREATE:-true} + - OBJECTSTORE_S3_KEY=${GARAGES3_KEY_ID:-${GARAGES3_KEY:-}} + - OBJECTSTORE_S3_SECRET=${GARAGES3_SECRET:-} + depends_on: + - db + - garage + networks: + - internal + + cron: + image: nextcloud:${NEXTCLOUD_VERSION:-stable-apache} + restart: unless-stopped + volumes: + - ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/custom-php.ini + - ./volumes/nextcloud:/var/www/html + - ../volumes/nextcloud/config/s3.config.php:/var/www/html/config/s3.config.php:ro + environment: + - POSTGRES_DB=${POSTGRES_DB:-nextcloud} + - POSTGRES_HOST=${POSTGRES_HOST:-db} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD} + - POSTGRES_USER=${POSTGRES_USER:-nextcloud} + - OBJECTSTORE_S3_BUCKET=${GARAGES3_BUCKET:-nextcloud} + - OBJECTSTORE_S3_REGION=${GARAGES3_REGION:-garage} + - OBJECTSTORE_S3_HOST=${GARAGES3_HOSTNAME:-garage} + - OBJECTSTORE_S3_PORT=${GARAGES3_PORT:-3900} + - OBJECTSTORE_S3_SSL=${GARAGES3_USE_SSL:-false} + - OBJECTSTORE_S3_USEPATH_STYLE=${GARAGES3_USE_PATH_STYLE:-true} + - OBJECTSTORE_S3_AUTOCREATE=${GARAGES3_AUTOCREATE:-true} + - OBJECTSTORE_S3_KEY=${GARAGES3_KEY_ID:-${GARAGES3_KEY:-}} + - OBJECTSTORE_S3_SECRET=${GARAGES3_SECRET:-} + entrypoint: /cron.sh + depends_on: + - db + - garage + networks: + - internal diff --git a/scripts/bootstrap-garages3.sh b/scripts/bootstrap-garages3.sh new file mode 100755 index 0000000..dfab0dd --- /dev/null +++ b/scripts/bootstrap-garages3.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +set -euo pipefail + +compose_file=${GARAGES3_COMPOSE_FILE:-docker-compose-garages3.yml} +garage_service=${GARAGES3_SERVICE:-garage} +garage_toml=${GARAGES3_GARAGE_TOML:-garage/garage.toml} +bucket_name=${GARAGES3_BUCKET:-nextcloud} +key_name=${GARAGES3_KEY_NAME:-nextcloud-app} +env_file=${GARAGES3_ENV_FILE:-.env} +skip_garage_up=${GARAGES3_SKIP_GARAGE_UP:-0} +auto_layout=${GARAGES3_AUTO_LAYOUT:-false} +layout_zone=${GARAGES3_LAYOUT_ZONE:-local} +layout_capacity=${GARAGES3_LAYOUT_CAPACITY:-1TB} + +compose() { + docker compose -f "$compose_file" "$@" +} + +garage_exec() { + compose exec -T "$garage_service" /garage "$@" +} + +env_value() { + local file=$1 + local key=$2 + + if [[ ! -f "$file" ]]; then + return 0 + fi + + awk -F= -v key="$key" ' + $1 == key { + print substr($0, length(key) + 2) + exit + } + ' "$file" +} + +ensure_rpc_secret() { + if grep -q 'rpc_secret = "CHANGE_ME_WITH_OPENSSL_RAND_HEX_32"' "$garage_toml"; then + local rpc_secret + rpc_secret=${GARAGES3_RPC_SECRET:-} + + if [[ -z "$rpc_secret" ]]; then + if command -v openssl >/dev/null 2>&1; then + rpc_secret=$(openssl rand -hex 32) + else + echo "openssl is required when GARAGES3_RPC_SECRET is not set" >&2 + exit 1 + fi + fi + + perl -0pi -e 's/^rpc_secret = ".*"$/rpc_secret = "'"$rpc_secret"'"/m' "$garage_toml" + echo "Updated $garage_toml with a rpc_secret value" + fi +} + +ensure_rpc_secret + +if [[ ! -f "$env_file" ]]; then + if [[ -f .env.example ]]; then + cp .env.example "$env_file" + else + : > "$env_file" + fi +fi + +if [[ "$skip_garage_up" != 1 ]]; then + compose up -d --force-recreate "$garage_service" +fi + +until garage_exec status >/dev/null 2>&1; do + sleep 2 +done + +if [[ "$auto_layout" == 1 || "${auto_layout,,}" == "true" ]]; then + if garage_exec layout show | grep -q "No nodes currently have a role in the cluster."; then + node_id=$(garage_exec node id | awk -F'@' 'NR == 1 {print $1; exit}') + if [[ -z "$node_id" ]]; then + echo "Could not determine Garage node id for automatic layout setup" >&2 + exit 1 + fi + + garage_exec layout assign "$node_id" -z "$layout_zone" -c "$layout_capacity" >/dev/null + layout_show_output=$(garage_exec layout show) + layout_version=$(printf '%s\n' "$layout_show_output" | awk '/garage layout apply --version/ {print $5; exit}') + if [[ -z "$layout_version" ]]; then + echo "$layout_show_output" >&2 + echo "Could not determine the new Garage layout version" >&2 + exit 1 + fi + + garage_exec layout apply --version "$layout_version" >/dev/null + echo "Applied automatic Garage layout for node $node_id (version $layout_version)" + + until garage_exec status | grep -q "HEALTHY NODES"; do + sleep 2 + done + fi +fi + +garage_exec bucket create "$bucket_name" >/dev/null 2>&1 || true + +secret_key=$(env_value "$env_file" GARAGES3_SECRET) +existing_key_id=$(env_value "$env_file" GARAGES3_KEY_ID) +legacy_key=$(env_value "$env_file" GARAGES3_KEY) + +if [[ -n "$existing_key_id" && -n "$secret_key" ]]; then + key_id="$existing_key_id" + echo "Reusing Garage key credentials from $env_file" +elif [[ -n "$legacy_key" && -n "$secret_key" ]]; then + key_id=$(garage_exec key list | awk -v name="$key_name" '$2 == name {print $1; exit}') + + if [[ -n "$key_id" ]]; then + echo "Reusing Garage key credentials from $env_file" + else + matching_key_ids=$(garage_exec key list | awk -v name="$key_name" '$2 == name {print $1}') + + if [[ -n "$matching_key_ids" ]]; then + while read -r stale_key_id; do + [[ -n "$stale_key_id" ]] || continue + garage_exec key delete --yes "$stale_key_id" >/dev/null + done <<< "$matching_key_ids" + echo "Removed stale Garage keys named $key_name" + fi + + key_output=$(garage_exec key create "$key_name" 2>&1 || true) + if [[ "$key_output" != *"Secret key:"* ]]; then + echo "$key_output" >&2 + echo "Failed to create Garage key $key_name" >&2 + exit 1 + fi + + key_id=$(printf '%s\n' "$key_output" | awk -F': ' '/^Key ID:/ {print $2; exit}') + secret_key=$(printf '%s\n' "$key_output" | awk -F': ' '/^Secret key:/ {print $2; exit}') + if [[ -z "$key_id" ]]; then + key_id=$(printf '%s\n' "$key_output" | awk '/^GK[0-9a-f]+/ {print $1; exit}') + fi + if [[ -z "$secret_key" ]]; then + echo "$key_output" >&2 + echo "Could not extract the secret key from Garage output" >&2 + exit 1 + fi + if [[ -z "$key_id" ]]; then + echo "$key_output" >&2 + echo "Could not extract the Garage access key ID from output" >&2 + exit 1 + fi + fi +else + key_output=$(garage_exec key create "$key_name" 2>&1 || true) + if [[ "$key_output" != *"Secret key:"* ]]; then + echo "$key_output" >&2 + echo "Failed to create Garage key $key_name" >&2 + exit 1 + fi + + key_id=$(printf '%s\n' "$key_output" | awk -F': ' '/^Key ID:/ {print $2; exit}') + secret_key=$(printf '%s\n' "$key_output" | awk -F': ' '/^Secret key:/ {print $2; exit}') + if [[ -z "$key_id" ]]; then + key_id=$(printf '%s\n' "$key_output" | awk '/^GK[0-9a-f]+/ {print $1; exit}') + fi + if [[ -z "$secret_key" ]]; then + echo "$key_output" >&2 + echo "Could not extract the secret key from Garage output" >&2 + exit 1 + fi + if [[ -z "$key_id" ]]; then + echo "$key_output" >&2 + echo "Could not extract the Garage access key ID from output" >&2 + exit 1 + fi +fi + +garage_exec bucket allow --read --write --key "$key_id" "$bucket_name" + +write_env() { + local file=$1 + local key=$2 + local value=$3 + local tmp + + tmp=$(mktemp) + awk -v key="$key" -v value="$value" ' + BEGIN { found = 0 } + $0 ~ "^" key "=" { + print key "=" value + found = 1 + next + } + { print } + END { + if (!found) { + print key "=" value + } + } + ' "$file" > "$tmp" + mv "$tmp" "$file" +} + +write_env "$env_file" GARAGES3_BUCKET "$bucket_name" +write_env "$env_file" GARAGES3_KEY "$key_name" +write_env "$env_file" GARAGES3_KEY_ID "$key_id" +write_env "$env_file" GARAGES3_SECRET "$secret_key" +write_env "$env_file" GARAGES3_HOSTNAME "${GARAGES3_HOSTNAME:-host.docker.internal}" +write_env "$env_file" GARAGES3_PORT "${GARAGES3_PORT:-3900}" +write_env "$env_file" GARAGES3_REGION "${GARAGES3_REGION:-garage}" +write_env "$env_file" GARAGES3_USE_SSL "${GARAGES3_USE_SSL:-false}" +write_env "$env_file" GARAGES3_USE_PATH_STYLE "${GARAGES3_USE_PATH_STYLE:-true}" +write_env "$env_file" GARAGES3_AUTOCREATE "${GARAGES3_AUTOCREATE:-true}" +write_env "$env_file" GARAGES3_VERIFY_BUCKET_EXISTS "${GARAGES3_VERIFY_BUCKET_EXISTS:-true}" + +echo "Wrote Garage credentials to $env_file" +echo "Bucket: $bucket_name" +echo "Key: $key_name" From 83534dda6984da60d33a6bbec043971a131bd17d Mon Sep 17 00:00:00 2001 From: henmohr Date: Wed, 27 May 2026 22:37:16 -0300 Subject: [PATCH 2/2] Add local Garage runtime ignore --- local/garage/.gitignore | 5 +++++ local/garage/garage.toml | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 local/garage/.gitignore create mode 100644 local/garage/garage.toml diff --git a/local/garage/.gitignore b/local/garage/.gitignore new file mode 100644 index 0000000..a8a5d72 --- /dev/null +++ b/local/garage/.gitignore @@ -0,0 +1,5 @@ +meta/* +data/* +!.gitignore +!meta/.gitkeep +!data/.gitkeep diff --git a/local/garage/garage.toml b/local/garage/garage.toml new file mode 100644 index 0000000..346fdde --- /dev/null +++ b/local/garage/garage.toml @@ -0,0 +1,22 @@ +metadata_dir = "/var/lib/garage/meta" +data_dir = "/var/lib/garage/data" +db_engine = "lmdb" +metadata_auto_snapshot_interval = "6h" + +replication_factor = 1 + +compression_level = 2 + +rpc_bind_addr = "[::]:3901" +rpc_public_addr = "127.0.0.1:3901" +rpc_secret = "5f34a73fca71d4c1e8246781d619670f2f1b5e9a1abeceaf04b2d7b5c7cfe778" + +[s3_api] +s3_region = "garage" +api_bind_addr = "[::]:3900" +root_domain = ".s3.garage" + +[s3_web] +bind_addr = "[::]:3902" +root_domain = ".web.garage" +index = "index.html"