storage/README.md
Sergei Poljanski 25ad601754
All checks were successful
Deploy storage stack / deploy (push) Successful in 19s
garage-bootstrap: let asxpio write to the public bucket
asxpio's admin is getting a page for uploading files meant for public
use. They belong in the public bucket, which so far only the
public-upload key could write to. Grant asxpio's existing key put and
delete there instead of handing the app a second set of credentials.

Write-only is enough: the app keeps its own index of what it uploaded
and never reads the objects back.

Assisted-by: Claude:fable-5-2
2026-11-12 05:46:48 +04:00

81 lines
3.1 KiB
Markdown

# 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`) or asxpio's key, which may put and delete there for its
admin's file uploads.
## Looking inside
The image has no shell; use the CLI through `docker exec`:
```sh
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`](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.