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:
+73
@@ -0,0 +1,73 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# Single-container image for Velodrome (docs/DECISIONS.md D15/D16): Caddy serves the SvelteKit
|
||||
# static SPA and reverse-proxies /api/* to the FastAPI app, both running in the same container
|
||||
# behind whatever TLS-terminating reverse proxy already exists on the host. Build context is the
|
||||
# repo root, since this needs both apps/api and apps/web:
|
||||
#
|
||||
# docker build -t velodrome .
|
||||
#
|
||||
# Replaces apps/api/Dockerfile and apps/web/Dockerfile from the old 4-container compose plan
|
||||
# (PR #4, closed as superseded) — those built two images meant to run as separate services;
|
||||
# this builds one.
|
||||
|
||||
# ---- web: produces /app/build, nothing from this stage ends up running ----
|
||||
FROM node:22-slim AS web-builder
|
||||
WORKDIR /app
|
||||
COPY apps/web/package.json apps/web/pnpm-lock.yaml ./
|
||||
RUN corepack enable && corepack prepare pnpm@9 --activate \
|
||||
&& pnpm install --frozen-lockfile
|
||||
COPY apps/web .
|
||||
RUN pnpm run build
|
||||
|
||||
# ---- api: produces the venv at /app/.venv ----
|
||||
FROM python:3.12-slim AS api-builder
|
||||
RUN pip install --no-cache-dir uv
|
||||
WORKDIR /app
|
||||
COPY apps/api/pyproject.toml apps/api/uv.lock ./
|
||||
# Dependencies first, isolated from source changes, so touching velodrome/ doesn't invalidate
|
||||
# this layer.
|
||||
RUN uv sync --frozen --no-install-project --no-dev
|
||||
COPY apps/api/velodrome ./velodrome
|
||||
COPY apps/api/alembic ./alembic
|
||||
COPY apps/api/alembic.ini ./
|
||||
RUN uv sync --frozen --no-dev
|
||||
|
||||
# ---- runtime ----
|
||||
FROM python:3.12-slim AS runtime
|
||||
|
||||
# Caddy's official images ship a single statically-linked Go binary (no CGO) — copying it out of
|
||||
# the upstream image is the standard way to get Caddy into a non-Caddy base image without a
|
||||
# second package manager or a source build.
|
||||
COPY --from=caddy:2 /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
# tini is PID 1: reaps zombies and forwards signals correctly to entrypoint.sh's two background
|
||||
# processes, which a bare `CMD` running a shell script as PID 1 would not do on its own.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd --system velodrome && useradd --system --gid velodrome --create-home velodrome
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=api-builder --chown=velodrome:velodrome /app /app
|
||||
COPY --from=web-builder --chown=velodrome:velodrome /app/build /srv/web
|
||||
COPY --chown=velodrome:velodrome deploy/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY --chown=velodrome:velodrome deploy/entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
# Absolute path into the mounted volume below. See deploy/README.md for the full env var table —
|
||||
# this is the one variable NOT meant to be overridden per-deployment, since /data is the contract
|
||||
# with the volume mount, not a per-instance setting.
|
||||
ENV VELODROME_DATABASE_URL="sqlite+aiosqlite:////data/velodrome.db"
|
||||
|
||||
RUN mkdir -p /data && chown velodrome:velodrome /data
|
||||
|
||||
USER velodrome
|
||||
VOLUME ["/data"]
|
||||
# Non-privileged port: Caddy needs no root/setcap here, and TLS termination is the host reverse
|
||||
# proxy's job (docs/PLAN.md "Service topology"), not this container's.
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["/app/entrypoint.sh"]
|
||||
Reference in New Issue
Block a user