storage/README.md
Sergei Poljanski e9388c0b0d
All checks were successful
Deploy storage stack / deploy (push) Successful in 20s
Remove MinIO
Garage serves all three buckets and both apps, and a last comparison
found nothing in MinIO that Garage lacks. Drop the MinIO service, its
bootstrap and its root credentials from the deploy, and let compose
remove the leftover containers.

The storage_minio_data volume is left on the host. All images can be
pulled again, so the deploy goes back to failing on a pull error.

Document Garage in the README and SECRETS, including the key format and
how the existing secrets map to it, and swap the MinIO client in the
flake for awscli and rclone.

Assisted-by: Claude:fable-5-2
2026-11-11 03:55:18 +04:00

3 KiB

storage

Postgres + Garage (S3) for asxp.io and any future small services that need a database or object storage. Single-host, no clustering. Deploys via Forgejo Actions to the same box that runs the apps.

What's here

  • docker-compose.yml — postgres, its one-shot init container, and garage.
  • garage.toml — Garage's config (single node, no secrets in it).
  • init/postgres-init.sh — idempotent role/DB bootstrap.
  • init/garage-bootstrap.sh — idempotent buckets, per-app keys and grants; run on the host by the deploy, through docker exec.
  • .forgejo/workflows/deploy.yaml — pushes compose + .env to the prod host and runs docker compose up -d.

Networking

Two Docker networks:

  • storage — internal-only; apps join this as an external network to reach postgres:5432 and garage:3900.
  • traefik — existing reverse-proxy network. Only Garage joins it, to expose the S3 API at https://s3.asxp.io.

Garage replaced MinIO in November 2026 (MinIO was abandoned upstream and its images removed from the registries).

The public bucket

Garage answers no anonymous S3 request. public is marked as a website instead, and Traefik sends unsigned GET/HEAD of https://s3.asxp.io/public/<key> to Garage's web endpoint. Anyone who knows a key can read it; nobody can list the bucket. Uploading needs the uploader key (see SECRETS.md).

Looking inside

The image has no shell; use the CLI through docker exec:

ssh prod 'docker exec storage-garage /garage status'
ssh prod 'docker exec storage-garage /garage bucket list'
ssh prod 'docker exec storage-garage /garage key list'

Adding a new app

  1. Pick an app name, e.g. foo.
  2. Pick a DB password, S3 access key, S3 secret key for it (any random hex).
  3. Add Forgejo secrets FOO_DB_PASSWORD, FOO_S3_ACCESS_KEY, FOO_S3_SECRET_KEY to this repo and to the foo repo.
  4. Extend init/postgres-init.sh with ensure_role "foo" ... and ensure_db "foo" "foo".
  5. Extend init/garage-bootstrap.sh with ensure_bucket "foo-...", an ensure_key foo ... and a grant for it.
  6. Pass the new envs through in docker-compose.yml and the deploy workflow.
  7. Push.

State and backups

Three named Docker volumes are stateful:

  • pg_data (Postgres data dir) — /var/lib/docker/volumes/storage_pg_data/_data
  • garage_data (S3 object bytes) and garage_meta (Garage's database, with a snapshot every 6 h) — /var/lib/docker/volumes/storage_garage_*/_data

/opt/storage/secrets/garage_rpc_secret (Garage's node-to-node and CLI secret) is made on the host at the first deploy and is not in Forgejo.

Server-level backup covers all three. If you ever move off whole-server backup, the minimum is pg_dumpall for Postgres and rclone sync of each bucket.

The old storage_minio_data volume is still on the host from before the move; nothing uses it.

Secrets

See SECRETS.md for the full list of Forgejo secrets this repo needs. Apps that consume storage (asxpio, future) also need the per-app credentials added on their repo so they can connect.