Using Traefik as a Reverse Proxy for a Self-Hosted Stack
TIL that manually editing an Nginx config and restarting it for every new service didn't scale past a handful of containers — Traefik's label-based routing removed that step entirely by discovering services automatically.
The Problem #
The self-hosted home server stack (Jellyfin, the *arr apps, qBittorrent, and eventually a handful of personal project deployments) started behind a hand-written Nginx config: one server block per service, manually edited and reloaded every time a new container was added or a domain changed.
Context #
This began with two or three services, where a manual Nginx config was genuinely the simplest option — the friction only became real once the number of services kept growing.
What I Tried #
Kept adding server blocks to the same Nginx config, copy-pasting the previous block and updating the port and domain each time.
What Went Wrong #
Every new service meant editing a shared config file, checking the syntax was right, and reloading Nginx — a manual step that was easy to get wrong (a typo'd port, a forgotten reload) and that coupled "deploy a new container" to "also remember to update an unrelated config file."
The Solution #
Replaced Nginx with Traefik, configured to discover routing rules from Docker labels on each container directly, so adding a service means adding labels to that service's own docker-compose definition — no shared config file to edit at all.
services:
jellyfin:
image: jellyfin/jellyfin
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(`watch.example.com`)"
- "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"Why It Works #
Traefik watches the Docker socket and builds its routing table from container labels automatically, which means the routing configuration lives with the service it belongs to instead of in a separate, easy-to-forget file. Adding a service is a self-contained change again, the way it should be.
Lessons Learned #
A manual reverse-proxy config that's fine for three services is a different problem at ten — not because Nginx itself is wrong, but because the coordination cost of a shared config file grows with every service that has to remember to update it.
What I Would Do Differently #
I'd set up label-based service discovery from the second or third container, rather than waiting until manually maintaining the Nginx config had become a recurring source of small mistakes.
Related Concepts #
Docker label-based service discovery, automatic TLS via Let's Encrypt, reverse proxy routing tables.
Related content
Using Docker for Application Deployment: The Boring Win I Underrated
TIL that 'works on my machine' stopped being a real category of bug the day I containerized the API, not because Docker is clever but because it removed an entire class of environment-drift I'd been debugging manually.
rrr-stack: The Docker Compose File Behind My Arr Media Server
A service-by-service breakdown of rrr-stack, my one-command Docker Compose setup for Jellyseerr, Prowlarr, Sonarr, Radarr, Lidarr, qBittorrent, Jellyfin, Homepage, and Maintainerr.
Building My Self-Hosted Home Media Server with Docker
A complete walkthrough of my home server — TrueNAS for backups, Proxmox for virtualization, and a fully automated Docker media stack with the *Arr apps and Jellyfin.