Skip to content

Traefik with Let's Encrypt — HTTP-01, TLS-ALPN, and DNS-01 in production

Configure Traefik certificate resolvers for Let''s Encrypt — pick the right challenge, persist acme.json safely, and rotate when the API token expires.

Traefik can solve every ACME challenge on its own without certbot or cert-manager — a single certificatesResolvers block in static config gets you Let’s Encrypt issuance and renewal, with the result stored in acme.json. The trap is that the right challenge type depends on what you serve and from where: HTTP-01 needs port 80 reachable, TLS-ALPN-01 needs port 443 reachable and breaks on intermediate proxies, DNS-01 needs an API token but is the only way to get a wildcard. This article walks through configuring each, persisting the store, and what to check before the next renewal cycle.

How to verify

ls -l /opt/traefik/acme/acme.json
# Must be mode 600. World-readable means Traefik refuses to write.
sudo jq '.letsencrypt.Certificates | length' /opt/traefik/acme/acme.json
# Number of certificates currently stored.
sudo jq -r '.letsencrypt.Certificates[].domain.main' /opt/traefik/acme/acme.json | sort -u
# Hostnames currently held — confirm the wildcard is there.
docker logs traefik 2>&1 | grep -iE 'acme|certificate' | tail -30
openssl s_client -connect app.example.com:443 -servername app.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -dates

The issuer must read Let's Encrypt (not STAGING). If the dates are within 30 days of notAfter and the log shows no renewal attempts, something is wrong with the resolver.

What’s happening

ACME is a request-response protocol. Traefik asks Let’s Encrypt for a certificate, Let’s Encrypt issues a challenge to prove control of the domain, Traefik answers, Let’s Encrypt verifies, and the cert is issued. The three challenge types differ in how Traefik proves control.

HTTP-01 serves a token at http://<domain>/.well-known/acme-challenge/<token>. It needs Traefik’s web entrypoint (port 80) to be reachable from Let’s Encrypt’s validation servers. It does not work for wildcards. If anything redirects port 80 to HTTPS before reaching Traefik (a CDN, another reverse proxy, a firewall doing HTTP redirection), HTTP-01 fails.

TLS-ALPN-01 serves the token via a special TLS handshake on port 443 using the acme-tls/1 ALPN protocol. It needs port 443 reachable directly to Traefik, and it does not work behind any TLS-terminating proxy or CDN. It does not work for wildcards either.

DNS-01 creates a _acme-challenge.<domain> TXT record via the provider’s API. It works regardless of inbound network access, and it is the only way to get a wildcard certificate. It needs an API token from your DNS provider (Cloudflare, Route 53, DigitalOcean DNS, OVH, etc.) scoped tightly to the zone. The token sits in an environment variable that Traefik reads.

Once issued, the cert is stored in acme.json. Traefik handles renewal automatically, attempting at 30 days remaining. The file must be mode 600 and owned by the UID Traefik runs as, or Traefik silently refuses to write to it.

The procedure

  1. HTTP-01 — the simplest, for non-wildcard hosts when port 80 is reachable.

    certificatesResolvers:
      letsencrypt:
        acme:
          email: [email protected]
          storage: /etc/traefik/acme/acme.json
          keyType: EC256
          httpChallenge:
            entryPoint: web
  2. TLS-ALPN-01 — for environments where port 80 is closed and port 443 reaches Traefik directly.

    certificatesResolvers:
      letsencrypt:
        acme:
          email: [email protected]
          storage: /etc/traefik/acme/acme.json
          keyType: EC256
          tlsChallenge: {}
  3. DNS-01 — required for wildcards, recommended when you front Traefik with a CDN or load balancer.

    certificatesResolvers:
      letsencrypt:
        acme:
          email: [email protected]
          storage: /etc/traefik/acme/acme.json
          keyType: EC256
          dnsChallenge:
            provider: cloudflare
            resolvers:
              - "1.1.1.1:53"
              - "1.0.0.1:53"

    The Cloudflare provider reads CF_DNS_API_TOKEN from the environment. Pass it via compose:

    services:
      traefik:
        image: traefik:v3.1
        environment:
          - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
  4. Pin to the staging endpoint while testing — Let’s Encrypt rate limits production at 5 failures per hour per account-host combination. A wrong DNS token can burn that budget in five minutes.

    certificatesResolvers:
      letsencrypt:
        acme:
          caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"

    Remove caServer once issuance succeeds and rerun. Move the old acme.json aside first — staging certs cannot be promoted to prod.

  5. Request a wildcard from a router by setting tls.domains.

    http:
      routers:
        api:
          rule: "HostRegexp(`{subdomain:[a-z0-9-]+}.example.com`)"
          entryPoints: [websecure]
          service: api-backend
          tls:
            certResolver: letsencrypt
            domains:
              - main: "example.com"
                sans:
                  - "*.example.com"
  6. Back up acme.json regularly. It contains the account key — losing it means re-registering and re-issuing every cert. The simplest backup is a daily snapshot to S3 / object storage.

    sudo cp -p /opt/traefik/acme/acme.json /backup/acme.$(date +%F).json

Common pitfalls

  • Pointing Cloudflare DNS-01 at a token without “Zone:DNS:Edit” — issuance fails with a vague “TXT record not propagated” message; the underlying cause is the token couldn’t create the record.
  • Forgetting resolvers: on dnsChallenge inside a private network where the host’s resolver does not see public DNS — the pre-check fails before Let’s Encrypt even gets called.
  • Running two Traefik instances against the same acme.json — only one can write at a time; the second loses every renewal. Use the consul/etcd-backed storage or a single-writer pattern.
  • Putting the email at a Gmail of an engineer who leaves — Let’s Encrypt warns there to expiry; route it to a shared inbox.
  • Leaving caServer on staging in production — your acme.json fills with staging certs and you only discover it when a browser shows the staging warning.

ACME issuance is mostly self-driving once configured correctly, but the rotation of the API token, the integrity of acme.json, and the renewal cadence are what fail in year two. In the engagements Stack Harbor operates, those are tracked alongside DNS and edge config in our managed operations playbook. For the dashboard-and-router pieces that consume these certs, see traefik-routers-services-middlewares.