Four things, all found by the same question: what does an operator standing at the
venue actually need?
A RESTORE PROCEDURE. There was none anywhere, and a backup you have never restored
isn't a backup. Two hazards worth writing down: media must be extracted preserving
ownership (the app runs as uid 100 / gid 101, and a root-owned restore makes every
upload fail with EACCES surfacing as a generic 500), and the app must be STOPPED
first, because migrations run on boot and a live pool will fight the restore.
Both the backup and the restore commands were run against the real stack before being
written down, which caught two that would have failed:
- The plain `pg_dump` did not restore: `psql` aborted on `ERROR: schema
"_sqlx_test" already exists`. pg_dump emits no DROPs without --clean --if-exists,
so the documented dump could only ever be restored into an empty database. Fixed
at the source (the dump is now self-cleaning) and verified end to end: 16 tables
back, exit 0.
- `--same-owner` does not exist in BusyBox tar, which is what `alpine` ships, so
the extract aborted before unpacking anything. `--numeric-owner` plus the
explicit chown, verified to land 100:101.
BACKUP CADENCE. "Weekly offsite" is the wrong shape when every irreplaceable byte is
created in one eight-hour window and nobody can retake a wedding. The backup that
matters runs that night, and again after the release so the keepsake is captured.
Also: take the DB dump and the media tarball back to back, or you get rows pointing
at files the dump doesn't know about.
quota_tolerance WAS DOCUMENTED AS SOMETHING IT ISN'T. .env.example called it "fraction
of disk that triggers the low-storage warning". It is the multiplier in
`floor(free_disk * tolerance / active_uploaders)` -- so an operator who wants "warn me
later" and sets 0.95 is actually authorising guests to fill 95% of the disk, moving
the fixed point from 43% to ~49% and eating the export headroom. The admin UI labelled
it "Toleranz (0-1)" with no explanation at all, which invites exactly that reading;
it is now "Speicher-Anteil für Gäste" with the formula in the hint. Wrong docs on a
tuning knob are worse than no docs.
SIZING. New section with the arithmetic: three volumes on one filesystem, the quota
fixed point at tolerance/(1+tolerance), and the fact the 80 GB baseline does not cover
the keepsake -- both archives are built concurrently and each is roughly a second copy
of every original. Provision ~3x expected media, or give exports its own volume.
Also ticks the low-disk alert off the roadmap, since it now exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
5.4 KiB
Plaintext
86 lines
5.4 KiB
Plaintext
# ── Domain ────────────────────────────────────────────────────────────────────
|
|
# Public domain Caddy will serve and obtain a TLS certificate for.
|
|
DOMAIN=my-event.example.com
|
|
|
|
# ── App server ────────────────────────────────────────────────────────────────
|
|
APP_PORT=3000
|
|
# Set to `production` in real deployments. This activates the secret guard that
|
|
# refuses to boot with placeholder JWT_SECRET / ADMIN_PASSWORD_HASH values.
|
|
# (docker-compose.yml already sets APP_ENV=production for the app service.)
|
|
APP_ENV=production
|
|
|
|
# ── Database ──────────────────────────────────────────────────────────────────
|
|
# Set a strong password and keep it in sync between DATABASE_URL and
|
|
# POSTGRES_PASSWORD. Generate one with: openssl rand -hex 24
|
|
DATABASE_URL=postgres://eventsnap:CHANGE_ME_use_a_strong_password@db:5432/eventsnap
|
|
POSTGRES_USER=eventsnap
|
|
POSTGRES_PASSWORD=CHANGE_ME_use_a_strong_password
|
|
POSTGRES_DB=eventsnap
|
|
# Connection pool size. Default 10. For a busy event (~100 guests polling the feed
|
|
# + SSE + uploads at once) raise to ~30 so requests don't queue on a pool permit.
|
|
DATABASE_MAX_CONNECTIONS=30
|
|
|
|
# ── Authentication ────────────────────────────────────────────────────────────
|
|
# Generate with: openssl rand -hex 64
|
|
JWT_SECRET=change_me_to_a_random_64_byte_hex_string
|
|
SESSION_EXPIRY_DAYS=30
|
|
|
|
# Admin dashboard password (bcrypt hash).
|
|
# Generate with: htpasswd -bnBC 12 "" yourpassword | tr -d ':\n'
|
|
# IMPORTANT: keep the SINGLE QUOTES. A bcrypt hash is full of `$` (e.g. $2b$12$…$…),
|
|
# and both Docker Compose's env_file interpolation and dotenvy's variable substitution
|
|
# would otherwise eat the `$…` segments (reading them as unset vars) and corrupt the
|
|
# hash — every admin login then 401s. Single quotes make both read it literally.
|
|
ADMIN_PASSWORD_HASH='$2y$12$placeholder_replace_me'
|
|
|
|
# ── Event ─────────────────────────────────────────────────────────────────────
|
|
EVENT_NAME=Max & Maria's Wedding
|
|
EVENT_SLUG=max-maria-2026
|
|
|
|
# ── Storage ───────────────────────────────────────────────────────────────────
|
|
MEDIA_PATH=/media
|
|
# Export archives (Gallery.zip / Memories.zip). MUST be outside MEDIA_PATH —
|
|
# /media is publicly served, so exports here would be downloadable without auth.
|
|
EXPORT_PATH=/exports
|
|
|
|
# ── Runtime settings (upload limits, rate limits, capacity) ───────────────────
|
|
# NOTE: These are NOT environment variables. Upload size caps, rate limits, guest
|
|
# count and quota tolerance are stored in the database `config` table (seeded once
|
|
# at first boot) and changed at runtime from the ADMIN DASHBOARD — the backend does
|
|
# not read them from .env. Setting them here has no effect. Current seeded defaults:
|
|
# upload rate 100 / hour / guest (raised from 10 by migration 015)
|
|
# feed rate 60 / minute
|
|
# export rate 3 / day
|
|
# max image size 20 MB
|
|
# max video size 500 MB
|
|
# estimated guests 100
|
|
# quota tolerance 0.75 (see below — NOT a warning threshold)
|
|
# Adjust these in the admin UI before the event if needed.
|
|
#
|
|
# quota_tolerance is the MULTIPLIER IN THE PER-USER QUOTA FORMULA, not the point at
|
|
# which anything warns you:
|
|
#
|
|
# per_user_limit = floor(free_disk * quota_tolerance / active_uploaders)
|
|
#
|
|
# It is recomputed against LIVE free space on every upload, so it self-throttles: guests
|
|
# converge on a fixed point at tolerance/(1+tolerance) of the free space you started
|
|
# with — 43% at 0.75, i.e. ~30 GB of a fresh 70 GB.
|
|
#
|
|
# Raising it therefore AUTHORISES GUESTS TO FILL MORE OF THE DISK. Setting 0.95 in the
|
|
# belief that it means "warn me later" moves the fixed point to ~49% and eats the
|
|
# headroom the keepsake needs — and the keepsake needs a lot, because Gallery.zip and
|
|
# Memories.zip are each roughly a second copy of every original (both store media
|
|
# uncompressed). Budget for media + 2x media, or move exports to their own volume.
|
|
#
|
|
# 0.75 is the tested default. Lower it if the box is tight; raise it only if you have
|
|
# provisioned export headroom separately.
|
|
|
|
# ── Workers ───────────────────────────────────────────────────────────────────
|
|
# Number of parallel image/video compression workers. Default 2. This is the main
|
|
# throughput bottleneck: with 2 workers a burst of uploads can take ~5s to appear.
|
|
# For a large event (100+ guests) 4 is a good target — but each worker can run an
|
|
# ffmpeg transcode, so if you raise this ALSO raise the app container's memory limit
|
|
# in docker-compose.yml (`app.deploy.resources.limits.memory`) from 1G to ~2G, or a
|
|
# burst of large videos can OOM the box and take Postgres down with it.
|
|
COMPRESSION_WORKER_CONCURRENCY=2
|