6.7 KiB
Traefik
Self-hosted reverse proxy replacing Nginx Proxy Manager — gives every service a friendly <service>.home.server hostname with locally-trusted HTTPS, configured entirely as code.
Why the migration from Nginx Proxy Manager
NPM works fine, but the switching cost grew as the homelab did. Two things pushed the move:
1. Infrastructure as code
NPM's entire configuration lives behind a web UI — proxy hosts, SSL certificates, custom locations, access rules. None of it is version-controlled, exportable, or reproducible. If the container dies and comes back without its data volume, you're rebuilding every proxy host by hand.
Traefik is the opposite: every route, middleware, TLS cert, and entrypoint is defined in YAML files that live in this repo. The compose file, the dynamic/ configs, the cert files — all version-controlled, all diffable, all reproducible with docker compose up -d. This is the same pattern the rest of the homelab already follows (each service is a Compose file in a folder), and Traefik fits that model naturally where NPM didn't.
2. Docker daemon auto-discovery
NPM requires you to manually add each proxy host through its web UI — type the domain, point it at the container's IP and port, attach an SSL cert, save. Every new service means another round-trip through the UI.
Traefik talks directly to the Docker daemon and watches for containers with traefik.enable=true. Add a few labels to any compose file and Traefik picks up the new route automatically — no UI interaction, no restart, no manual configuration. This means adding a new service to the reverse proxy is a two-line addition to that service's compose file, not a separate step in a different tool.
How it works
traefik/
├── compose.yaml # Traefik container + static config
├── certs/ # Local TLS certificates (mkcert)
│ ├── home.server.crt
│ └── home.server.key
└── dynamic/ # File-based provider — routes, middlewares, TLS
├── tls.yaml # Default TLS cert store
├── middlewares.yaml # HTTPS redirect + security headers
├── pihole.yaml # Pi-hole route
├── plex.yml # Plex route (external host)
├── sentinel.yml # Sentinel route (external host)
└── dockscope-auth.yml # Basic auth middleware for DockScope
Static config (compose.yaml)
Traefik's static configuration is passed as command-line flags:
- Docker provider — watches the Docker daemon for labeled containers on the
web_netnetwork - File provider — watches
/dynamic/for YAML config changes (hot-reloaded, no restart needed) - Entrypoints —
:80(HTTP) and:443(HTTPS) - API dashboard — enabled but not insecure (accessed via its own HTTPS route)
Dynamic config (dynamic/)
Routes and middlewares are defined as YAML files, not labels. This keeps the compose files clean and makes it easy to see all routing rules in one place:
middlewares.yaml— sharedredirect-to-httpsandsecurity-headersmiddlewares (HSTS, XSS filter, content-type nosniff, SAMEORIGIN framing)tls.yaml— default TLS cert store pointing at the mkcert-generated certificatepihole.yaml,plex.yml,sentinel.yml— per-service route definitions for services that can't use Docker labels (host-network or external hosts)
Services using Docker labels
Most services define their Traefik routes directly in their compose files via labels:
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.<service>.rule=Host(`<service>.home.server`)"
- "traefik.http.routers.<service>.entrypoints=websecure"
- "traefik.http.routers.<service>.tls=true"
- "traefik.http.routers.<service>.middlewares=security-headers@file"
- "traefik.http.routers.<service>-http.rule=Host(`<service>.home.server`)"
- "traefik.http.routers.<service>-http.entrypoints=web"
- "traefik.http.routers.<service>-http.middlewares=redirect-to-https@file"
- "traefik.http.services.<service>.loadbalancer.server.port=<internal-port>"
Services currently fronted by Traefik:
| Service | Hostname | Internal Port |
|---|---|---|
| Prometheus | prometheus.home.server |
9090 |
| Grafana | grafana.home.server |
3000 |
| cAdvisor | cadvisor.home.server |
8080 |
| Loki | loki.home.server |
3100 |
| Alloy | alloy.home.server |
12345 |
| Pi-hole | pihole.home.server |
8082 (host) |
| Portainer | portainer.home.server |
9000 |
| n8n | n8n.home.server |
5678 |
| Filebrowser | filebrowser.home.server |
80 |
| DockScope | dockscope.home.server |
4681 |
| Semaphore | semaphore.home.server |
3000 |
| MinIO | minio.home.server |
9001 |
| Plex | plex.home.server |
32400 (external) |
| Sentinel | sentinel.home.server |
8088 (external) |
| Vaultwarden | vaultwarden.home.server |
80 |
| Traefik Dashboard | traefik.home.server |
API@internal |
TLS certificates
Same approach as before — mkcert generates a local CA and signs certificates for *.home.server. The cert and key live in certs/ and are mounted read-only into the Traefik container.
Traefik loads the default cert via dynamic/tls.yaml:
tls:
stores:
default:
defaultCertificate:
certFile: /certs/home.server.crt
keyFile: /certs/home.server.key
Adding a new service with a .home.server hostname automatically gets the right cert — no per-host certificate assignment needed.
Adding a new service
- If the service is a Docker container on
web_net— add Traefik labels to its compose file (copy the pattern from any existing service above) - If the service is host-network or external — create a new YAML file in
dynamic/with the router and service definition - Add the hostname to Pi-hole — so DNS resolves
<service>.home.serverto the server's LAN IP
No restart required — Traefik watches both the Docker daemon and the dynamic/ directory and picks up changes automatically.
Network
All Traefik-fronted services join the web_net external Docker network. This network must be created before starting Traefik:
docker network create web_net
Security notes
- The Traefik dashboard is behind its own HTTPS route with security headers — not exposed on the public internet, LAN-only via Pi-hole DNS
- DockScope additionally uses a basicAuth middleware (
dynamic/dockscope-auth.yml) since it mounts the Docker socket - All HTTP requests are redirected to HTTPS permanently (301)
- Security headers include HSTS (1 year, includeSubdomains, preload), XSS filter, content-type nosniff, and SAMEORIGIN framing