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.

2 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

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.

Docker label-based service discovery, automatic TLS via Let's Encrypt, reverse proxy routing tables.