Traefik as a Reverse Proxy: Automatic HTTPS for All Your Services

How I replaced my Nginx reverse proxy config with Traefik and got automatic SSL certificates, service discovery, and a dashboard — with a fraction of the config.

For a long time, my reverse proxy setup was a pile of Nginx config files — one per service, each with its own SSL cert renewal cron job, each slightly different. It worked. It was also a maintenance headache.

Traefik replaced all of that with a single docker-compose.yml and automatic Let’s Encrypt certificate management. Here’s the setup that runs on my server today.

What Traefik Does

Traefik is a reverse proxy built specifically for container environments. The key difference from Nginx: it discovers services automatically through Docker labels. You don’t write a config file per service — you add labels to your containers and Traefik figures out the routing.

Add a service, it appears. Remove a container, its route disappears. No reload, no restart.

Prerequisites

  • A VPS with Docker and Docker Compose installed
  • A domain pointed at your server’s IP
  • Ports 80 and 443 open in your firewall

The Core Traefik Setup

Create a directory for Traefik and its data:

mkdir -p ~/traefik/data
touch ~/traefik/data/acme.json
chmod 600 ~/traefik/data/acme.json  # Critical — Traefik won't start without this

Static Configuration

Create ~/traefik/traefik.yml:

api:
  dashboard: true
  insecure: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: you@yourdomain.com
      storage: /data/acme.json
      httpChallenge:
        entryPoint: web

providers:
  docker:
    exposedByDefault: false
  file:
    filename: /data/config.yml
    watch: true

The exposedByDefault: false line is important — it means Traefik won’t automatically route to containers unless they explicitly opt in with labels.

Docker Compose

Create ~/traefik/docker-compose.yml:

version: "3.8"

networks:
  proxy:
    external: true

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./data/acme.json:/data/acme.json
      - ./data/config.yml:/data/config.yml:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(`traefik.yourdomain.com`)"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$05$$yourhashedpasswordhere"

Create the shared network first:

docker network create proxy
docker compose up -d

Adding a Service

Here’s where Traefik gets satisfying. Adding a new service behind your reverse proxy is just adding labels. Here’s Uptime Kuma as an example:

version: "3.8"

networks:
  proxy:
    external: true

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    networks:
      - proxy
    volumes:
      - ./data:/app/data
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.uptime.rule=Host(`status.yourdomain.com`)"
      - "traefik.http.routers.uptime.entrypoints=websecure"
      - "traefik.http.routers.uptime.tls.certresolver=letsencrypt"

That’s it. Traefik detects the container, fetches a certificate from Let’s Encrypt, and status.yourdomain.com is live over HTTPS. No Nginx config, no cert renewal cron jobs.

Useful Middlewares

Traefik’s middleware system lets you add functionality at the router level without modifying your application.

Security Headers

Create ~/traefik/data/config.yml:

http:
  middlewares:
    secure-headers:
      headers:
        sslRedirect: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: "strict-origin-when-cross-origin"

Apply to any router:

- "traefik.http.routers.uptime.middlewares=secure-headers@file"

Rate Limiting

http:
  middlewares:
    rate-limit:
      rateLimit:
        average: 100
        burst: 50

The Dashboard

Traefik’s dashboard at traefik.yourdomain.com shows all your routers, services, and middlewares in real time. It’s genuinely useful for debugging — you can see exactly what Traefik thinks is configured and why a route might not be matching.

Make sure your dashboard route has the BasicAuth middleware applied. An unprotected Traefik dashboard leaks your service topology to anyone who finds it.

What I Learned the Hard Way

The acme.json permissions matter. If it’s not 600, Traefik refuses to start. The error message isn’t obvious about why.

Let’s Encrypt rate limits are real. You get 5 certificate failures per hostname per hour before you’re temporarily blocked. Use the staging resolver (caServer: https://acme-staging-v02.api.letsencrypt.org/directory) while testing.

Don’t expose the Docker socket on a public-facing server without caution. Read-only socket access with a security proxy like socket-proxy is worth the extra setup on anything serious.

Traefik has made my self-hosted setup dramatically easier to manage. Adding a new service takes 10 lines of labels instead of a new Nginx config and a cert renewal job.