Containers and platforms

How to set up Let's Encrypt in Traefik v3

By · Last verified

Traefik v3 obtains and renews Let's Encrypt certificates itself through a certificate resolver, which each router selects with a Docker label, and an entry-point redirect sends HTTP to HTTPS. The example routes example.com to a Docker container.

The steps apply to Traefik v3.x (the reference docs describe v3.7 as of November 2026) running in Docker with Docker Compose.

Prerequisites

Traefik must own ports 80 and 443 on the host.

  • A Linux host with Docker Engine and the Compose plugin.
  • DNS A/AAAA records for example.com pointing at the host, for example 203.0.113.10.
  • Inbound TCP 80 and 443 open to all sources.
  • A project directory, for example /opt/traefik.

Step 1: Write the static configuration

The static configuration defines the two entry points, the HTTP-to-HTTPS redirect and a certificate resolver named le:

# /opt/traefik/traefik.yml
entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

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

providers:
  docker:
    exposedByDefault: false

Traefik answers ACME HTTP challenges on the web entry point before the redirect applies, so redirecting all HTTP traffic is safe.

Step 2: Create the Compose file

The Compose file runs Traefik and attaches the le resolver to a service through four labels:

# /opt/traefik/compose.yaml
services:
  traefik:
    image: traefik:v3.7
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - ./letsencrypt:/letsencrypt
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: unless-stopped

  whoami:
    image: traefik/whoami
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami.rule=Host(`example.com`)
      - traefik.http.routers.whoami.entrypoints=websecure
      - traefik.http.routers.whoami.tls.certresolver=le

Use the current v3 tag from the Traefik docs. Replace whoami with your own service and keep the labels.

Step 3: Prepare acme.json and start

Create acme.json with mode 600 before the first start, because Traefik refuses a file that other users can read:

cd /opt/traefik
mkdir -p letsencrypt
touch letsencrypt/acme.json && chmod 600 letsencrypt/acme.json
sudo docker compose up -d
sudo docker compose logs -f traefik

While testing, add caServer: https://acme-staging-v02.api.letsencrypt.org/directory under acme: to avoid production rate limits, then remove it and delete acme.json before going live.

Step 4 (optional): Wildcards with the DNS challenge

Replace httpChallenge with a DNS provider from the lego list, pass its credentials as environment variables, and request the wildcard on the router:

certificatesResolvers:
  le:
    acme:
      email: admin@example.com
      storage: /letsencrypt/acme.json
      dnsChallenge:
        provider: cloudflare
    environment:
      - CF_DNS_API_TOKEN=your-cloudflare-api-token
    labels:
      - traefik.http.routers.whoami.tls.domains[0].main=example.com
      - traefik.http.routers.whoami.tls.domains[0].sans=*.example.com

The environment block belongs to the traefik service, the labels to your application. To request Let's Encrypt's 45-day profile, add profile: tlsserver and certificatesDuration: 1080 to the resolver (see Let's Encrypt certificate profiles).

Verify

The issuer should be Let's Encrypt, not TRAEFIK DEFAULT CERT, and HTTP should redirect to HTTPS. Check the result with the SSL checker, or from any terminal:

curl ssl.sh/example.com

On the host:

curl -sI http://example.com | head -n 3
sudo docker compose logs traefik | grep -iE 'acme|certificate' | tail -n 20

Automatic renewal

Traefik renews certificates in the background while it runs; no cron job is needed. With the default certificatesDuration of 2160 hours it treats certificates as 90-day and starts renewing 30 days before expiry. Because renewal timing comes from that setting rather than from the certificate, update it when lifetimes change: Let's Encrypt's default becomes 64 days on February 10, 2027 and 45 days on February 16, 2028. Check the Traefik release notes for that version's recommended values, and add the domain to ssl.sh monitoring (in development) to confirm renewals happen.

Troubleshooting

  • permissions 644 for /letsencrypt/acme.json are too open: run chmod 600 on the file.
  • Browser shows TRAEFIK DEFAULT CERT: the router has no resolver or issuance failed; see NET::ERR_CERT_AUTHORITY_INVALID and the Traefik logs.
  • Timeout during connect in the logs: port 80 does not reach Traefik; the causes match certbot timeout during connect.
  • too many certificates already issued: acme.json was not persisted between restarts; see Let's Encrypt rate limits.
  • router uses a nonexistent resolver: the name in tls.certresolver differs from the one in certificatesResolvers.

FAQ

Why does the browser show TRAEFIK DEFAULT CERT?

Traefik serves its self-signed default certificate when no certificate matches the requested host, usually because the router has no tls.certresolver, the Host() rule does not match, or issuance failed. Check the Traefik logs for ACME errors.

Do I lose certificates when the container restarts?

Only if acme.json is not on a persistent volume. Without it, Traefik requests new certificates on every start and soon runs into Let's Encrypt rate limits.

Should I use the HTTP, TLS or DNS challenge?

Use httpChallenge when port 80 reaches Traefik, tlsChallenge when only port 443 does, and dnsChallenge for wildcards or private hosts. Only one challenge type is used per resolver.

Sources

  1. Traefik: ACME certificate resolvers (reference)
  2. Traefik: Let's Encrypt
  3. lego DNS provider: Cloudflare
  4. Let's Encrypt: Challenge Types