second-brain/deploy/web
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
..
.env.example deploy/web: containerized FastAPI UI on the homelab docker network 2026-05-25 11:48:37 -04:00
.gitignore deploy/web: containerized FastAPI UI on the homelab docker network 2026-05-25 11:48:37 -04:00
compose.yml deploy/web: containerized FastAPI UI on the homelab docker network 2026-05-25 11:48:37 -04:00
Dockerfile deploy/web: containerized FastAPI UI on the homelab docker network 2026-05-25 11:48:37 -04:00
README.md deploy/web: containerized FastAPI UI on the homelab docker network 2026-05-25 11:48:37 -04:00

second-brain web app — deploy notes

Target host: herbys-dev (10.0.21.207). Runs the FastAPI + Jinja UI under docker compose, on the existing homelab bridge network so it can reach homelab-postgres:5432 and ollama:11434 by service DNS.

Traefik (file-provider VM at 10.0.11.20) handles TLS + the public hostname brain.herbylab.dev. The compose file does NOT carry traefik.* labels — file-provider Traefik can't see them.


⚠ Security note (no SSO)

Travis opted not to put this behind Authentik for the initial roll-out. The web app has no authentication, no CSRF, no rate limiting, and it exposes mutating endpoints (accept/reject sources, edit pipeline settings).

→ The Traefik route should be reachable from LAN / trusted networks only. If you ever publish this past Cloudflare for off-LAN access, gate it behind the Authentik forward-auth middleware first, or add an auth_required decorator to the FastAPI app.


1. One-time setup on herbys-dev

cd /opt/projects/homelab/second-brain/deploy/web

# Real env file — paste the lovebug password.
cp .env.example .env
chmod 0600 .env
$EDITOR .env
# SECOND_BRAIN_DATABASE_URL must use host=homelab-postgres (NOT 127.0.0.1).
# Password lives at /opt/backups/postgres-consolidation/credentials.env.

# Build + start. Compose will join the existing external `homelab` net.
docker compose up -d --build

# Confirm it's healthy.
docker compose ps
docker compose logs --tail 50
curl -fsS http://10.0.21.207:8080/dashboard | head

Day-to-day:

docker compose logs -f                        # tail
docker compose restart                        # bounce (config reload)
docker compose pull && docker compose up -d   # if image were registry-hosted
git pull && docker compose up -d --build      # build-on-host update path

2. INFRA STEPS — Travis applies these manually

The compose stack is up once docker compose up -d --build returns. The public URL at https://brain.herbylab.dev only resolves after the two steps below land on their respective infra. They live on machines outside this repo's authority (Knot on the DNS box; Traefik on 10.0.11.20), so this README documents them rather than executing them.

2a. Knot DNS — A record for brain.herbylab.dev

Add this to the herbylab.dev zone file on the Knot host (file path / include layout matches the existing per-service A records — pick the spot used for the other *.herbylab.dev services):

brain.herbylab.dev.   300   IN   A   10.0.11.20

300s TTL matches the existing convention. Reload Knot:

sudo knotc reload
dig +short brain.herbylab.dev @<knot-host>     # should return 10.0.11.20

2b. Traefik file-provider router on 10.0.11.20

Add the snippet below to the Traefik dynamic config (same file the rest of the *.herbylab.dev services live in — file-provider config picks up changes without a restart):

http:
  routers:
    second-brain:
      rule: "Host(`brain.herbylab.dev`)"
      entryPoints:
        - websecure
      tls:
        certResolver: cloudflare
      service: second-brain
      # No middleware on day one. If/when SSO lands, prepend the
      # authentik forward-auth chain here (see other routers for the
      # shape) and remove the LAN restriction.

  services:
    second-brain:
      loadBalancer:
        servers:
          - url: "http://10.0.21.207:8080"
        passHostHeader: true

Verify on the Traefik host:

# Watch for config-reload events:
docker logs traefik --tail 20 -f
# Or hit the API if it's exposed:
curl -fsS http://localhost:8080/api/http/routers/second-brain@file

Once both 2a and 2b are in place, https://brain.herbylab.dev/dashboard should resolve through Cloudflare → Traefik → 10.0.21.207:8080 → the second-brain-web container.


3. Verification checklist

After docker compose up -d --build:

Check Command Expect
Container up docker compose ps second-brain-web is Up, not restarting
Startup log clean docker compose logs --tail 50 uvicorn Started server process + Application startup complete, no tracebacks
Local-host reachable curl -fsS http://10.0.21.207:8080/dashboard | head -1 <!DOCTYPE html> or similar
DB reachable curl -fsS http://10.0.21.207:8080/dashboard dashboard renders status counts (proves the homelab-net path to Postgres)
Settings round-trip open /settings in a browser, save green "Saved." flash + new updated_at

After Travis's 2a + 2b infra steps:

Check Command Expect
DNS dig +short brain.herbylab.dev 10.0.11.20
TLS curl -fsS https://brain.herbylab.dev/dashboard | head -1 dashboard HTML
Cert openssl s_client -connect brain.herbylab.dev:443 -servername brain.herbylab.dev <<<'' Cloudflare-issued cert

4. Follow-ups (not built this round)

  • SSO via Authentik. Add the existing forward-auth middleware to the Traefik router once we want this off-LAN. Authentik lives at 10.0.21.207:9000 (same host as this container — different port).
  • CSRF. Settings save is an HTMX POST with no token; trivial to forge from a same-origin browser. Acceptable while LAN-restricted.
  • Image registry. Currently build-on-host. Push to Gitea's container registry (gitea.plantbasedsoutherner.com/petal-power/second-brain-web) when there's a second box that needs to pull the image.
  • Dev-side extraction systemd timer. Travis is running extraction manually for now; the timer/cron path is deferred.