Files
ResumeSite/docs/spec/DOCKER_SPEC.md
T
cesnimda bfa8cfd357 feat: homelab case study from live infra; Traefik-based deploy
- rewrote homelab (content + topology diagram + stack) from a live inspection of the host:
  Ubuntu 24.04, ~30 Docker services behind Traefik (Cloudflare-fronted, TLS, HTTP/3),
  Authentik SSO forward-auth, CrowdSec, Pi-hole, self-hosted Gitea + CI runner, socket-proxy,
  per-app network isolation; WordPress framed as being decommissioned (not future arch)
- deploy: docker-compose now uses Traefik labels + traefik_proxy network (was assumed nginx
  edge); .env.example adds SITE_HOST/TRAEFIK_ENTRYPOINT; colophon + ARCHITECTURE/DOCKER/
  DEPLOYMENT specs corrected nginx-edge -> Traefik (site container still serves via nginx)
- PROJECT_STATUS: pre-launch checklist updated; infra section added

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:23:09 +02:00

4.4 KiB

DOCKER_SPEC.md

Container strategy. Descriptive spec — actual Dockerfiles/compose files are Phase 3 artifacts.


1. Images

site (production)

  • Multi-stage build: stage 1 node:22-alpine + pnpm → install (frozen lockfile) → astro builddist/; stage 2 nginx:alpine (digest-pinned) copying dist/ + a site-local nginx config.
  • Site-local nginx config responsibilities: gzip/brotli precompressed asset serving, cache headers (immutable hashed assets / no-cache HTML per ARCHITECTURE §5), security headers + CSP (ARCHITECTURE §6), trailing-slash 301s, /Linkedin 301, legacy-WP 410 map, custom 404 page wiring.
  • Runs as non-root (nginx unprivileged image), read-only filesystem, no writable volumes, port 8080 internal.
  • Image contains zero secrets and zero build tooling — final image is nginx + static files, ~25 MB.

contact-relay (production)

  • Multi-stage: .NET 9 SDK build → mcr aspnet runtime-deps/alpine final; non-root, read-only FS, listens 8081 internal.
  • Config via environment only: Smtp__Host/Port/User/Password (secret), Relay__ToAddress, Relay__RateLimit*, Relay__AllowedOrigin (site origin for CORS/Origin check).
  • Healthcheck endpoint /healthz (no auth, no info leakage) wired to compose healthcheck.

site-dev (development)

  • node:22 dev container running astro dev with HMR; site/ bind-mounted, node_modules in a named volume (Windows-host performance), port 4321 published.
  • Relay runs alongside via dotnet watch in an SDK-image dev container (or directly on host — both supported); Astro dev proxy forwards /api/contact → relay so the form works identically in dev.
  • Purpose: parity with prod behaviour (headers/proxying approximated) while keeping the fast inner loop; contributors need only Docker, not local Node/.NET installs.

2. Compose topology

deploy/
  docker-compose.yml            # prod: site + contact-relay, internal network,
                                #   exposed only to the host reverse-proxy network
  docker-compose.dev.yml        # dev: site-dev + relay-dev
  .env.example                  # documented variables, no real values
  • Prod compose joins the server's existing Traefik network (traefik_proxy); the host Traefik (existing, TLS-terminating, Cloudflare-fronted) discovers the containers via labels and routes cesnimda.co.uksite:8080 and cesnimda.co.uk/api/contactrelay:8081 (the relay's PathPrefix rule outranks the site's Host rule). The stack publishes no host ports itself.
  • restart: unless-stopped, log rotation via compose logging options, images referenced by registry tag + digest.
  • Optional third service slot (analytics, self-hosted Umami/Plausible) reserved in the compose file as a commented profile — decision deferred.

3. Environment variables

Var Service Notes
SMTP_HOST/PORT/USER relay His mail provider's submission endpoint
SMTP_PASSWORD relay Secret — provided via env file with 600 perms on server (or Docker secret); never in git, never in image
RELAY_TO relay Destination inbox
RELAY_ALLOWED_ORIGIN relay https://cesnimda.co.uk
RELAY_RATE_* relay Requests/window per IP
PUBLIC_SITE_URL site (build-time) Canonical origin for sitemap/OG absolute URLs

Build-time vs runtime separation is explicit: the static site consumes variables only at build; the relay only at runtime. .env.example documents every variable (pattern Connor already uses in both projects).

4. Build pipeline approach (interface to DEPLOYMENT_SPEC)

  • CI builds both images on Gitea Actions runners, tags :<git-sha> + :latest, pushes to Gitea's built-in container registry (git.cesnimda.uk — keeps the whole chain self-hosted).
  • The site image build runs the full quality gate first (typecheck, unit, Playwright against a preview build, Lighthouse budgets) — an image only exists if gates passed.
  • Reproducibility: pnpm frozen lockfile, pinned base images, no network fetches during astro build beyond npm (fonts vendored in repo).

5. Local ops conventions

  • make-style task runner or npm scripts wrapping compose invocations (dev, build, test, deploy) so every workflow is one documented command — README-driven, same convention as JobTrack/InboxIntel repos.
  • Backups: nothing stateful to back up (site is git + registry; relay is stateless) — the deliberate architectural win; documented so nobody adds state casually.