Skip to content

Traefik

Traefik is the reverse proxy every other service sits behind — it terminates TLS and routes requests to the right container. Set it up first.

Verified 29 Jul 2026 against traefik:v3.6

Not exercised: recovering the proxy from a backup. The certificate store carries file permissions that a careless restore flattens, and nothing else is reachable until Traefik is back.

This path uses Cloudflare DNS-01 for a wildcard certificate — it doesn’t require port 80 to be publicly reachable. Needs: a domain with DNS at Cloudflare, and a Cloudflare API token (My Profile → API Tokens → Custom Token, with Zone:DNS:Edit + Zone:Zone:Read).

Don’t use Cloudflare, or don’t want a wildcard? See “Certificate strategy” under Going further — per-domain certificates work with or without Cloudflare.

  1. From core/traefik/:

    Terminal window
    cp .env.example .env
  2. Set these in .env:

    ACME_EMAIL=you@example.com
    TRAEFIK_DASHBOARD_HOST=traefik.example.com
    CF_DNS_API_TOKEN=<your Cloudflare token>
    ACME_WILDCARD_DOMAIN=example.com
  3. Render and start:

    Terminal window
    bash ops/scripts/render.sh
    docker compose up -d
  4. Confirm both containers are healthy:

    Terminal window
    docker compose ps
Terminal window
curl -I https://<your-dashboard-domain>

The dashboard is Tailscale-only by default — connect over your tailnet to reach it.

Check certificate issuance:

Terminal window
docker exec traefik-core cat /etc/traefik/acme/acme.json | jq '.[] | .Certificates[]?.domain // "no certs yet"'

The installation above is IPv4-only, which works for most setups. The failure dual-stack exists to prevent, in this architecture, is Tailscale: it always hands a client both addresses, and if the client connects over IPv6 while Traefik’s network is IPv4-only, Traefik loses the real client address and may wrongly refuse a Tailscale-only service.

That is the case this blueprint has hit, not the only one IPv6 matters for — another VPN that assigns IPv6, or a service reached directly over public IPv6 rather than through a proxy, raises the same question. Traffic arriving through a Cloudflare-proxied (orange-cloud) record is unaffected, because the real address is recovered from a trusted forwarded header instead of from the connection.

No extra setup. Skip this section if you don’t use Tailscale, or haven’t run into this.

The path above is one of three. Two more work too — the only hard constraint is that wildcard certificates require DNS-01, since Let’s Encrypt doesn’t issue wildcards over HTTP-01 at all, regardless of this Blueprint.

One DNS-01 certificate covers *.example.com — what the installation above sets up. Needs a Cloudflare account and token; does not need port 80 reachable from the internet. Best when all your services live under one domain, and avoids registering every subdomain in public Certificate Transparency logs.

These combine, too: keep a wildcard for your main domain and let individual apps opt into their own certificate (either resolver) via the same per-app label.

Cloudflare’s proxy status (orange-cloud vs. DNS-only) is a separate, per-record choice — independent of certificate strategy and resolver, and irrelevant to the HTTP-01 path above.

Terminal window
docker compose logs traefik 2>&1 | grep -iE "acme|cert|challenge" | tail -20

Most first-setup certificate failures are a Cloudflare token missing zone permissions, or a mismatch between ACME_WILDCARD_DOMAIN and the per-app tls.certresolver label state.

Terminal window
# Read release notes first: https://github.com/traefik/traefik/releases
# Bump TRAEFIK_IMAGE in .env, then:
bash ops/scripts/render.sh
docker compose pull
docker compose up -d
docker compose logs traefik 2>&1 | grep -i error # should be empty

Never jump major versions without reading the migration guide — the configuration schema changes.