platform/platform-infra/stacks/traefik/docker-compose.yml
Bart Van Geyt 41a01c60ef Phase 2: platform service stacks + CLAUDE.md
Add base Docker Compose projects under platform-infra/stacks, using the
Phase 1 ZFS platform datasets and shared networks:

- bootstrap-networks.sh: idempotent creation of shared `proxy` (edge) and
  `platform` (internal) Docker networks.
- traefik: edge router with TLS/ACME (HTTP-01), global HTTP->HTTPS redirect,
  file-based security-headers/TLS middlewares, and a basic-auth dashboard.
  Docker access via a least-privilege tecnativa/docker-socket-proxy instead
  of mounting docker.sock directly.
- mariadb: shared instance on tank/platform/mariadb, utf8mb4, tuned; not
  published to the host (internal `platform` network only).
- forgejo: Git + container registry over HTTPS (SSH disabled to avoid extra
  inbound ports), SQLite backend, data on tank/platform/forgejo.

Each stack ships a committed .env.example (real .env is git-ignored) and the
stacks README documents bring-up order, secrets, and verification.

Also add root CLAUDE.md so fresh sessions orient from files cheaply:
philosophy, doc pointers, naming quick-ref, repo map, commands, agreements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:58:18 +02:00

88 lines
3 KiB
YAML

# Traefik edge router. Static config is passed as flags (so .env can drive
# ACME email + dashboard host); dynamic config (middlewares, TLS options) is
# loaded from ./dynamic. Docker access goes through a least-privilege socket-proxy.
services:
socketproxy:
image: tecnativa/docker-socket-proxy:0.3.0
container_name: socketproxy
restart: unless-stopped
environment:
# Grant only what Traefik needs to discover routes; everything else denied.
CONTAINERS: 1
NETWORKS: 1
SERVICES: 1
TASKS: 1
EVENTS: 1
PING: 1
VERSION: 1
# Explicitly deny the dangerous surfaces.
POST: 0
EXEC: 0
IMAGES: 0
VOLUMES: 0
INFO: 0
AUTH: 0
SECRETS: 0
SWARM: 0
SYSTEM: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- socketproxy
traefik:
image: traefik:v3.3
container_name: traefik
restart: unless-stopped
depends_on:
- socketproxy
command:
- "--global.checknewversion=false"
- "--global.sendanonymoususage=false"
- "--log.level=INFO"
- "--accesslog=true"
# Entry points + global HTTP->HTTPS redirect
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
# Providers: Docker (via socket-proxy) + file (dynamic dir)
- "--providers.docker=true"
- "--providers.docker.endpoint=tcp://socketproxy:2375"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"
# Let's Encrypt (HTTP-01 challenge on the web entrypoint)
- "--certificatesresolvers.le.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.le.acme.storage=/acme/acme.json"
- "--certificatesresolvers.le.acme.httpchallenge=true"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
# Dashboard/API (secured via labels below)
- "--api.dashboard=true"
# Prometheus metrics (scraped in Phase 6)
- "--metrics.prometheus=true"
ports:
- "80:80"
- "443:443"
volumes:
- /tank/platform/traefik/acme:/acme
- ./dynamic:/etc/traefik/dynamic:ro
networks:
- proxy
- socketproxy
labels:
- "traefik.enable=true"
# Dashboard router (HTTPS + basic auth)
- "traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=le"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dashboard-auth@docker,security-headers@file"
- "traefik.http.middlewares.dashboard-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
networks:
proxy:
external: true
socketproxy:
internal: true