docs(backup): make the backup commands work; fix the e2e/prod divergences
Backup. Both documented commands failed on the shipped stack, and the sentence
explaining them was wrong too:
- `pg_dump $DATABASE_URL` — `DATABASE_URL` is only ever in the compose
environment, never an operator's shell, and it points at `db:5432`, which is
compose-internal DNS. The app image has no postgres client either.
- `> /media/backups/…` — `/media` is a named volume mounted inside the app
container, not a host path, and nothing ever creates a `backups` subdirectory.
- `rsync /opt/eventsnap/media/` — that path does not exist anywhere.
- "a single path to back up" — false, and dangerously so: exports were moved to
their own `exports_data` volume precisely so a keepsake (which contains every
photo in the event) can't be served off the media tree. Backing up only
`media_data` silently loses every generated keepsake.
Rewritten as three commands — db via `docker compose exec -T db pg_dump`, and one
`docker run … tar` per volume — all verified against the running stack. The
volume mounts use `/src`, not `/media`: I hit the footgun while testing this.
Docker pre-populates an EMPTY volume from the image's own directory and chowns it
to match, so `-v media_data:/media alpine` tars alpine's cdrom/floppy/usb, writes
them into the volume, and leaves it root-owned so the non-root app can no longer
write. Mounting where the image has nothing avoids all of it. Documented inline
so the next person doesn't rediscover it.
Also correct the architecture notes: `/media/*` no longer routes to the backend
(that static tree was removed as a gating bypass), and `exports_data` was missing
from the volume list — the one volume an operator most needs to know about.
e2e stack: add the `EXPORT_PATH` + `/exports` volume it was missing. The file
says "mirrors production layout"; without these, exports landed on the container's
writable layer at the default path, so export-leak and export-video wrote real
archives into ephemeral storage and the "exports live outside media" invariant
was never actually exercised.
Pre-existing red test, unrelated to the audit: all four 02-upload/quota tests
have been failing since 4464147 "stop /me/quota leaking raw disk to guests"
(2026-07-19), which post-dates the spec's last edit. `setLimitTo` calibrated
`quota_tolerance` from `free_disk_bytes` read through the GUEST's token — a field
that commit deliberately zeroes for non-staff. Dividing by it yields a NaN
tolerance, so every test in the block died in the helper. Read the calibration
inputs through a staff token and keep reading the ceiling back through the guest,
whose limit is the thing under test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
README.md
51
README.md
@@ -162,23 +162,58 @@ See [.env.example](.env.example) for the full list with descriptions and default
|
||||
└────────┘
|
||||
```
|
||||
|
||||
- `/api/*` and `/media/*` → Rust backend
|
||||
- `/api/*` → Rust backend
|
||||
- Everything else → SvelteKit frontend (`adapter-node`)
|
||||
- Named volumes: `postgres_data`, `media_data`, `caddy_data`
|
||||
- Named volumes: `postgres_data`, `media_data`, `exports_data`, `caddy_data`
|
||||
|
||||
Media is **not** served as static files. Every image goes through a
|
||||
visibility-checked alias (`/api/v1/upload/{id}/{preview,display,thumbnail,original}`)
|
||||
so a host takedown or a ban actually revokes access to the bytes.
|
||||
|
||||
---
|
||||
|
||||
## Backup
|
||||
|
||||
```bash
|
||||
# Database snapshot
|
||||
pg_dump $DATABASE_URL | gzip > /media/backups/db_$(date +%Y-%m-%d).sql.gz
|
||||
There are **three** things to back up, and they live in three different places.
|
||||
`DATABASE_URL` and the container paths (`/media`, `/exports`) are meaningful only
|
||||
*inside* the compose network — they are not host paths, and `DATABASE_URL` is
|
||||
never exported into an operator's shell — so every command below runs through
|
||||
`docker compose` from the repo directory.
|
||||
|
||||
# Weekly offsite sync (Hetzner Storage Box or similar)
|
||||
rsync -az /opt/eventsnap/media/ user@storagebox.example.com:backup/eventsnap/
|
||||
```bash
|
||||
# 1. Database snapshot. Runs pg_dump inside the db container (the app image has no
|
||||
# postgres client), reading credentials from the compose environment.
|
||||
mkdir -p ./backups
|
||||
docker compose exec -T db \
|
||||
sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' \
|
||||
| gzip > ./backups/db_$(date +%Y-%m-%d).sql.gz
|
||||
|
||||
# 2. Uploaded media (originals + derivatives) out of the named volume.
|
||||
# NOTE the mountpoint is /src, not /media: if the volume is ever empty, Docker
|
||||
# pre-populates a fresh mount from the image's own directory, and alpine ships a
|
||||
# /media containing cdrom/floppy/usb. Mounting somewhere the image has nothing
|
||||
# avoids silently tarring (and polluting the volume with) those.
|
||||
docker run --rm \
|
||||
-v eventsnap_media_data:/src:ro -v "$PWD/backups":/backup \
|
||||
alpine tar czf /backup/media_$(date +%Y-%m-%d).tar.gz -C /src .
|
||||
|
||||
# 3. Export archives — a SEPARATE volume (see the security note below).
|
||||
docker run --rm \
|
||||
-v eventsnap_exports_data:/src:ro -v "$PWD/backups":/backup \
|
||||
alpine tar czf /backup/exports_$(date +%Y-%m-%d).tar.gz -C /src .
|
||||
|
||||
# Weekly offsite sync of the three artefacts above.
|
||||
rsync -az ./backups/ user@storagebox.example.com:backup/eventsnap/
|
||||
```
|
||||
|
||||
The `/media` volume holds originals, previews, thumbnails, exports, and DB backups — a single path to back up.
|
||||
Volume names are prefixed with the compose project name — `eventsnap_` if you run
|
||||
from a directory called `eventsnap`. Confirm yours with `docker volume ls`.
|
||||
|
||||
> **Exports are deliberately NOT under `/media`.** They live on their own
|
||||
> `exports_data` volume (`EXPORT_PATH=/exports`) because a keepsake archive
|
||||
> contains every photo in the event; keeping it outside the media tree is what
|
||||
> stops it being reachable except through the ticket-gated download handler.
|
||||
> Backing up only the media volume therefore loses every generated keepsake.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user