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:

  1. Added the Traefik service to docker-compose.yml
  2. Configured routing via Docker labels on existing services
  3. Removed the cronginx container
  4. 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

  1. DNS configuration matters: Make sure Cloudflare proxy is disabled (DNS only) for Let’s Encrypt HTTP challenge
  2. Network isolation: Keep services on separate Docker networks for security

Next Steps

Planning to add monitoring and automated backups…