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
74 lines
3.0 KiB
Docker
74 lines
3.0 KiB
Docker
# 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"]
|