second-brain/deploy/web/Dockerfile
Travis Herbranson 597c83649c deploy/web: containerized FastAPI UI on the homelab docker network
Dockerfile, compose.yml, env template, and runbook for the second-brain
web container. Targets herbys-dev (10.0.21.207); Traefik (file-provider
on 10.0.11.20) reaches the published host port at 10.0.21.207:8080.

Image:
- python:3.12-slim + the official uv binary copied from the upstream
  image, plus apt-installed git + ca-certificates so the Gitea VCS pin
  for embedding-chunking resolves at build time.
- uv sync --frozen --no-dev --no-install-project, source copy, then a
  second uv sync to install the project itself. Two-step so the lock
  install layer caches independently of source edits.
- No ffmpeg / claude CLI / faster-whisper — web role doesn't need any
  of them. Extraction runs on the dev host's CLI; transcription on the
  tower.
- Drops to uid 1000 (`app`) before CMD. Uvicorn binds 0.0.0.0:8000
  inside the container, with --proxy-headers + --forwarded-allow-ips=*
  so Traefik's X-Forwarded-* survive.

compose.yml:
- Joins the existing external `homelab` bridge network so the container
  reaches homelab-postgres:5432 and ollama:11434 by service DNS.
- Publishes the uvicorn port at 10.0.21.207:8080 (LAN IP bound, not
  0.0.0.0) for Traefik on the separate VM to reach. NO traefik.* labels
  — file-provider Traefik can't read them.
- env_file: .env (0600, gitignored) — SECOND_BRAIN_DATABASE_URL points
  at homelab-postgres:5432 (containerised), NOT the host's 127.0.0.1:5433
  port-map.
- restart: unless-stopped.

README.md:
- Build + bring-up commands.
- SECURITY note: no SSO / no CSRF / mutating endpoints — Traefik route
  must be LAN-only for now (Travis's call).
- The two infra steps Travis applies himself, with ready-to-paste
  snippets:
  - Knot DNS: brain.herbylab.dev → 10.0.11.20.
  - Traefik dynamic config: file-provider router + service block.
- Verification checklist for both before-and-after-DNS states.

Live-verified on herbys-dev: container Up, dashboard returns 200 with
real status counts from petalbrain, settings save round-trip works,
no tracebacks in logs.
2026-05-25 11:48:37 -04:00

64 lines
2.3 KiB
Docker

# second-brain web container — review/dashboard UI only.
#
# Slim by design: this image runs `uvicorn` against second_brain.web.app.
# It does NOT need ffmpeg, the claude CLI, faster-whisper, or any GPU
# tooling. Extraction runs on the dev host's CLI; transcription runs on
# the tower. The web role is just "FastAPI + psycopg + Jinja templates".
FROM python:3.12-slim AS base
ENV PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/app/.venv
# uv is fetched as a static binary from its official image so we don't
# pull a full python stack to install it.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
# `git` is needed at *build* time because pyproject.toml pins
# embedding-chunking to a Gitea VCS source. `ca-certificates` so the
# https handshake validates. Slim image to clean up apt afterwards.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Project metadata first so the layer cache survives source edits.
COPY pyproject.toml uv.lock ./
# `--frozen` enforces lockfile parity. `--no-dev` excludes pytest/ruff.
# `--no-install-project` so we don't try to install the (not-yet-copied)
# source tree in this layer.
RUN uv sync --frozen --no-dev --no-install-project
# Source + runtime config. config/settings.toml.example is committed; a
# real settings.toml gets bind-mounted in by compose at runtime.
COPY src/ ./src/
COPY config/ ./config/
COPY alembic/ ./alembic/
COPY alembic.ini ./alembic.ini
# Now install the project itself into the venv (puts the `second-brain`
# console script + the second_brain package on PATH).
RUN uv sync --frozen --no-dev
# Drop privileges. The image doesn't write to the host bind-mounts; the
# settings.toml mount is read-only.
RUN useradd --uid 1000 --create-home --shell /sbin/nologin app \
&& chown -R app:app /app
USER app
EXPOSE 8000
# Bind to all interfaces inside the container — the host port publish
# decides who can reach it from outside. `--proxy-headers` lets Traefik
# (off-box on 10.0.11.20) forward X-Forwarded-* correctly.
CMD ["/app/.venv/bin/uvicorn", \
"second_brain.web.app:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--proxy-headers", \
"--forwarded-allow-ips=*"]