* memswap_limit is now IN docker-compose.yml on all four services. Compose
sets Memory but leaves MemorySwap unset, and Docker then permits swap equal
to the memory limit — so following §5's "add 2 GB of swap" silently DOUBLED
every ceiling, to ~5 GiB on a 3.82 GiB box. Nothing OOMs; instead Postgres's
working set becomes swap-eligible on a shared-tenancy SSD, turning a bounded
OOM-kill that restarts in seconds into unbounded latency with no signal but
"everything is slow". The runbook told the operator to hand-add it, which
also broke §0's own gate that docker-compose.yml must be unmodified.
Verified rather than assumed: service-level memswap_limit does compose with
deploy.resources.limits.memory (docker inspect → Memory=1073741824
MemorySwap=1207959552).
* DATABASE_MAX_CONNECTIONS pinned in compose. It is the one env var that is
now boot-FATAL when unparseable — the right call, but it means a stray quote
or a trailing inline comment in .env crash-loops the app behind a live
Caddy. MEDIA_PATH, EXPORT_PATH and APP_PORT are pinned for weaker reasons.
* .env.example's quota narrative was sized for a CX33: "~30 GB of a fresh
70 GB" on a box with 40 GB. And on THIS box the fixed point never binds at
all — ~210 MB/guest is below the 500 MiB floor, so everyone gets the floor
and the per-user quota stops bounding aggregate growth. What actually stops
uploads is the keepsake preflight at ~8 GB of media. That paragraph is what
an operator reads when a guest is blocked, and it pointed at the wrong knob.
* The emergency card gains the one disk symptom that can appear mid-event,
where `df -h` — its only disk instruction — actively misleads: the gate
fires ~10 GB + 2.2x media BEFORE the disk is full, so df shows ~20 GB free
at the moment uploads are being refused.
* Two code comments that now assert the opposite of the code: claim_job
promised that "the update_progress liveness check bails such a worker out
early" — it cannot, its predicate is on the job row, which a reopen does not
touch, so a mid-export reopen grinds the whole gallery to completion on a
2-vCPU box during the live event. And prune_superseded_archives still argued
"deleted bytes cannot be rolled back" as an invariant, after the reclaim
path was changed to prune even when that will not close the shortfall.
Both now describe what the code does.
* Smaller corrections: runbook §3's "two 48 MP photos ≈ 800 MB" scenario is
unreachable (compression.rs takes an exclusive heavy permit, so they
serialise) and contradicted .env.example; "all four healthy" is wrong since
caddy has no healthcheck; a README line reference pointed at a comment added
by the same commit that broke it.