Traefik v3 Reverse Proxy - Automatic HTTPS and Label-Based Routing for Docker
Status: Active
Last Updated: 2026-08-26
Category: Networking - Phase 3: Reverse Proxies
Prerequisites: tcp-ip-fundamentals, dns-fundamentals, docker-networking
Time: 2-3 hours
Tags: traefik, reverse-proxy, docker, letsencrypt, tls, middleware, rate-limit, basic-auth, security-headers, compose
Summary
Traefik is a modern edge router/reverse proxy that discovers services automatically โ in this guide, from Docker container labels โ and terminates TLS with certificates it obtains and renews itself from Let's Encrypt. This lesson stands up a production-ready Traefik v3 instance with Docker Compose: two entrypoints (HTTP/HTTPS), automatic ACME certs, label-based routing, and the three middlewares almost every deployment needs (rate limiting, basic auth, security headers).
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Explain what a reverse proxy does and why it sits at the edge of your network
- โ Describe Traefik's core model: entrypoints โ routers โ middlewares โ services
- โ Deploy Traefik v3 with Docker Compose and a persistent ACME store
- โ Get automatic Let's Encrypt certificates via the HTTP-01 challenge
- โ Route traffic to containers using Docker labels (no config file edits)
- โ Apply rate limit, basic auth, and security headers middlewares
- โ Expose the dashboard safely (or not at all)
- โ Troubleshoot certificate, routing, and "Gateway Timeout" problems
Table of Contents
- What Is a Reverse Proxy?
- Traefik's Mental Model
- Installing Traefik v3 with Docker Compose
- Automatic Let's Encrypt Certificates
- Routing Containers with Labels
- Common Middlewares
- The Dashboard
- Troubleshooting
- Key Takeaways
- Next Steps
What Is a Reverse Proxy?
A forward proxy acts on behalf of clients going out. A reverse proxy acts on behalf of servers coming in: clients hit one public endpoint, and the proxy decides which internal service actually answers.
Why put one at the edge?
| Concern | Without reverse proxy | With reverse proxy |
|---|---|---|
| TLS | Each app manages its own certs | One component terminates TLS for everything |
| Ports | Apps fight over 80/443 or use ugly :8081 URLs |
Every app gets a clean hostname on standard ports |
| Exposure | Each container publishes ports to the host | Only the proxy is public; backends stay on an internal network |
| Cross-cutting policy | Reimplemented per app | Rate limits, auth, headers applied centrally |
Other well-known reverse proxies are Nginx, Caddy, HAProxy, and Apache httpd. Traefik's differentiator is dynamic configuration discovery: instead of writing server {} blocks by hand, Traefik watches providers (Docker, Kubernetes, files) and builds its routing table live as containers come and go.
Traefik's Mental Model
Every request flows through four concepts (this is the v3 model documented under Routing & Load Balancing):
```text
Client
โ
โผ
EntryPoint (:443) โโ where traffic enters
โผ
Router โโ matches a rule (e.g. Host(`app.example.com`))
โผ
Middleware(s) โโ transforms the request (auth, rate limit, headers)
โผ
Service โโ load-balances to the actual backend (your container)
- Entrypoints: named listeners, typically
webon :80 andwebsecureon :443. - Routers: match rules (
Host,PathPrefix,HeaderRegexp, ...) and attach to an entrypoint. In v3 rule syntax changed slightly vs v2 โ e.g., backtick quoting is required and some matchers were renamed (PathPrefixstill exists;X-Forwarded-For-based logic moved into middleware). - Middlewares: request/response transformers, attached by reference in the router.
- Services: the upstream targets, usually auto-created from the container's port.
Configuration comes from static settings (entrypoints, providers, cert resolvers โ CLI flags or traefik.yml) and dynamic settings (routers/middlewares/services โ labels, files, APIs).
Installing Traefik v3 with Docker Compose
Directory layout:
traefik/
โโโ docker-compose.yml
โโโ traefik/ # mounted as /etc/traefik
โ โโโ traefik.yml # static config
โ โโโ acme.json # created manually; Traefik stores certs here (chmod 600)
โโโ .env # domain + acme email
Static configuration โ traefik.yml
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false # only route containers that opt in with traefik.enable=true
network: proxy # default network to reach backends
certificatesResolvers:
letsencrypt:
acme:
email: "${ACME_EMAIL}"
storage: /etc/traefik/acme.json
httpChallenge:
entryPoint: web
api:
dashboard: true # enable, but keep it OFF the public internet (see below)
log:
level: INFO
accessLog: {}
Two lines matter enormously:
exposedByDefault: falseโ otherwise every container on the host gets a route. Opt in explicitly per service.network: proxyโ tells Traefik which Docker network to use when connecting to backends that are on several networks.
docker-compose.yml
services:
traefik:
image: traefik:v3.3
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- ACME_EMAIL=${ACME_EMAIL}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/acme.json:/etc/traefik/acme.json
networks:
- proxy
networks:
proxy:
name: proxy
.env:
ACME_EMAIL=you@example.com
Bring it up:
mkdir -p traefik && touch traefik/acme.json && chmod 600 traefik/acme.json
docker compose up -d
docker compose logs -f traefik # watch for ACME errors early
Security note: mounting the Docker socket gives Traefik root-equivalent access to the host. Read-only (
:ro) is the minimum; if that bothers you, look at socket proxies (e.g. tecnativa/docker-socket-proxy) or running Traefik outside Docker with a file provider.
Automatic Let's Encrypt Certificates
The pieces wired above do the whole job:
certificatesResolvers.letsencryptdefines an ACME resolver using Let's Encrypt.httpChallenge.entryPoint: webmeans the challenge is answered on port 80 โ so DNS for the hostname must already point at this server, and nothing else may own :80.storage: /etc/traefik/acme.jsonpersists issued certs across restarts. If you forget the file or mount it read-write-less, Traefik will happily re-request certificates until it hits Let's Encrypt rate limits (5 duplicates/week per exact hostname set).
Verify issuance in the logs:
docker compose logs traefik | grep -i acme
Alternatives worth knowing: the TLS-ALPN-01 challenge (works when port 80 is unavailable but 443 is), and the DNS-01 challenge (needed for wildcard certs like *.example.com, requires a supported DNS provider API). All are options under the same acme: block.
For staging tests, add caServer: https://acme-staging-v02.api.letsencrypt.org/directory first so you don't burn production rate limits while debugging.
Routing Containers with Labels
With the provider watching Docker and exposedByDefault: false, enabling routing for any service is pure labels:
services:
whoami:
image: traefik/whoami
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
# Router: match the host, use HTTPS entrypoint, resolve certs
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
# Service: which port inside the container to forward to
- "traefik.http.services.whoami.loadbalancer.server.port=80"
# no "ports:" section needed โ Traefik reaches it over the shared network
networks:
proxy:
external: true
Notes:
- The backend container must join the same
proxynetwork as Traefik, and should not publish its port withports:โ keeping it internal is half the point. - Rule syntax uses backticks around values:
Host(\app.example.com`) && PathPrefix(`/api`)`. - Multiple routers per container are fine (e.g., one for
app.example.com/apiwith auth, one catch-all without).
Test: point DNS at the server, then curl -v https://whoami.example.com โ you should get a valid cert and the whoami response including the X-Forwarded-* headers Traefik added.
Common Middlewares
Middlewares are declared dynamically (labels here) and attached by adding them to a router:
- "traefik.http.routers.<router>.middlewares=<name@docker>"
Rate limiting
Per the RateLimit middleware docs, the token bucket takes an average (steady requests/sec), a period (how often that average refills), and a burst (allowed momentary spike). By default the client is identified by its source IP.
- "traefik.http.middlewares.web-ratelimit.ratelimit.average=10"
- "traefik.http.middlewares.web-ratelimit.ratelimit.period=1s"
- "traefik.http.middlewares.web-ratelimit.ratelimit.burst=20"
Behind another LB/NAT? Set sourceCriterion.ipStrategy.depth or key on a header instead, otherwise everyone shares one bucket.
Basic auth
Generate a hash (note: $ signs need escaping or single-quoting in Compose):
# apt install apache2-utils
htpasswd -nB admin
# New password: ********
# admin:$2y$05$e6kPz0zW... (copy this)
- "traefik.http.middlewares.admin-auth.basicauth.users=admin:$$2y$$05$$e6kPz0zW..."
Double each $ in Compose files (or use ${...}-free single quotes in a dynamic file provider). For anything beyond throwaway protection, prefer ForwardAuth against a real identity provider (Authelia, Authentik, OAuth2 Proxy).
Security headers
The Headers middleware sets response headers:
- "traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.secure-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.secure-headers.headers.browserXssFilter=true"
- "traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.secure-headers.headers.frameDeny=true"
- "traefik.http.middlewares.secure-headers.headers.referrerPolicy=no-referrer-when-downgrade"
Chaining them together
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
- "traefik.http.routers.whoami.middlewares=secure-headers,web-ratelimit"
Order matters: middlewares run left-to-right on requests.
The Dashboard
api.dashboard: true enables it, but never expose it publicly unauthenticated. Safe pattern โ route it through basic auth on an internal hostname:
# On the traefik service itself:
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=admin-auth"
Even safer: bind it to localhost only and open an SSH tunnel when you need it.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
404 page not found |
Router rule doesn't match, or container lacks traefik.enable=true; check the dashboard/router list |
| Certificate not issued | DNS doesn't point here yet, port 80 blocked, or wrong ACME email; check docker compose logs traefik | grep -i acme |
| Hit Let's Encrypt rate limit | You restarted with an empty/unwritable acme.json. Fix permissions (600, correct mount) and test against the staging CA |
502 Bad Gateway |
Backend listening on a different port than server.port, or container isn't on the proxy network |
Gateway Timeout |
Backend app bound to 127.0.0.1 inside its container instead of 0.0.0.0 |
| Middleware ignored | Typo in name, or missing the @docker provider suffix; names must match exactly |
| Labels silently ignored | YAML indentation error, or labels on the wrong service in a multi-service Compose file |
| Dashboard unreachable | You enabled the API but never defined a router to api@internal โ that's by design |
Debug escalation path: log.level: DEBUG in traefik.yml, then read the logs โ Traefik prints exactly why a router was skipped or a challenge failed.
Key Takeaways
- A reverse proxy centralizes TLS, naming, exposure, and edge policy for every backend service.
- Traefik v3's flow is entrypoint โ router โ middlewares โ service; static config bootstraps, labels provide dynamic routes.
- Persist
acme.json, setexposedByDefault: false, and keep backends off published ports on an internal network. - Rate limit + auth + security headers as reusable middlewares covers 90% of edge hardening.
Next Steps
- Wildcard certificates with the DNS-01 challenge once you have a supported DNS provider
- ForwardAuth with Authelia/Authentik for real SSO instead of basic auth
- TCP/UDP routers for non-HTTP services (databases, game servers)
Sources & Related
Sources consulted during research (Traefik v3.3 documentation and the Traefik user guides):
- https://doc.traefik.io/traefik/v3.3/routing/routers/
- https://doc.traefik.io/traefik/v3.3/routing/entrypoints/
- https://doc.traefik.io/traefik/v3.3/providers/docker/
- https://doc.traefik.io/traefik/v3.3/https/acme/ (HTTP/TLS/DNS challenges)
- https://doc.traefik.io/traefik/v3.3/middlewares/http/ratelimit/
- https://doc.traefik.io/traefik/v3.3/middlewares/http/basicauth/
- https://doc.traefik.io/traefik/v3.3/middlewares/http/headers/
- https://doc.traefik.io/traefik/v3.3/user-guides/docker-compose/basic-example/
Related KB articles:
- TCP/IP Fundamentals โ what HTTP/TLS ride on
- DNS Fundamentals โ pointing hostnames at your proxy
- Docker Networking โ how Traefik reaches backend containers
- TLS Configuration โ deeper dive into certificate hygiene
- Firewall Basics โ pair with Firewalls & nftables so only 80/443 face the internet
Change Log
- 2026-08-26: Initial draft created via headless-browser web research session.