Traefik TLS Configuration: Automatic Let’s Encrypt Certificates for Docker Services

Traefik is a reverse proxy designed for container environments. One of its most useful features is automatic TLS certificate management — Traefik can request, store, and renew Let’s Encrypt certificates without any manual certificate management. This article explains how to configure Traefik v3 for automatic HTTPS with Let’s Encrypt using both HTTP-01 and DNS-01 challenges, set up TLS for Docker services using labels, configure custom certificates for internal services, and handle certificate renewal.

How Traefik handles TLS

Traefik manages TLS through three mechanisms:

  1. ACME (Let’s Encrypt) — Traefik automatically requests and renews certificates. Two challenge types: HTTP-01 (requires port 80) and DNS-01 (works behind firewalls using DNS provider APIs).
  2. Custom certificates — you provide PEM files or reference secrets.
  3. Passthrough — Traefik forwards TLS connections without terminating them.

Static vs dynamic configuration

Traefik has two configuration layers:

  • Static configuration (traefik.yml or CLI flags) — startup settings including ACME configuration. Requires a restart to change.
  • Dynamic configuration (Docker labels, files, K8s CRDs) — routing rules, middlewares, TLS options. Applied without restart.

Basic Traefik setup with Let’s Encrypt (HTTP-01)

docker-compose.yml

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    command:
      # API and dashboard
      - "--api.dashboard=true"
      - "--api.insecure=false"

      # Docker provider
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"

      # Entrypoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"

      # Redirect HTTP to HTTPS globally
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.web.http.redirections.entrypoint.permanent=true"

      # Let's Encrypt ACME with HTTP-01 challenge
      - "--certificatesresolvers.myresolver.acme.email=admin@example.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"

      # Uncomment to use Let's Encrypt staging (for testing)
      # - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"

    ports:
      - "80:80"
      - "443:443"

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik_letsencrypt:/letsencrypt

    labels:
      # Expose the dashboard over HTTPS
      - "traefik.enable=true"
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
      - "traefik.http.routers.traefik-dashboard.tls.certresolver=myresolver"
      - "traefik.http.routers.traefik-dashboard.service=api@internal"
      # Add basic auth for the dashboard
      - "traefik.http.routers.traefik-dashboard.middlewares=traefik-auth"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$..."  # htpasswd output

  myapp:
    image: nginx:alpine
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls.certresolver=myresolver"
      - "traefik.http.services.myapp.loadbalancer.server.port=80"

volumes:
  traefik_letsencrypt:

acme.json is created automatically. The permissions on the mounted directory must allow Traefik to write to it.

Let’s Encrypt with DNS-01 challenge (for private networks)

DNS-01 validates domain ownership by creating a TXT record, so it works even when port 80 is not accessible. Traefik supports many DNS providers via the lego library.

Cloudflare example

services:
  traefik:
    image: traefik:v3.0
    environment:
      # Cloudflare API token (must have Zone:DNS:Edit permission)
      CF_DNS_API_TOKEN: "your-cloudflare-api-token"
    command:
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      # DNS-01 challenge via Cloudflare
      - "--certificatesresolvers.cloudflare.acme.email=admin@example.com"
      - "--certificatesresolvers.cloudflare.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare"
      - "--certificatesresolvers.cloudflare.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
      # Wildcard certificate
      - "--certificatesresolvers.cloudflare.acme.dnschallenge.delaybeforecheck=10"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik_letsencrypt:/letsencrypt
    ports:
      - "80:80"
      - "443:443"

With wildcard certificates, use Host(\*.example.com`)` in router rules:

labels:
  - "traefik.http.routers.myservice.rule=Host(`api.example.com`)"
  - "traefik.http.routers.myservice.tls.certresolver=cloudflare"
  # For wildcard, specify the main domain for certificate lookup:
  - "traefik.http.routers.myservice.tls.domains[0].main=example.com"
  - "traefik.http.routers.myservice.tls.domains[0].sans=*.example.com"

Static configuration file (traefik.yml)

For more complex setups, use a configuration file instead of CLI flags:

# /etc/traefik/traefik.yml

api:
  dashboard: true
  insecure: false

log:
  level: INFO

providers:
  docker:
    exposedByDefault: false
  file:
    filename: /etc/traefik/dynamic.yml
    watch: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: myresolver

certificatesResolvers:
  myresolver:
    acme:
      email: admin@example.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

Custom TLS certificates (non-Let’s Encrypt)

For internal services with your own CA:

Create a dynamic configuration file:

# /etc/traefik/dynamic.yml

tls:
  certificates:
    - certFile: /etc/traefik/certs/internal.crt
      keyFile:  /etc/traefik/certs/internal.key

  stores:
    default:
      defaultCertificate:
        certFile: /etc/traefik/certs/internal.crt
        keyFile:  /etc/traefik/certs/internal.key

  options:
    default:
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
        - TLS_AES_256_GCM_SHA384
        - TLS_CHACHA20_POLY1305_SHA256
      sniStrict: true

Reference the custom options in Docker labels:

labels:
  - "traefik.http.routers.myapp.tls=true"
  - "traefik.http.routers.myapp.tls.options=default@file"

TLS for Docker Swarm

In Docker Swarm mode, Traefik labels go on the service, not individual containers:

services:
  myapp:
    image: myapp:latest
    deploy:
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
        - "traefik.http.routers.myapp.entrypoints=websecure"
        - "traefik.http.routers.myapp.tls.certresolver=myresolver"
        - "traefik.http.services.myapp.loadbalancer.server.port=8080"

Hardening Traefik TLS

Set a global TLS options configuration to enforce minimum TLS version and cipher suites:

# dynamic.yml
tls:
  options:
    modern:
      minVersion: VersionTLS13
    intermediate:
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305

Apply per router:

labels:
  - "traefik.http.routers.myapp.tls.options=intermediate@file"

HTTPS middleware: headers and HSTS

Add security headers globally via middleware:

# dynamic.yml
http:
  middlewares:
    security-headers:
      headers:
        stsSeconds: 63072000
        stsIncludeSubdomains: true
        stsPreload: true
        forceSTSHeader: true
        contentTypeNosniff: true
        browserXssFilter: true
        frameDeny: true

Apply to routers:

labels:
  - "traefik.http.routers.myapp.middlewares=security-headers@file"

Monitoring certificate expiry

Traefik exposes certificate expiry metrics when using Prometheus:

# traefik.yml
metrics:
  prometheus:
    addEntryPointsLabels: true
    addRoutersLabels: true
    addServicesLabels: true

The metric traefik_tls_certs_not_after contains the expiry timestamps of all certificates Traefik manages.

Troubleshooting

ProblemCauseFix
acme: error: 403: urn:acme:error:unauthorizedDomain DNS not pointing to this serverUpdate DNS A record
too many registrationsHit Let’s Encrypt rate limitUse staging server for testing
Certificate not renewedacme.json permissions wrongchmod 600 acme.json
router does not have TLS enabledMissing tls=true or tls.certresolver labelAdd traefik.http.routers.X.tls=true label
Services get HTTP even though HTTPS configuredexposedByDefault: false and no traefik.enable=true labelAdd traefik.enable=true to the service
Wildcard cert not matching subdomainstls.domains[0].sans not setExplicitly set the wildcard SAN in router labels

Summary

Traefik automates TLS certificate management through its ACME integration. For public-facing services, the HTTP-01 challenge with Let’s Encrypt is a single configuration block in traefik.yml. For private networks or wildcard certificates, the DNS-01 challenge uses your DNS provider’s API. Services opt into HTTPS by adding traefik.http.routers.X.tls.certresolver=myresolver to their Docker labels. Custom certificates for internal CAs are configured in a dynamic configuration file and referenced per router. Traefik handles renewal automatically before expiry with no manual intervention.

Scroll to Top