dom4in.net
← All resources
Guide · Traefik

Put Traefik in front of your containers with automatic HTTPS

Traefik watches the Docker socket, routes by labels, and gets Let's Encrypt certificates on its own — add two labels to any container and it's live on its own subdomain with TLS.

The process

The whole path from containers on random ports to named subdomains with certificates. DNS comes first because Let's Encrypt has to reach you before anything gets issued.

Directions

1Point the DNS records at the host

Let's Encrypt's HTTP challenge only succeeds if the hostname already resolves to your server. Create an A record for each app hostname (app.example.com, api.example.com) — or one wildcard-ish setup with individual records — pointing at the host's public IP, and let it propagate before you start Traefik.

Ports 80 and 443 must reach the machine: open them in the cloud provider's firewall or security group. Port 80 stays open even in an HTTPS-only setup — it's where the challenge and the redirect live.

2Write Traefik's static configuration

Traefik splits config in two: static (entrypoints, the Docker provider, the certificate resolver) set at startup, and dynamic (routing) discovered from container labels. Define two entrypoints — web on :80 redirecting to websecure on :443 — and a certificate resolver using the HTTP challenge with your email for expiry notices.

Set exposedByDefault: false on the Docker provider. With the default of true, every container on the host gets routed automatically, including things you never meant to publish.

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint: { to: websecure, scheme: https }
  websecure:
    address: ":443"
providers:
  docker:
    exposedByDefault: false
certificatesResolvers:
  le:
    acme:
      email: [email protected]
      storage: /letsencrypt/acme.json
      httpChallenge: { entryPoint: web }

3Run the Traefik container

In your compose file give Traefik ports 80 and 443, the config file, a named volume for /letsencrypt (the acme.json certificate store — lose it and you re-issue everything, hit rate limits, and have a bad afternoon), and the Docker socket mounted read-only so it can watch containers.

The socket mount is the trade of this architecture: whoever controls Traefik's container can see Docker. Keep the dashboard off the public internet — bind it to localhost or protect it with auth — and mount the socket :ro.

traefik:
  image: traefik:v3.1
  ports: ["80:80", "443:443"]
  volumes:
    - ./traefik.yml:/etc/traefik/traefik.yml:ro
    - letsencrypt:/letsencrypt
    - /var/run/docker.sock:/var/run/docker.sock:ro

4Route apps with labels

Now any container joins the edge with labels: enable it, give its router a hostname rule, and name the certificate resolver. Traefik picks the change up live — no restart, no config reload. The container needs no published ports at all; Traefik reaches it over the shared Docker network.

If a container listens on a non-obvious port or exposes several, add a loadbalancer.server.port label so Traefik knows which one to hit.

labels:
  - traefik.enable=true
  - traefik.http.routers.app.rule=Host(`app.example.com`)
  - traefik.http.routers.app.tls.certresolver=le
  # only if the port isn't detected:
  - traefik.http.services.app.loadbalancer.server.port=3000

5Confirm certificates and the redirect

Load https://app.example.com — first request can take a few seconds while the certificate is issued, then it's cached in acme.json. Check the padlock shows a real Let's Encrypt certificate, and that plain http:// 301s to https://.

While testing repeatedly, use Let's Encrypt's staging CA (add caServer to the resolver) — the production rate limit is 5 duplicate certificates per week, and burning it on config experiments locks you out of real issuance for days.

Common issues & fixes

Traefik serves its own self-signed default certificate.

Issuance failed. Check Traefik's logs for the ACME error — almost always DNS not pointing at the host yet, or port 80 blocked at the cloud firewall.

404 from Traefik for a container that's running.

With exposedByDefault: false the container needs traefik.enable=true, and the Host() rule must match exactly. Also confirm both containers share a Docker network.

502 Bad Gateway.

Traefik reached the router but not the app — wrong loadbalancer.server.port, or the app binds 127.0.0.1 inside its container instead of 0.0.0.0.

Rate limited by Let's Encrypt.

Too many issuances for the same hostname while testing. Switch to the staging caServer until config is stable, and make sure acme.json is on a volume so certs persist across restarts.

acme.json permissions error on start.

Traefik requires it to be chmod 600. Fix the mode on the volume file and restart.

Works on the host, unreachable from the internet.

The cloud security group or provider firewall isn't passing 80/443. OS-level firewalls (ufw) can also block Docker's published ports depending on setup — check both layers.

Honesty note: articles are drafted with AI assistance, then a human checks each one against the vendor's current setup flow before it publishes — nothing goes live unreviewed. Vendor interfaces still change; if a step looks different, the underlying record is what matters.