# Traefik v3 + Let's Encrypt: automatic HTTPS with Docker

> Configure Traefik v3 to get Let's Encrypt certificates automatically: certificate resolver, acme.json, Docker labels, HTTPS redirect, wildcards.

Source: https://ssl.sh/guides/traefik-lets-encrypt/
Last updated: 2026-11-10

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`:

```yaml
# /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:

```yaml
# /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:

```bash
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:

```yaml
certificatesResolvers:
  le:
    acme:
      email: admin@example.com
      storage: /letsencrypt/acme.json
      dnsChallenge:
        provider: cloudflare
```

```yaml
    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](https://ssl.sh/guides/lets-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](https://ssl.sh/tools/ssl-checker/), or from any terminal:

```bash
curl ssl.sh/example.com
```

On the host:

```bash
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](https://ssl.sh/monitor/) (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](https://ssl.sh/errors/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](https://ssl.sh/errors/certbot-timeout-during-connect/).
- **too many certificates already issued**: `acme.json` was not persisted between restarts; see [Let's Encrypt rate limits](https://ssl.sh/errors/lets-encrypt-rate-limit/).
- **router uses a nonexistent resolver**: the name in `tls.certresolver` differs from the one in `certificatesResolvers`.
