deploy: pull prebuilt images instead of building on the event server
The production compose still carried `build:` keys and no `image:` keys, so a
`git clone` onto the CX22 followed by `docker compose up -d` would have started a
fat-LTO release build of 427 crates on a 2-vCPU/4 GB box — the outcome the whole
build-on-the-Mac decision exists to avoid, reached silently because `pull` skips a
service it is told to build rather than failing.
Both services now pull `registry.mc02.dev/eventsnap/*:${EVENTSNAP_VERSION}` with the
`:?` form, so a missing tag fails the command instead of resolving to an empty one.
`docker-compose.build.yml` restores the `build:` keys for the workstation that
produces the images, from the same context paths.
Also here:
- `DOMAIN` gets the same `:?` guard. Blank did not fail — it produced `https://` for
the frontend's ORIGIN and collapsed the Caddyfile's site block into a malformed
global block, so the stack came up with no TLS and no site.
- `stop_grace_period: 20s` on the app. Docker's default stop timeout is 10s, exactly
the app's own drain budget, so a redeploy could SIGKILL the process at the moment it
was finishing — truncating the in-flight upload the graceful shutdown protects.
- `COMMENTS_ENABLED` is pinned "false" alongside MEDIA_PATH. It is a product decision
for this event, and `.env.example` ships the generic `true`; pinning it means an
operator who copies the example and edits only the secrets cannot ship comments on.
- The frontend runtime stage now copies the lockfile and uses `npm ci`. Without it the
three `^`-ranged deps re-resolved at build time, so an image rebuilt days later could
differ from the one that was tested. Image also drops 120 MB -> 65 MB.
- `docker-compose.dev.yml` told the operator that production had the same `$`-eating
bug and to escape the hash as `$$` in `.env`. That is wrong and it breaks a working
deployment: Compose uses single-quoted env_file values literally, and doubling
produces a 74-character string `looks_bcrypt` rejects. Verified with `printenv`.
The runbook's rollback pointed at `v0.12.0`, which has 6 migrations against HEAD's 22
and was never built or pushed — running the emergency card's rollback line would have
crash-looped the app with `VersionMissing` during the event. §9 now has you tag one
build twice so the rollback target is bit-identical, and says plainly what that can and
cannot fix. Every `$DOMAIN` command gained the `set -a; . ./.env` it needs, the
down-migration psql commands are wrapped in `sh -c` so the container expands the
credentials rather than sending `-U ""`, and the advice to lower `max_video_size_mb`
is withdrawn: the client guard it was premised on does exist, but is pinned to a
compile-time constant, so lowering the DB value only moves failures later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
67
.env.example
67
.env.example
@@ -1,7 +1,19 @@
|
||||
# ── Domain ────────────────────────────────────────────────────────────────────
|
||||
# Public domain Caddy will serve and obtain a TLS certificate for.
|
||||
#
|
||||
# The DNS A record must already point at this server BEFORE the first `up -d`: Caddy
|
||||
# requests a certificate on boot, and Let's Encrypt allows only 5 failed validations per
|
||||
# hostname per hour. Never delete the caddy_data volume — it holds the certificate and
|
||||
# the ACME account key.
|
||||
DOMAIN=my-event.example.com
|
||||
|
||||
# ── Image version ─────────────────────────────────────────────────────────────
|
||||
# Tag pulled for the `app` and `frontend` services (docker-compose.yml). Production runs
|
||||
# prebuilt images from the registry and never compiles — see DEPLOYMENT_RUNBOOK.md.
|
||||
# Always an immutable tag, never `latest`: rollback is `EVENTSNAP_VERSION=<previous>`
|
||||
# + `docker compose up -d`, which works offline if that image is still resident locally.
|
||||
EVENTSNAP_VERSION=v0.13.0
|
||||
|
||||
# ── App server ────────────────────────────────────────────────────────────────
|
||||
APP_PORT=3000
|
||||
# Set to `production` in real deployments. This activates the secret guard that
|
||||
@@ -90,10 +102,53 @@ EXPORT_PATH=/exports
|
||||
# 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.
|
||||
# Number of parallel media compression workers. Default 2. Boot-time only.
|
||||
#
|
||||
# CORRECTION TO EARLIER GUIDANCE: this used to say "each worker can run an ffmpeg
|
||||
# transcode, so raise the app memory limit to ~2G if you set 4". There is NO video
|
||||
# transcode anywhere in this codebase — services/video.rs runs
|
||||
# `ffmpeg -ss <t> -i <src> -vframes 1 -vf scale=...`, a single poster frame, and video
|
||||
# originals are stored and served byte-for-byte. Poster extraction costs ~150-250 MB
|
||||
# for a moment; it is not the constraint.
|
||||
#
|
||||
# The real memory consumer is the IMAGE path. `image` 0.25's resize builds an Rgba32F
|
||||
# intermediate at 16 BYTES PER PIXEL, sized (source_width x target_height) — which the
|
||||
# 256 MiB decode guard in imaging.rs does NOT cover. Peak per photo, decode + the 2048px
|
||||
# display resize: ~145 MB at 12 MP, ~223 MB at 24 MP, ~354 MB at 48 MP.
|
||||
#
|
||||
# So on a 2 vCPU / 4 GB box (e.g. Hetzner CX22) KEEP THIS AT 2:
|
||||
# * concurrency 2, two 48 MP photos ≈ 800 MB against the 1G app limit — ~25% margin.
|
||||
# * concurrency 4, the same pair ≈ 1.5 GB — OOM.
|
||||
# * and app=2G + db=1G + frontend/caddy 256M each + ~370 MB of OS/Docker exceeds the
|
||||
# ~3910 MiB a "4 GB" VM actually reports. Raising the limit oversubscribes the host.
|
||||
# 4 is only reasonable on the 4 vCPU / 8 GB box README.md documents.
|
||||
#
|
||||
# Throughput at 2 is not the bottleneck anyone thinks it is: ~2.5s per 12 MP photo, so
|
||||
# 100 photos is ~250 CPU-seconds spread over an entire evening.
|
||||
COMPRESSION_WORKER_CONCURRENCY=2
|
||||
|
||||
# ── Comments ──────────────────────────────────────────────────────────────────
|
||||
# Master switch for the comment feature. Boot-time only (NOT in the admin UI), so it
|
||||
# needs a `docker compose up -d` to apply. Anything other than false/0/no/off is on.
|
||||
#
|
||||
# When false the backend rejects NEW comments with 403 and the frontend hides the whole
|
||||
# comment UI, including in the offline keepsake viewer. Likes and captions are entirely
|
||||
# separate features and are unaffected. Existing comments stay in the database (hidden),
|
||||
# so flipping it back restores them.
|
||||
#
|
||||
# Note it gates POSTING only: GET /upload/{id}/comments still serves already-existing
|
||||
# comments, and the keepsake's data.json still embeds their text. Irrelevant if the flag
|
||||
# is off from the first boot, since no comment can ever have been written.
|
||||
# NOTE for the current deployment: `docker-compose.yml` PINS this to "false" on the app
|
||||
# service, and `environment` overrides `env_file` — so changing it here has no effect in
|
||||
# production. Remove that line from the compose file first if you want comments back.
|
||||
COMMENTS_ENABLED=true
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||
# SET THIS IN PRODUCTION. Without it the app falls back to
|
||||
# `eventsnap_backend=debug,tower_http=debug` (see main.rs), and with TraceLayer that is a
|
||||
# debug line per HTTP request — including every preview and thumbnail fetch. Combined with
|
||||
# Docker's json-file driver it writes to the same filesystem as the database and the media.
|
||||
# docker-compose.yml caps each service's logs at 30 MB; this keeps the volume sane in the
|
||||
# first place. The e2e stack has always used exactly this value.
|
||||
RUST_LOG=eventsnap_backend=info,tower_http=warn
|
||||
|
||||
Reference in New Issue
Block a user