In-stack S3 backend (Garage)
Optional profile for deployments that don’t want to provision an external S3-compatible provider (AWS S3, Cloudflare R2, Backblaze B2, MinIO, etc.). Runs Garage as a single-node container inside the Compose stack and serves the buckets the api needs locally.
When to use this
Section titled “When to use this”- Self-hosted deployments without a paid S3 provider.
- Air-gapped or compliance-restricted environments where data must stay on the same host.
- Trial / staging stacks where you want zero external dependencies.
How it works
Section titled “How it works”The garage Compose service runs garage server --single-node --default-access-key:
--single-nodeauto-applies a one-node cluster layout on first boot.--default-access-keyauto-creates an S3 access key from theGARAGE_DEFAULT_ACCESS_KEY/GARAGE_DEFAULT_SECRET_KEYenv vars (the compose file maps them fromS3_API_KEY/S3_API_SECRETin.env).
The api creates its own buckets on first boot — no manual bootstrap is required. When it calls CreateBucket on Garage via the S3 API, Garage grants the calling key ownership, so the api can read and write immediately.
Activate the profile
Section titled “Activate the profile”In .env:
COMPOSE_PROFILES=garage # or backup,garage for bothS3_ENDPOINT=http://garage:3900Without garage in COMPOSE_PROFILES, the service is defined in docker-compose.yaml but does not start.
Generate the three secrets
Section titled “Generate the three secrets”Garage needs an S3 access key pair (which the api reuses) and an internal RPC secret:
echo "S3_API_KEY=GK$(openssl rand -hex 16)"echo "S3_API_SECRET=$(openssl rand -hex 32)"echo "GARAGE_RPC_SECRET=$(openssl rand -hex 32)"Paste the three lines into .env. The “GK” prefix is a Garage convention so the key is easy to spot in logs — any string works, but keep it.
First boot
Section titled “First boot”docker compose pulldocker compose up -dThat’s the whole setup. On first start:
-
Garage applies a single-node cluster layout (no
garage layout assignneeded). -
Garage creates the access key from
GARAGE_DEFAULT_ACCESS_KEY/GARAGE_DEFAULT_SECRET_KEY. -
The api runs migrations, creates the project bucket (
sancho-env-${DEPLOY_ENV}), and then seeds the root account, which creates the account bucket (sancho-env-${DEPLOY_ENV}-account-1).
Both buckets are owned by the calling key, so the api can use them immediately.
Verify
Section titled “Verify”docker compose exec garage /garage status # node Up, layout applieddocker compose exec garage /garage key list # the auto-imported keydocker compose exec garage /garage bucket list # 2 buckets, both with the key allowed
# End-to-end: upload an avatar from the web app, thenls ./data/garage/data # blob files appearBackup profile on top
Section titled “Backup profile on top”If you also set COMPOSE_PROFILES=backup,garage, the postgres-backup service writes daily dumps to Garage. It does not create its bucket on its own, so run this one-liner once:
docker compose exec garage /garage bucket create "$BACKUP_S3_BUCKET"Garage grants the default key full access on bucket creation, so no bucket allow step is needed. After that, docker compose exec postgres-backup /backup.sh forces an immediate dump as a sanity check.
Operations
Section titled “Operations”Garage binds 3900 (S3 API), 3901 (RPC), and 3903 (admin) inside the sancho network (garage.toml does not configure [s3_web], so the web port 3902 is not bound). None are exposed on the host. The api reaches it as http://garage:3900; for ad-hoc CLI use, run docker compose exec garage /garage ....
Persistent data
Section titled “Persistent data”| Path | Contents |
|---|---|
./data/garage/meta |
SQLite metadata, bucket/key state |
./data/garage/data |
Object blobs |
Snapshot both directories together — splitting them between snapshots produces an inconsistent state. Stop the stack (docker compose down) before copy-style backups to avoid file-locking races.
Restoring
Section titled “Restoring”docker compose downsudo rm -rf ./data/garagesudo tar -xzf garage-data-YYYY-MM-DD.tar.gz # extracts ./data/garage/{meta,data}docker compose up -dThe access key, bucket list, and allow-list survive in the metadata snapshot — no extra steps after a restore.
Rotating S3 credentials
Section titled “Rotating S3 credentials”Garage tracks keys by access-key ID. Changing S3_API_KEY in .env makes the old key unusable on the api side but the old key still exists in Garage:
-
Generate a new pair:
Terminal window NEW_KEY="GK$(openssl rand -hex 16)"; NEW_SECRET="$(openssl rand -hex 32)" -
Import as a new key in Garage:
Terminal window docker compose exec garage /garage key import "$NEW_KEY" "$NEW_SECRET" -n sancho-new --yes -
Grant access to every bucket the old key could reach:
Terminal window for b in $(docker compose exec garage /garage bucket list | awk '/^ /{print $1}'); dodocker compose exec garage /garage bucket allow --read --write --owner "$b" --key "$NEW_KEY"done -
Update
.envand restartapi+postgres-backup:Terminal window sed -i -e "s|^S3_API_KEY=.*|S3_API_KEY=$NEW_KEY|" \-e "s|^S3_API_SECRET=.*|S3_API_SECRET=$NEW_SECRET|" .envdocker compose up -d api postgres-backup -
(Optional) delete the old key once confirmed working:
Terminal window docker compose exec garage /garage key delete <OLD_KEY_ID> --yes