chore(deploy): single-container Dockerfile, Caddy, and Unraid template
Builds the container the "1 container" decision (D15) actually needs, which D15 itself deferred as follow-up work: Caddy + the FastAPI app + the static SvelteKit build in one image, SQLite on a mounted volume. See docs/DECISIONS.md D16 for the specific choices and why (entrypoint-run migrations instead of a separate deploy-pipeline step, tini + a small supervisor script instead of s6-overlay/supervisord, copying the Caddy binary out of its official image). Removes apps/api/Dockerfile and apps/web/Dockerfile from the old 4-container compose plan (PR #4, closed as superseded) — the root Dockerfile replaces both with one multi-stage build. deploy/unraid-template.xml turns VELODROME_PUBLIC_URL, VELODROME_SECRET_KEY, etc. into fillable Unraid Community Applications web UI fields, per the earlier decision to keep config there instead of a .env file. .gitea/workflows/release.yml builds and pushes the image to the Gitea registry on a version tag or manual dispatch; it does not touch the running container. Verified by actually running the built image, not just building it: the health endpoint responds through Caddy's proxy, the SPA serves with working client-route fallback, alembic ran and produced a real (non-empty) SQLite file under /data, the process runs as the non-root velodrome user, and killing the uvicorn process brings the whole container down (exit 143) rather than leaving Caddy serving alone — confirming the entrypoint's coupled-lifetime behavior actually holds, not just that it reads correctly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R2ZKeWkZV7ehf7fivrAkkG
This commit is contained in:
@@ -181,6 +181,58 @@ containment), #6 (single ingestion path) — none of those were ever Postgres-sp
|
||||
(D6, opaque bearer tokens) is unaffected. FastAPI/SQLAlchemy/Alembic stay exactly as chosen in D4;
|
||||
only the database engine underneath them changed.
|
||||
|
||||
### D16 — Single-container packaging: entrypoint migrations, tini + a two-line supervisor, Caddy binary copy, Unraid template
|
||||
|
||||
**Chosen:** one Docker image (root `Dockerfile`), built by copying the SvelteKit static build and
|
||||
the API's venv into a runtime stage alongside a copied-out `caddy` binary. `deploy/entrypoint.sh`
|
||||
runs `alembic upgrade head`, then starts uvicorn (loopback-only) and Caddy as two background
|
||||
processes under `tini` as PID 1, and kills+exits if either one dies. Config surfaces as env vars
|
||||
read by the existing `VELODROME_`-prefixed Pydantic settings; `deploy/unraid-template.xml` exposes
|
||||
the required ones as Unraid Community Applications web UI fields instead of a `.env` file.
|
||||
|
||||
**Why not a real process manager (s6-overlay, supervisord):** two long-running processes with no
|
||||
dependency graph between them (Caddy doesn't need to wait on uvicorn — it just proxies) doesn't
|
||||
need a supervisor with restart policies, readiness ordering, or log multiplexing. A ~20-line bash
|
||||
script under `tini` (for correct signal forwarding and zombie reaping, which a bare shell script as
|
||||
PID 1 doesn't do) gets the one property that matters — if either process dies, the whole container
|
||||
exits non-zero so Docker/Unraid restarts it — without a new dependency or a config format to learn.
|
||||
Revisit if a third long-running process gets added later; two is the reasonable ceiling for "just
|
||||
write the script."
|
||||
|
||||
**Why migrations run from the entrypoint, contradicting what apps/api/Dockerfile's own comment
|
||||
used to say** ("Migrations run as an explicit step before this in deploy.yml... never from the
|
||||
entrypoint, so a failed migration fails the deploy visibly instead of crash-looping here"): that
|
||||
comment described the 3-container Postgres plan, where a separate `run --rm api alembic upgrade
|
||||
head` step existed *before* `compose up -d`. A single container has nowhere else to put that step.
|
||||
The property it was protecting — a failed migration must be visible, not silently served — still
|
||||
holds: `set -e` means the script exits non-zero on migration failure, so the container never starts
|
||||
serving traffic and shows as exited/restarting in `docker ps`/Unraid, which is the same visibility
|
||||
by a different mechanism. What's genuinely lost is the *old* mechanism's failure mode of "the
|
||||
previous version keeps running while the bad migration is investigated" — a single container that
|
||||
fails to start migrations has no previous version still up. Acceptable for a single-instance
|
||||
home-lab deployment; would need reconsidering (e.g. a blue/green swap) if this ever needed
|
||||
zero-downtime deploys.
|
||||
|
||||
**Why the Caddy binary is copied from `caddy:2` rather than using a Caddy base image:** the runtime
|
||||
needs both Python (for uvicorn) and Caddy; picking either official base image as the starting
|
||||
point means installing the other stack into it by hand. Caddy's official images are a single
|
||||
statically-linked Go binary with no CGO, so `COPY --from=caddy:2 /usr/bin/caddy /usr/bin/caddy`
|
||||
into a `python:3.12-slim` base is the documented, standard way to get both without a second
|
||||
package manager or a source build.
|
||||
|
||||
**Why an Unraid template file, not just documentation:** the earlier decision (in-session) was to
|
||||
move configuration out of a `.env` file and into fields the Unraid web UI can fill in — a plain env
|
||||
var table in a README doesn't do that by itself, since Unraid still needs a `Config`-tagged XML
|
||||
entry per field to render one. `deploy/unraid-template.xml` is that; every field stays hand-editable
|
||||
in the UI afterward regardless of what the template pre-fills, so getting a default slightly wrong
|
||||
here isn't load-bearing.
|
||||
|
||||
**What this doesn't do:** `.gitea/workflows/release.yml` builds and pushes the image to the Gitea
|
||||
registry; it does not SSH into the Unraid host and recreate the running container. Rolling a new
|
||||
image out is a manual/Unraid-side action (pull + Apply, or Unraid's own update check), not
|
||||
something CI does unattended — consistent with treating "affects a shared, already-running system"
|
||||
as something a human triggers, not automation.
|
||||
|
||||
---
|
||||
|
||||
## Deliberately deferred
|
||||
|
||||
Reference in New Issue
Block a user