# Traefik Self-hosted reverse proxy replacing Nginx Proxy Manager — gives every service a friendly `.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_net` network - **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`** — shared `redirect-to-https` and `security-headers` middlewares (HSTS, XSS filter, content-type nosniff, SAMEORIGIN framing) - **`tls.yaml`** — default TLS cert store pointing at the mkcert-generated certificate - **`pihole.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: ```yaml labels: - "traefik.enable=true" - "traefik.docker.network=web_net" - "traefik.http.routers..rule=Host(`.home.server`)" - "traefik.http.routers..entrypoints=websecure" - "traefik.http.routers..tls=true" - "traefik.http.routers..middlewares=security-headers@file" - "traefik.http.routers.-http.rule=Host(`.home.server`)" - "traefik.http.routers.-http.entrypoints=web" - "traefik.http.routers.-http.middlewares=redirect-to-https@file" - "traefik.http.services..loadbalancer.server.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](https://github.com/FiloSottile/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`: ```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 1. **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) 2. **If the service is host-network or external** — create a new YAML file in `dynamic/` with the router and service definition 3. **Add the hostname to Pi-hole** — so DNS resolves `.home.server` to 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: ```bash 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 [← Back to Home](../../docs/Home.md)