All checks were successful
Deploy storage stack / deploy (push) Successful in 19s
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
81 lines
3.1 KiB
Markdown
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.
|