I recently migrated my personal infrastructure from nginx to Traefik for reverse proxy and SSL management. Here’s what I learned.
The Setup
Running multiple services on a single VPS:
- OpenEMR for family health records
- This blog (Hugo static site)
- Future services as needed
Why Traefik?
Coming from nginx, Traefik offers several advantages:
- Automatic SSL certificate management with Let’s Encrypt
- Dynamic service discovery through Docker labels
- No manual config edits for new services
The Migration Process
Previously, I was using nginx with cronginx - a Docker container combining Cron, Nginx, and Certbot for automated Let’s Encrypt certificate management. It worked reliably for quite some time.
I discovered Traefik while watching Jim’s Garage on YouTube, and I was immediately impressed. Honestly, I was surprised I hadn’t heard of it before - it addresses so many container-related pain points that I’d just accepted as “the way things are.” The project recently celebrated 10 years, which speaks to its maturity and staying power.
The actual migration was straightforward:
- Added the Traefik service to
docker-compose.yml
- Configured routing via Docker labels on existing services
- Removed the cronginx container
- Started everything up
The simplicity was almost anticlimactic. What took hours with nginx config files now happens automatically through Docker labels. No more SSH-ing into the server to edit config files and restart nginx for every new service.
Key Configuration Details
Traefik static config (traefik.yml
):
- HTTP/HTTPS entry points with automatic redirect
- Let’s Encrypt HTTP challenge for certificate generation
- Docker provider for automatic service discovery
Service routing (docker-compose labels):
labels:
- "traefik.enable=true"
- "traefik.http.routers.service-secure.rule=Host(`domain.com`)"
- "traefik.http.routers.service-secure.tls.certresolver=cloudflare"
That’s it. Add a service, include the labels, and Traefik handles the rest.
What I Didn’t Expect The Docker networking took some debugging. Container name resolution matters - if your service expects to connect to mysql but the container is named xxx-mysql-1, you’ll get connection failures. Setting explicit container_name values in docker-compose solved this immediately.
Lessons Learned
- DNS configuration matters: Make sure Cloudflare proxy is disabled (DNS only) for Let’s Encrypt HTTP challenge
- Network isolation: Keep services on separate Docker networks for security
Next Steps
Planning to add monitoring and automated backups…