7.3 KiB
Traefik — Reverse Proxy
Traefik replaced Nginx Proxy Manager as the homelab's reverse proxy, giving every service a <service>.home.server hostname with locally-trusted HTTPS — now configured entirely as code instead of a web UI.
See the Traefik README for the full file-by-file breakdown and how to add new services.
Why the migration
Two reasons, both rooted in the same observation: NPM works, but it doesn't scale with the rest of the infrastructure-as-code approach.
Everything as code. NPM's configuration lives entirely in its web UI — proxy hosts, SSL certs, custom locations, access rules. None of it is version-controlled or reproducible. If the data volume is lost, you're rebuilding every proxy host manually. Traefik's routes, middlewares, and TLS config are all YAML files in this repo — diffable, reviewable, reproducible with docker compose up -d.
Docker daemon auto-discovery. NPM requires manually adding each proxy host through its UI — domain, target IP/port, SSL cert, save. Every new service means another round-trip through a different tool. Traefik watches the Docker daemon directly and picks up containers with traefik.enable=true automatically. Adding a new service to the reverse proxy is a two-line label addition to that service's compose file.
Architecture
┌─────────────────────────────────────────────────────┐
│ Traefik v3.7 │
│ │
│ Entrypoints: :80 (HTTP) → redirect to HTTPS │
│ :443 (HTTPS) → route to service │
│ │
│ Providers: │
│ Docker daemon → auto-discovers labeled conns │
│ File provider → watches dynamic/ for YAML │
│ │
│ TLS: mkcert local CA, single cert for all hosts │
└─────────────────────────────────────────────────────┘
│ │ │
┌────┴────┐ ┌─────┴─────┐ ┌────┴────┐
│ Docker │ │ File │ │ certs/ │
│ labels │ │ dynamic/ │ │ mkcert │
│ per-svc │ │ routes │ │ CA │
└─────────┘ └───────────┘ └─────────┘
Static config
Passed as command-line flags in compose.yaml:
- Docker provider — watches for labeled containers on
web_net - File provider — hot-reloads
dynamic/directory - Entrypoints —
:80and:443 - Ping — health check endpoint
- API dashboard — enabled, secured via its own HTTPS route
Dynamic config (dynamic/)
| File | Purpose |
|---|---|
tls.yaml |
Default TLS cert store pointing at the mkcert certificate |
middlewares.yaml |
redirect-to-https (301) + security-headers (HSTS, XSS, nosniff, SAMEORIGIN) |
pihole.yaml |
Pi-hole route (host-network, can't use Docker labels) |
plex.yml |
Plex route (external host 192.168.100.6:32400) |
sentinel.yml |
Sentinel route (external host 192.168.100.6:8088) |
dockscope-auth.yml |
BasicAuth middleware for DockScope (Docker socket access) |
Services fronted by Traefik
| Service | Hostname | Method |
|---|---|---|
| Prometheus | prometheus.home.server |
Docker labels |
| Grafana | grafana.home.server |
Docker labels |
| cAdvisor | cadvisor.home.server |
Docker labels |
| Loki | loki.home.server |
Docker labels |
| Alloy | alloy.home.server |
Docker labels |
| Pi-hole | pihole.home.server |
File provider |
| Portainer | portainer.home.server |
Docker labels |
| n8n | n8n.home.server |
Docker labels |
| Filebrowser | filebrowser.home.server |
Docker labels |
| DockScope | dockscope.home.server |
Docker labels + basicAuth |
| Semaphore | semaphore.home.server |
Docker labels |
| MinIO | minio.home.server |
Docker labels |
| Plex | plex.home.server |
File provider (external) |
| Sentinel | sentinel.home.server |
File provider (external) |
| Vaultwarden | vaultwarden.home.server |
Docker labels |
| Traefik Dashboard | traefik.home.server |
Docker labels |
TLS certificates
Same approach as the NPM era — mkcert creates a local CA trusted on each device, then signs a single certificate covering all *.home.server hostnames. The cert and key live in traefik/certs/ and are mounted read-only.
Traefik's file-based TLS store (dynamic/tls.yaml) applies this cert as the default for all routes — no per-host certificate assignment needed. Adding a new .home.server hostname automatically gets the right cert.
Network
All Traefik-fronted services join the web_net external Docker network:
docker network create web_net
Services that can't join web_net (host-network containers like Pi-hole, or external machines like Plex) are routed via the file provider with explicit IP:port targets.
Adding a new service
- Docker container on
web_net— add Traefik labels to its compose file - Host-network or external — create a YAML file in
dynamic/ - Pi-hole — add DNS record for
<service>.home.server
No restart required — Traefik auto-discovers Docker label changes and hot-reloads file provider changes.
Port 80/443 conflict with Pi-hole
Pi-hole runs in network_mode: host, which means it binds directly to ports 80 and 443 for its web UI — the same ports Traefik needs. Resolved by moving Pi-hole's web interface to port 8082 via the FTL environment variable:
environment:
FTLCONF_webserver_port: '8082o,[::]:8082o'
Pi-hole's DNS service (port 53) is untouched — only its web UI moved. See Reverse Proxy for the full history of this port conflict.
Security notes
- Dashboard is LAN-only via Pi-hole DNS (not exposed publicly)
- DockScope has basicAuth in addition to HTTPS (Docker socket access)
- All HTTP → HTTPS redirects are permanent (301)
- Security headers: HSTS (1 year, includeSubdomains, preload), XSS filter, content-type nosniff, SAMEORIGIN framing
Lessons learned
- Traefik's Docker label approach means the reverse proxy configuration is co-located with each service — no separate tool or UI to maintain. This matches the "each service is a folder" philosophy the rest of the homelab already follows.
- File-based routes in
dynamic/are useful for services that can't use Docker labels (host-network containers, external machines). Keeping them as separate YAML files makes it obvious which services use which routing method. - The
web_netexternal network is a hard requirement — Traefik can only route to containers on networks it's connected to. Forgetting to add a new service toweb_netis the most common "why isn't my route working" issue. - Hot-reloading (file provider + Docker provider) means zero-downtime config changes — a major operational improvement over NPM, which required a container restart for some config changes.