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>
88 lines
3 KiB
YAML
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
|