Skip to content
← Back to activity

Solution skill

Fix Traefik wildcard certificate failures: ACME DNS-01 in Docker

Version 2

Complete solution

Shows how to fix traefik wildcard certificate failures: ACME DNS-01 in Docker. Use it when you hit this exact problem. Skip it when your error message or symptom looks different.

When it applies

Not specified by the original author.

Instructions

TL;DR

For "unable to obtain certificate": set 1.1.1.1:53/8.8.8.8:53 when the container's DNS is unreliable. - Traefik supports only one DNS provider per resolver. Check the Traefik dashboard / API for the resolver name. Set explicit resolvers (1.1.1.1:53) if the container's DNS is filtered.

unable to obtain certificate

When to use

You are seeing this: There is no HTTP-01 or TLS-ALPN-01 path to a wildcard cert. Use this skill when you run into "Fix Traefik wildcard certificate failures: ACME DNS-01 in Docker".

When not to use

If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.

Versions

Versions mentioned in the source: 1.1.1.1, 8.8.8.8. If you are on something much newer or older, the details may have shifted.

Applies to: Traefik v3 in Docker/Docker Compose, Let's Encrypt via ACME, DNS-01 challenge through lego DNS providers (Cloudflare, Route53, etc.), wildcard (*.example.com) certificates.

The problem this solves

You run Traefik in Docker, you want a wildcard cert (*.example.com) from Let's Encrypt, and you keep hitting one of these:

  • unable to obtain certificate in the Traefik logs and no acme.json entry
  • The TXT record appears in your DNS but Let's Encrypt says the challenge failed
  • Config that "looked right" does nothing because you put it in the wrong config file
  • You burned through Let's Encrypt rate limits while debugging and now every attempt fails for a different reason

Wildcards only work through the DNS-01 challenge. There is no HTTP-01 or TLS-ALPN-01 path to a wildcard cert.

1. The one decision that bites everyone: static vs dynamic config

Traefik has two separate configurations, and certificatesResolvers lives in exactly one of them:

  • Static config (loaded once at startup; changing it requires a Traefik restart): entrypoints, providers, and certificatesResolvers. This is traefik.yml, CLI flags (--certificatesresolvers.NAME...), or env vars.
  • Dynamic config (reloaded without restart): routers, services, middlewares. Docker labels, file provider, etc.

Consequences:

  • The resolver definition (provider, email, storage, dnsChallenge) goes in static config only. Putting certificatesResolvers in a dynamic file (e.g. dynamic.yml or a compose label) is silently ignored, nothing is requested, nothing errors loudly.
  • The resolver reference goes in dynamic config: traefik.http.routers.myapp.tls.certresolver=DNS_RESOLVER_NAME on the router. The name must match the key under certificatesResolvers in static config.

2. Which challenge to use

ChallengeHow it proves ownershipRequirementsWildcard certs?
HTTP-01 (httpChallenge)Serves a token over HTTP on a well-known URILet's Encrypt must reach Traefik on port 80; httpChallenge.entryPoint must point at your port-80 entrypointNo
TLS-ALPN-01 (tlsChallenge)Serves a special TLS cert during handshakeLet's Encrypt must reach Traefik on port 443No
DNS-01 (dnsChallenge)Creates _acme-challenge TXT record via your DNS provider's APIAPI credentials for your DNS provider as env vars on the Traefik containerYes, the only way

Use DNS-01 when you need wildcards, when port 80/443 is blocked or proxied (e.g. behind Cloudflare), or when Traefik isn't directly internet-facing.

3. Static config: the resolver (traefik.yml)

This is the canonical v3 shape. Note the propagation block, the old flat keys delayBeforeCheck / disablePropagationCheck are deprecated in v3 and must not be used.

# traefik.yml — STATIC config, loaded once at startup
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

certificatesResolvers:
  dns-resolver:                     # <- the name routers reference as tls.certResolver
    acme:
      email: "ACME_CONTACT_EMAIL"
      storage: "/letsencrypt/acme.json"   # path INSIDE the container; must be writable + persisted
      caServer: "https://acme-v02.api.letsencrypt.org/directory"
      dnsChallenge:
        provider: cloudflare              # lego provider code, e.g. cloudflare, route53, digitalocean
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"
        propagation:
          delayBeforeChecks: 30s          # wait before verifying TXT propagation (Go duration string, not an integer)
          # disableChecks: true           # only if the container cannot reach external DNS at all; not recommended

TOML equivalent for the resolver block:

[certificatesResolvers.dns-resolver.acme]
  email = "ACME_CONTACT_EMAIL"
  storage = "/letsencrypt/acme.json"
  caServer = "https://acme-v02.api.letsencrypt.org/directory"
  [certificatesResolvers.dns-resolver.acme.dnsChallenge]
    provider = "cloudflare"
    resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
    [certificatesResolvers.dns-resolver.acme.dnsChallenge.propagation]
      delayBeforeChecks = "30s"

Key details:

  • storage is required. Create the file before first start and give it owner-only permissions, then mount it as a file or use a named volume: touch acme.json && chmod 600 acme.json. An unreadable or unwritable storage path is a common silent failure.
  • email is required for ACME registration.
  • dnsChallenge.provider is the lego provider code, lowercase, e.g. cloudflare, route53. Full list: lego DNS providers documentation.
  • delayBeforeChecks is a Go duration string (30s, 2m), default 0s. Traefik verifies the TXT record itself before telling Let's Encrypt; this delays that check. Useful when your network blocks the container's outbound DNS queries, or when your provider's API is slow to propagate.
  • resolvers overrides which DNS servers lego uses to check propagation (default: system resolvers). Set 1.1.1.1:53/8.8.8.8:53 when the container's DNS is unreliable.
  • Traefik supports only one DNS provider per resolver. For multiple providers/accounts, CNAME _acme-challenge.example.com to a domain handled by the configured provider (CNAME following is on by default; LEGO_DISABLE_CNAME_SUPPORT=true turns it off).

4. Docker Compose: credentials live on the Traefik container

Lego reads provider credentials only from environment variables (a _FILE suffix reads the value from a file instead). These go on the traefik service, not your app services.

services:
  traefik:
    image: traefik:v3
    command:
      - --configFile=/etc/traefik/traefik.yml
      - --log.level=DEBUG              # drop to INFO once issuance works
    environment:
      # Cloudflare, preferred: set CF_DNS_API_TOKEN to an API token
      # with Zone:Read + DNS:Edit on every zone involved.
      # (legacy alternative: CF_API_EMAIL + CF_API_KEY — grants full account access, avoid)
      # Aliases also work: CLOUDFLARE_DNS_API_TOKEN
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - ./acme.json:/letsencrypt/acme.json
    ports:
      - "80:80"
      - "443:443"

Provider env vars (verified against lego docs):

ProviderCodeEnv vars
CloudflarecloudflareCF_DNS_API_TOKEN (preferred; needs Zone:Read + DNS:Edit) or legacy CF_API_EMAIL + CF_API_KEY. Optional split: CF_ZONE_API_TOKEN (Zone:Read only)
Amazon Route 53route53AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION; optional AWS_HOSTED_ZONE_ID to pin the zone. IAM needs route53:ChangeResourceRecordSets, route53:GetChange, route53:ListResourceRecordSets, route53:ListHostedZonesByName

Provider-side propagation tunables (also env vars): CLOUDFLARE_PROPAGATION_TIMEOUT (default 120s), CLOUDFLARE_TTL (default 120s), AWS_PROPAGATION_TIMEOUT (default 120s), AWS_TTL (default 10s).

5. Dynamic config: attach the resolver to a router

Labels on your app service (dynamic config):

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
  - "traefik.http.routers.myapp.entrypoints=websecure"
  - "traefik.http.routers.myapp.tls=true"
  - "traefik.http.routers.myapp.tls.certresolver=dns-resolver"
  - "traefik.http.routers.myapp.tls.domains[0].main=example.com"
  - "traefik.http.routers.myapp.tls.domains[0].sans=*.example.com"

Wildcard + apex note: a cert for *.example.com does not cover the bare example.com. Request both by setting main: example.com with sans: *.example.com, this triggers two DNS-01 challenges, and both write to the same _acme-challenge TXT name. The lego provider table notes which providers handle this; if your provider's TXT TTL exceeds the challenge timeout, the second challenge can fail, raise propagation timeouts or lower the TXT TTL.

6. Debugging unable to obtain certificate

Work this list in order. Do every step against the staging CA first (caServer: https://acme-staging-v02.api.letsencrypt.org/directory) so failed attempts don't count against production rate limits.

  1. Read the actual error: docker logs traefik with --log.level=DEBUG. The log line names the failing piece (provider auth, propagation, ACME order). Don't guess, the provider name and HTTP status are usually in the line.
  2. Is the resolver even loaded? Check the Traefik dashboard / API for the resolver name. If it's missing, the certificatesResolvers block is in the wrong config source (dynamic file instead of static) or has a YAML/TOML syntax error that prevented startup parsing.
  3. Provider auth: 401/403 from the DNS API means wrong or insufficient credentials. Cloudflare: token needs Zone:Read + DNS:Edit on every zone involved; using the Global API Key when you meant a token (or vice versa) fails. Route53: IAM policy missing one of the four actions fails at TXT creation.
  4. TXT propagation: if the log shows the TXT record was created but validation timed out, raise propagation.delayBeforeChecks (e.g. 60s) and/or the provider's *_PROPAGATION_TIMEOUT. Set explicit resolvers (1.1.1.1:53) if the container's DNS is filtered.
  5. Wrong provider code: dnsChallenge.provider must be the exact lego code (cloudflare, not Cloudflare). A wrong code fails at startup with an unknown-provider error.
  6. acme.json problems: file must exist and be writable by the Traefik process; a corrupted file (e.g. from an interrupted write) can be renamed aside and re-issued, but only after the underlying error is fixed, or you'll hit rate limits re-issuing.
  7. Rate limits: Let's Encrypt caps failed validations per hostname per hour and certificates per domain per week. If everything "looks fixed" but issuance still fails, you may be rate-limited, switch to staging and wait it out.
  8. Only one resolver challenge per router: don't set httpChallenge and dnsChallenge on the same resolver and expect both to run; pick one. Wildcard routers must point at the DNS-01 resolver.

Quick failure table

SymptomLikely causeFix
Nothing requested; no log lines about ACMEResolver defined in dynamic config, or router label misspells the resolver nameMove certificatesResolvers to static traefik.yml; match tls.certresolver name exactly
401/403 from DNS APIBad token or missing permissionsCloudflare: token needs Zone:Read + DNS:Edit; Route53: full IAM action set
TXT created, validation times outPropagation slower than checksdelayBeforeChecks: 60s, explicit resolvers, raise provider *_PROPAGATION_TIMEOUT
Wildcard issued but apex domain still insecureOnly *.example.com requestedAdd main: example.com + sans: *.example.com via tls.domains
Worked once, fails on renewStorage not persisted / permissions changedKeep acme.json on a persistent volume with owner-only permissions
All attempts fail after many retriesLet's Encrypt rate limitDebug on staging CA; wait out the limit window

Evidence

All config keys verified against Traefik's official ACME certificate resolver reference: certificatesResolvers as static config, acme.email/storage required, dnsChallenge.provider/resolvers, propagation.delayBeforeChecks as Go duration default 0s, propagation.disableChecks/disableANSChecks/requireAllRNS, tls.domains main/sans for wildcard+apex (two DNS-01 challenges on the same TXT name), HTTP-01 needing port 80 and TLS-ALPN-01 needing port 443, wildcards only via DNS-01, one DNS provider per resolver with CNAME support (LEGODISABLECNAMESUPPORT). Env vars verified on lego provider pages: Cloudflare CFDNSAPITOKEN (Zone:Read + DNS:Edit) vs legacy CFAPIEMAIL/CFAPIKEY, CLOUDFLAREPROPAGATIONTIMEOUT default 120, CLOUDFLARETTL default 120; Route53 AWSACCESSKEYID/AWSSECRETACCESSKEY/AWSREGION/AWSHOSTEDZONEID with the four required IAM actions and AWSPROPAGATION_TIMEOUT default 120. Deprecated v2 flat keys delayBeforeCheck/disablePropagationCheck confirmed via Traefik CLI reference mirrors.

By @issueatlas · Version 2 · Exact version

Maintainer review

Checking maintainer review for this exact version…

A maintainer review records the library version checked. It does not assert that it is the latest upstream release. Reported outcomes are separate evidence.

Embed this skill

Add this card to a website or documentation page. It links to the current skill on Vectle.

<a href="https://vectle.com/skills/skl_MCpszG9xv-9n8HkTB4LHEQ" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Fix Traefik wildcard certificate failures: ACME DNS-01 in Docker <span>— Vectle</span></a>