← Back to Blog

Setting up Traefik with Automatic SSL – Zero-touch Certificate Renewal for 30+ Services

A production-ready guide to configuring Traefik v2 as a reverse proxy with automatic Let's Encrypt SSL termination, dynamic configuration, HTTP-to-HTTPS redirection, and basic auth for internal dashboards.

5 min read Roman Swetly

Setting up Traefik with Automatic SSL

Zero-touch certificate renewal for 30+ services – a production-ready Traefik v2 configuration.


The Problem

When you manage 30+ web services across multiple servers, manually generating SSL certificates for each one quickly becomes a nightmare.

  • Renewing certificates every 90 days? Impossible at scale.
  • Managing certificate files across multiple containers? Error-prone.
  • Exposing services to the internet without proper encryption? Unacceptable.

I needed a single, automated entry point that would:

  1. Terminate SSL for all services.
  2. Automatically renew certificates without intervention.
  3. Route traffic to the correct backend service based on domain.
  4. Support both internal and externally-hosted services.

Enter Traefik.


Architecture Overview

My approach treats Traefik as the “Edge Gateway” — the single point of entry for all HTTP/HTTPS traffic.

Internet → Traefik (Docker) → Backend Services (containers + external hosts)
                │
                ├── Automatic SSL (Let's Encrypt)
                ├── Dynamic routing (based on Host rule)
                ├── HTTP → HTTPS redirect
                └── Dashboard + Basic Auth (secure internal access)

Key design decisions:

DecisionWhy
Docker-based deploymentPortable, maintainable, and self-contained.
External network (traefik-net)Allows containers on different compose stacks to communicate through Traefik.
File provider for dynamic configSupports services not running in Docker (e.g., physical hosts, VMs).
HTTP Challenge (port 80)Simpler than DNS challenge; no extra API keys required.
Dedicated letsencrypt/ volumeCertificate persistence across container restarts.

The Docker Compose Configuration

Here’s the production docker-compose.yml I run:

version: '3.7'

services:
  traefik:
    image: traefik:v2.10
    container_name: traefik
    restart: always
    command:
      # Dashboard (available on dash.example.com with auth)
      - "--api.insecure=true"  # Required for dashboard; secured with middleware below

      # Providers: Docker + dynamic files
      - "--providers.docker=true"
      - "--providers.docker.exposedByDefault=false"  # Only expose containers with explicit labels
      - "--providers.docker.network=traefik-net"
      - "--providers.file.directory=/etc/traefik/dynamic"
      - "--providers.file.watch=true"

      # Entry points
      - "--entrypoints.http.address=:80"
      - "--entrypoints.https.address=:443"

      # Let's Encrypt HTTP Challenge
      - "--certificatesresolvers.letsEncrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsEncrypt.acme.httpchallenge.entrypoint=http"
      - "--certificatesresolvers.letsEncrypt.acme.email=your@mail.com"
      - "--certificatesresolvers.letsEncrypt.acme.storage=/letsencrypt/acme.json"

    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"
      - "./dynamic:/etc/traefik/dynamic"
      - "./data/custom/:/custom/:ro"
    labels:
      # Enable Traefik for this container
      - "traefik.enable=true"

      # Dashboard route (protected with Basic Auth)
      - "traefik.http.routers.traefik_dash.rule=Host(`dash.example.com`)"
      - "traefik.http.routers.traefik_dash.entrypoints=http"
      - "traefik.http.routers.traefik_dash.service=api@internal"
      - "traefik.http.routers.traefik_dash.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$TXXXXXn$$oyOj9EjnzzEkr****"

    networks:
      - traefik-net

networks:
  traefik-net:
    external: true

Important Notes

  1. Dashboard Access: The dashboard is exposed at dash.example.com with HTTP Basic Auth. The password hash is generated using htpasswd -nb admin yourpassword.

  2. exposedByDefault=false: This is critical. It prevents accidental exposure of new containers. You must explicitly label each container with traefik.enable=true to route traffic.

  3. Persistent Certificates: The ./letsencrypt/acme.json file stores certificates. It’s mounted as a volume to survive container restarts.


Testing with a Sample Service

I use the traefik/whoami container as a test service:

services:
  whoami:
    image: "traefik/whoami"
    container_name: "whoami"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`example.com`)"
      - "traefik.http.routers.whoami.entrypoints=https"
      - "traefik.http.routers.whoami.tls.certresolver=letsEncrypt"
    networks:
      - traefik-net

This automatically:

  • Routes https://example.com to the whoami container.
  • Issues a Let’s Encrypt certificate on first request.
  • Renews it automatically before expiration.

Dynamic Configuration (File Provider)

For services not running in Docker (e.g., a Piwigo instance on a separate physical host 99.99.99.99:2222), I use static YAML files in the ./dynamic/ directory.

HTTP Route (Port 80)

# dynamic/2222_http_piwigo.yml
http:
  routers:
    2222-http-router1:
      entryPoints:
        - http
      service: 2222_piwigo_n1
      rule: Host(`example.com`)
  services:
    2222_piwigo_n1:
      loadBalancer:
        servers:
          - url: http://99.99.99.99:2222/
        passHostHeader: true

HTTPS Route (Port 443)

# dynamic/2222_piwigo_n.yml
http:
  routers:
    2222-http-router:
      entryPoints:
        - https
      service: 2222_piwigo_n
      rule: Host(`example.com`)
      tls:
        certResolver: letsEncrypt
  services:
    2222_piwigo_n:
      loadBalancer:
        servers:
          - url: http://99.99.99.99:2222/
        passHostHeader: true

Automatic HTTP → HTTPS Redirection

To enforce HTTPS for all traffic, I use a middleware:

# dynamic/4039_web_subname.yml
http:
  routers:
    4039-http-router:
      entryPoints:
        - http
      service: 4039_web_apache
      rule: Host(`subname.example.com`)
      middlewares:
        - redirect-to-https

    4039-https-router:
      entryPoints:
        - https
      service: 4039_web_apache
      rule: Host(`subname.example.com`)
      tls:
        certResolver: letsEncrypt

  middlewares:
    redirect-to-https:
      redirectScheme:
        scheme: https
        permanent: true

  services:
    4039_web_apache:
      loadBalancer:
        servers:
          - url: http://99.99.99.99:8080/
        passHostHeader: true

Result: Any request to http://subname.example.com is redirected to https://subname.example.com automatically (HTTP 301 permanent redirect).


The Dashboard

Traefik provides a web dashboard at /dashboard/ (or custom domain). With my configuration, it’s available at dash.example.com:

Traefik Dashboard

The dashboard shows:

  • All active routers and services.
  • TLS status for each route.
  • Live traffic information.

Security: Protected with HTTP Basic Auth to prevent public access.


Certificate Renewal in Action

Let’s Encrypt certificates are valid for 90 days. Traefik automatically attempts renewal 30 days before expiration.

If a certificate cannot be renewed (e.g., domain not resolving), Traefik will:

  1. Attempt again on the next schedule.
  2. Continue serving the existing certificate until it expires.
  3. Log errors to the container logs for debugging.

My advice: Monitor the Traefik logs and set up alerts (e.g., in Grafana) for failed ACME challenges.


Key Takeaways

ConceptImplementation
Zero-touch SSLLet’s Encrypt + HTTP Challenge eliminates manual certificate management.
Scalability30+ services handled with zero config duplication.
Mixed WorkloadsSupports both Docker containers and external hosts via file provider.
SecurityBasic Auth on dashboards, HTTP → HTTPS redirect, and exposedByDefault=false.
MaintainabilityDynamic config reloads automatically on file changes.

Final Thoughts

This setup has been running 24/7 for months, handling traffic for 30+ services including WordPress, Nextcloud, Matrix, Rocket.Chat, Laravel apps, and IoT dashboards. It requires zero manual intervention for certificate renewal — I’ve never had to touch the acme.json file.

The architecture is battle-tested, proven, and scales to 100+ services with ease.


This is part of my Industrial Infrastructure Platform (II-PaaS). For detailed architecture diagrams or a technical walkthrough, feel free to reach out.

← Previous Post
Hardening Ubuntu for Industrial IoT – Firewall, Fail2ban, and Kernel Tuning for 24/7 Operation
Next Post →
Heltec V3 – LoRa Receiver with OLED Display

Related Articles