I Replaced Nginx Proxy Manager with Traefik

The Uptime Kuma Alert
I got an SSL certificate expiry alert from my Uptime Kuma on a Tuesday. Ten days left. Fine, I thought. Nginx Proxy Manager handles renewal. That’s literally the one thing it needs to do.
Ten days later, my sites were serving browser warnings. You know those full-page “Your connection is not private” screens that make your site look like it’s distributing malware? Yeah. That.
Nginx Proxy Manager just… stopped renewing. No error in the logs, no failed attempt, nothing. The UI still showed the certificates as “valid.” They weren’t. I restarted the container, clicked “Renew” manually, waited. Clicked again. Refreshed. Clicked again like it’s a crosswalk button.
Nothing.
Googled it, checked the logs again, tried different things. No clue what caused it. And that’s almost worse than a clear error - at least then you know what to fix. Nginx Proxy Manager just quietly decided to stop doing its one job. Like a smoke detector that works fine for two years and then just watches your kitchen burn down.
I Always Thought Traefik Was for Masochists
I’d heard of Traefik before. Every self-hosting thread has that one guy going “just use Traefik.” And every time I looked at the docs I saw diagrams with arrows going in seven directions, concepts like “entrypoints” and “routers” and “middlewares” and “providers” and I thought yeah, no, I have a life.
Nginx Proxy Manager had a pretty web UI. Click, click, SSL, done. Why would I trade that for whatever Traefik was trying to explain to me with those diagrams?
Well, because the click-click-done thing stopped doing the “done” part.
It’s Just Docker Labels
Okay so here’s the thing that nobody told me and I’m kind of annoyed about: setting up Traefik as a reverse proxy is not that complicated. The docs make it look like you’re configuring a space shuttle. You’re not. The entire concept is:
Traefik watches your Docker containers. You slap some labels on a container. Traefik reads them and goes “ah, this thing wants to be reachable at app.yourdomain.com, got it.” That’s it. That’s the whole thing.
No web UI, no database, no clicking through forms. Your reverse proxy config lives right next to the service in the same docker-compose.yml. I set up my Hugo blog deployment with Docker already, so this was basically just adding labels.
I wasted like two years being intimidated by this.
Traefik Docker Compose Config
Here’s my Traefik container, anonymized but structurally identical to what’s actually running:
services:
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
# HTTP → HTTPS redirect
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Wildcard cert via DNS challenge
- "--entrypoints.websecure.http.tls=true"
- "--entrypoints.websecure.http.tls.certresolver=letsencrypt"
- "--entrypoints.websecure.http.tls.domains[0].main=yourdomain.com"
- "--entrypoints.websecure.http.tls.domains[0].sans=*.yourdomain.com"
# Let's Encrypt
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=your-dns-provider"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=8.8.8.8:53,1.1.1.1:53"
- "--certificatesresolvers.letsencrypt.acme.email=you@yourdomain.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
environment:
- DNS_PROVIDER_API_KEY=your-api-key
- DNS_PROVIDER_API_SECRET=your-api-secret
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "letsencrypt:/letsencrypt"
networks:
- proxy
volumes:
letsencrypt:
networks:
proxy:
external: true
Looks like a wall of text. It’s not that bad, I promise.
The entrypoints are just “port 80” and “port 443.” The redirect block sends all HTTP to HTTPS. Four lines, never think about it again.
exposedbydefault=false is important. Without it Traefik happily exposes every container on your system to the internet. Ask me how I know. Actually don’t. Just set it to false.
The wildcard cert stuff - Nginx Proxy Manager could do this too, sure. But with Traefik it’s just lines in the command block. Domain, DNS provider, API credentials as environment variables, done. No UI wizard, no separate cert management page. Traefik supports a ton of providers - Cloudflare, Hetzner, Netcup, Route53, whatever you’re using.
And yeah, the renewal actually works. Automatically. Without silently failing. What a concept.
Adding a Docker Service to Traefik
This is the part that made me feel stupid for not switching sooner.
services:
myapp:
image: whatever/app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.yourdomain.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.services.myapp.loadbalancer.server.port=8080"
networks:
- proxy
Four labels. Container goes up, route exists. Container goes down, route disappears. No orphaned proxy hosts in some UI, no “wait, is this one still pointing somewhere?” I used to have like eight proxy entries in Nginx Proxy Manager and couldn’t remember what half of them were for.
Bonus: Error Pages That Don’t Look Like Ass
Totally optional but I love it. Instead of Traefik’s default “404 page not found” in plain text like it’s 1998, tarampampam/error-pages gives you a container that catches all errors and shows actually styled pages. They have a bunch of templates - I’m using ghost because it looks clean:
error-pages:
image: tarampampam/error-pages:latest
container_name: error-pages
environment:
- TEMPLATE_NAME=ghost
labels:
- "traefik.enable=true"
- "traefik.http.routers.error-pages.rule=HostRegexp(`{host:.+}`)"
- "traefik.http.routers.error-pages.priority=1"
- "traefik.http.services.error-pages.loadbalancer.server.port=8080"
Wire this up as a global middleware on the Traefik entrypoint and every 4xx/5xx gets caught and replaced. Even subdomains that don’t point anywhere yet get a nice 404 instead of a connection refused. Your broken stuff looks professional now. Priorities.
Traefik vs Nginx Proxy Manager: Do You Need a GUI?
People always ask this. “But Nginx Proxy Manager had a GUI.”
Yeah, and I opened it maybe twice a year. Every time I had to remember what port it was on, log in, find the right proxy host, click edit, change things, save. With Traefik I open the docker-compose.yml I’m already working on, change a label, docker compose up -d. Done.
And the config is in git. I can grep it, diff it, copy it to another server in seconds. Try migrating a Nginx Proxy Manager setup to a new server. You’re exporting a SQLite database and praying.
Migrating from Nginx Proxy Manager to Traefik
My plan was very sophisticated: open Nginx Proxy Manager one last time, screenshot every proxy host, then go through them one by one. Very organized. Very adult.
Portainer took like two minutes. Four labels, networks: proxy, done. I sat there waiting for something to go wrong. It didn’t. Weird feeling honestly, like when you parallel park on the first try and nobody’s around to witness it.
Open WebUI - same thing. I didn’t even remember what port that thing runs on. 3000? 8080? Doesn’t matter. You tell Traefik the port, Traefik handles it. I used to have a sticky note with port numbers on my monitor like some kind of medieval sysadmin. That’s gone now.
Then I hit the 502 wall. One service just wouldn’t come up. Traefik could see it, the labels were right, everything looked fine. I checked the config like four times. Restarted everything. Started questioning my life choices.
Forgot to add it to the proxy network.
Ten minutes of my life I’m not getting back. But honestly that was the only real hiccup. The rest was just copy-paste the same four labels, change the subdomain and port, move on. Some services I’d set up in Nginx Proxy Manager so long ago I couldn’t even remember what they did. Migrated them anyway. Figured I’d find out if anything breaks.
After everything was running I docker stop’d Nginx Proxy Manager, let it sit for a day just in case, then docker rm’d it. Felt like throwing out a printer that’s been jamming for months. Should’ve done it way sooner but you keep thinking maybe this time it’ll work.
TL;DR
Nginx Proxy Manager silently stopped renewing my SSL certs. Switched to Traefik as my Docker reverse proxy - it configures itself from container labels, so routing config lives right next to each service. Wildcard Let’s Encrypt certs via DNS challenge that actually auto-renew. Took about an hour to migrate everything. I spent longer being scared of Traefik than actually setting it up.