Docker 88 🐳 Reverse Proxying Docker Containers with Automatic SSL/TLS via Traefik & Let’s Encrypt
Traefik is a reverse proxy that discovers Docker containers through the Docker socket and routes traffic to them based on labels attached to the container itself. The routing configuration is not a separate file that must be edited when a service is added; it is a set of traefik.http.routers.* and traefik.http.services.* labels on the container, and Traefik watches the Docker event stream to reconfigure itself when containers start and stop . Let’s Encrypt integration is built into Traefik through its ACME client: the certificatesResolvers configuration defines an email, a storage file, and a challenge type, and a router label attaches the resolver to the route. Traefik requests the certificate on the first request, stores it in the acme.json file, and renews it automatically before expiration .
The two pieces fit together through the router’s TLS configuration. A router with tls.certresolver=myresolver tells Traefik to obtain a certificate for the host in the router rule and to terminate TLS for that route. The entryPoints.web and entryPoints.websecure entries define the ports, and the HTTP-to-HTTPS redirect is configured on the web entrypoint . The result is a container that is reachable at https://app.example.com with a valid certificate, and the only configuration required is the label set and a shared Docker network.
This chapter covers the Docker provider configuration and its security implications, the ACME certificate resolver and the three challenge types, the label set that routes a container, the shared network requirement that lets Traefik reach the container, and the common failure modes that produce 404s and certificate errors. It assumes familiarity with Docker Compose and DNS basics.
Key point: Traefik routes to containers on a shared Docker network. The container must be on the same network as Traefik, the traefik.enable=true label must be present, and the router rule must match the host. The certificate resolver is attached with tls.certresolver, and the acme.json file must be persisted with chmod 600 or the certificates are lost on restart.
Why Traefik’s Docker provider changes the reverse proxy model
The configuration lives on the container, not in the proxy. A traditional reverse proxy (Nginx, Apache) requires a configuration file that lists every upstream server. Adding a service means editing the file and reloading the proxy. Traefik’s Docker provider inverts this: the container declares its own routing rules through labels, and Traefik discovers them by watching the Docker API . The proxy configuration is distributed across the containers it serves.
The Docker socket is a privileged interface. Traefik needs access to /var/run/docker.sock to list containers and watch events. The socket is the control channel for the Docker daemon, and access to it is effectively root access on the host . The recommended mitigation is a socket proxy that exposes only the CONTAINERS and NETWORKS API endpoints, limiting the damage if Traefik is compromised . The :ro mount flag on the socket is a partial mitigation, but it does not prevent API calls that mutate Docker state.
The ACME client is built in, but the challenge type determines the architecture. HTTP-01 requires port 80 to be reachable from the internet, because Let’s Encrypt validates the domain by fetching a file from http://example.com/.well-known/acme-challenge/. TLS-ALPN-01 requires port 443. DNS-01 requires API credentials for the DNS provider and supports wildcard certificates because the validation is a DNS TXT record, not an HTTP request . The choice depends on the network topology: a public server with port 80 open uses HTTP-01; a private server behind a firewall uses DNS-01; a server with only port 443 open uses TLS-ALPN-01.
The trade-off. Traefik’s label-based configuration is dynamic and requires no reload, but it spreads routing logic across the deployment. A service that is not on the shared network, or that lacks the traefik.enable label, is silently invisible to the proxy. The Docker socket is a security risk that must be mitigated. The ACME challenge type must match the network topology, or certificate issuance fails. The convenience is real, but it depends on a few configuration details that are easy to get wrong.
a. The Docker provider and the shared network
Traefik’s static configuration enables the Docker provider with providers.docker=true. The exposedByDefault=false setting is the recommended default: containers are not routed unless they have the traefik.enable=true label . Without it, every container on the host is exposed, including databases and internal services.
The network setting tells Traefik which Docker network to use when connecting to containers. A container that is not on that network cannot be reached by Traefik, even if the labels are correct. The common pattern is to create a dedicated network (docker network create proxy or traefik-public) and attach both Traefik and the backend containers to it .
# docker-compose.yaml
services:
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--providers.docker.network=proxy"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- proxy
app:
image: my-app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=myresolver"
- "traefik.http.services.app.loadbalancer.server.port=8080"
networks:
- proxy
networks:
proxy:
name: proxy
The loadbalancer.server.port label is required when the container exposes multiple ports or no ports. Traefik uses the lowest exposed port if there is only one, but the explicit label removes the ambiguity .
b. The ACME certificate resolver and the three challenges
The certificatesResolvers block defines the Let’s Encrypt configuration. The email is the account contact, the storage is the path to the acme.json file, and the challenge block selects the validation method .
command:
- "--certificatesresolvers.myresolver.acme.email=admin@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.myresolver.acme.httpChallenge.entryPoint=web"
The acme.json file must be persisted in a volume and must have chmod 600 permissions. Traefik creates the file with the correct permissions on first write, but if the file is created manually with different permissions, the ACME client refuses to use it . The file contains the account private key and the issued certificates; losing it means re-issuing every certificate, which counts against Let’s Encrypt’s rate limits.
HTTP-01 is the simplest challenge. It requires port 80 to be reachable from the internet. Traefik serves the challenge response on the web entrypoint, and the HTTP-to-HTTPS redirect is compatible with the challenge because Traefik handles the challenge path before the redirect . The limitation is that HTTP-01 cannot issue wildcard certificates.
TLS-ALPN-01 uses port 443 instead of port 80. It is useful when port 80 is blocked but port 443 is open. The configuration is tlsChallenge=true instead of httpChallenge .
DNS-01 requires API credentials for the DNS provider. It supports wildcard certificates and works when neither port 80 nor port 443 is reachable from the internet, because the validation is a DNS TXT record. The configuration includes the provider name and the environment variables for the credentials .
c. The label set and the common failure modes
A routed container needs three labels at minimum: traefik.enable=true, a router rule with a Host matcher, and a service port label. A router with a certificate resolver needs tls.certresolver. The entrypoint label restricts the router to HTTPS; without it, the router may match both HTTP and HTTPS, depending on the entrypoint configuration .
| Label | Purpose |
|---|---|
traefik.enable=true | Opt the container into routing |
traefik.http.routers.<name>.rule=Host(...) | Match the host header |
traefik.http.routers.<name>.entrypoints=websecure | HTTPS only |
traefik.http.routers.<name>.tls.certresolver=<name> | Issue/renew certificate |
traefik.http.services.<name>.loadbalancer.server.port=<port> | Target port |
The most common failure is a 404. It means Traefik received the request but no router matched. The causes are a missing traefik.enable label, a host rule that does not match the requested domain, or the container not being on the shared network. The Traefik dashboard shows the routers and their rules, which is the fastest way to diagnose a 404 .
The second most common failure is a certificate error. It means the ACME challenge failed. The causes are port 80 not being reachable (HTTP-01), port 443 not being reachable (TLS-ALPN-01), or DNS credentials missing (DNS-01). The Traefik logs show the ACME error, and the Let’s Encrypt staging server should be used during testing to avoid rate limits .
Complete Example Session
# ============================================
# PART 1: DOCKER COMPOSE WITH TRAEFIK
# ============================================
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--providers.docker.network=proxy"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.email=admin@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.myresolver.acme.httpChallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- proxy
app:
image: my-app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=myresolver"
- "traefik.http.services.app.loadbalancer.server.port=8080"
networks:
- proxy
networks:
proxy:
name: proxy
# ============================================
# PART 2: ACME STORAGE PERMISSIONS
# ============================================
mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json
# ============================================
# PART 3: DNS CHALLENGE FOR PRIVATE SERVERS
# ============================================
# command:
# - "--certificatesresolvers.myresolver.acme.dnsChallenge=true"
# - "--certificatesresolvers.myresolver.acme.dnsChallenge.provider=cloudflare"
# environment:
# - "CF_API_EMAIL=..."
# - "CF_API_KEY=..."
# ============================================
# PART 4: TLS-ALPN CHALLENGE (PORT 443 ONLY)
# ============================================
# command:
# - "--certificatesresolvers.myresolver.acme.tlsChallenge=true"
# ============================================
# PART 5: STAGING SERVER FOR TESTING
# ============================================
# command:
# - "--certificatesresolvers.myresolver.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"
# ============================================
# PART 6: SOCKET PROXY (SECURITY)
# ============================================
# services:
# socket-proxy:
# image: tecnativa/docker-socket-proxy
# environment:
# - CONTAINERS=1
# - NETWORKS=1
# volumes:
# - "/var/run/docker.sock:/var/run/docker.sock:ro"
# traefik:
# command:
# - "--providers.docker.endpoint=tcp://socket-proxy:2375"
# ============================================
# PART 7: DASHBOARD WITH BASIC AUTH
# ============================================
# labels:
# - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
# - "traefik.http.routers.dashboard.service=api@internal"
# - "traefik.http.routers.dashboard.middlewares=auth"
# - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$..."
# ============================================
# PART 8: VERIFICATION
# ============================================
curl -I https://app.example.com
# HTTP/2 200
# certificate valid
# ============================================
# PART 9: TROUBLESHOOTING 404
# ============================================
# Check: traefik.enable=true label present
# Check: container on proxy network
# Check: Host rule matches requested domain
# Check: Traefik dashboard shows router
# ============================================
# PART 10: THE COMPLETE FLOW
# ============================================
# 1. Traefik starts, reads Docker socket.
# 2. App container starts with labels.
# 3. Traefik discovers container, creates router.
# 4. First HTTPS request triggers ACME.
# 5. Certificate stored in acme.json.
# 6. Traefik routes to app on proxy network.
# 7. Renewal is automatic.
Quick Reference
Static Configuration
| Directive | Purpose |
|---|---|
providers.docker=true | Enable Docker provider |
providers.docker.exposedByDefault=false | Require explicit opt-in |
providers.docker.network=proxy | Default network |
entrypoints.web.address=:80 | HTTP entrypoint |
entrypoints.websecure.address=:443 | HTTPS entrypoint |
entrypoints.web.http.redirections.entrypoint.to=websecure | HTTP→HTTPS |
ACME Resolver
| Directive | Purpose |
|---|---|
certificatesresolvers.<name>.acme.email | Account contact |
certificatesresolvers.<name>.acme.storage | acme.json path |
...acme.httpChallenge.entrypoint=web | HTTP-01 |
...acme.tlsChallenge=true | TLS-ALPN-01 |
...acme.dnsChallenge.provider=<name> | DNS-01 |
...acme.caServer=<url> | Staging server |
Container Labels
| Label | Purpose |
|---|---|
traefik.enable=true | Opt in |
traefik.http.routers.<name>.rule=Host(...) | Match host |
traefik.http.routers.<name>.entrypoints=websecure | HTTPS only |
traefik.http.routers.<name>.tls.certresolver=<name> | ACME resolver |
traefik.http.services.<name>.loadbalancer.server.port=<port> | Target port |
Challenge Types
| Challenge | Port | Wildcard | Use When |
|---|---|---|---|
| HTTP-01 | 80 | No | Public server, port 80 open |
| TLS-ALPN-01 | 443 | No | Public server, only 443 open |
| DNS-01 | None | Yes | Private server, or wildcard |
Best Practices
✅ Do This:
# Use exposedByDefault=false. // ✅
"--providers.docker.exposedByDefault=false"
# Persist acme.json with correct permissions. // ✅
touch letsencrypt/acme.json && chmod 600 letsencrypt/acme.json
# Attach Traefik and app to the same network. // ✅
networks: [proxy]
# Use the staging server during testing. // ✅
"--certificatesresolvers.myresolver.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"
# Use a socket proxy to limit Docker API access. // ✅
endpoint: "tcp://socket-proxy:2375"
❌ Don’t Do This:
# Expose every container by default. // ❌
# exposedByDefault=true (default) exposes databases.
# Create acme.json with wrong permissions. // ❌
# ACME client refuses to use it.
# Forget the shared network. // ❌
# Traefik cannot reach the container; 404.
# Use the production Let's Encrypt server for testing. // ❌
# Rate limits lock the domain for a week.
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| 404 from Traefik | No router matches | Check traefik.enable, network, Host rule |
| Certificate not issued | Challenge failed | Check port reachability, logs |
acme.json permissions error | File not 600 | chmod 600 |
| Traefik cannot reach container | Not on shared network | Attach both to proxy |
| Rate-limited by Let’s Encrypt | Production server used for testing | Use staging server |
Real-World Examples
1. Basic Traefik Compose
image: traefik:v3.0
command: ["--providers.docker=true", "--providers.docker.exposedByDefault=false"]
2. HTTP Challenge
"--certificatesresolvers.myresolver.acme.httpChallenge.entrypoint=web"
3. DNS Challenge
"--certificatesresolvers.myresolver.acme.dnsChallenge.provider=cloudflare"
4. App Labels
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.tls.certresolver=myresolver"
5. Target Port
- "traefik.http.services.app.loadbalancer.server.port=8080"
6. HTTP Redirect
"--entrypoints.web.http.redirections.entrypoint.to=websecure"
7. Staging ACME
"--certificatesresolvers.myresolver.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"
8. Socket Proxy
"--providers.docker.endpoint=tcp://socket-proxy:2375"
9. Shared Network
networks: [proxy]
10. ACME Storage
"--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
Visual
Traefik Discovery Flow
┌──────────────────────────────────────────────┐
│ DOCKER HOST │
│ │
│ Traefik container │
│ watches /var/run/docker.sock │
│ │ │
│ ▼ │
│ App container starts with labels │
│ traefik.enable=true │
│ Host(`app.example.com`) │
│ │ │
│ ▼ │
│ Traefik creates router + service │
│ │ │
│ ▼ │
│ Request → router → service → container │
│ │
│ No config file edit. Labels are config. │
│ │
└──────────────────────────────────────────────┘
ACME Challenge
┌──────────────────────────────────────────────┐
│ CERTIFICATE ISSUANCE │
│ │
│ First HTTPS request to app.example.com │
│ │ │
│ ▼ │
│ Traefik has no cert → ACME │
│ │ │
│ ▼ │
│ Let's Encrypt challenges domain │
│ │ │
│ ├── HTTP-01: fetch :80/.well-known │
│ ├── TLS-ALPN-01: TLS handshake :443 │
│ └── DNS-01: check TXT record │
│ │ │
│ ▼ │
│ Certificate issued, stored in acme.json │
│ │ │
│ ▼ │
│ Renewed automatically before expiry │
│ │
└──────────────────────────────────────────────┘
Network Requirement
┌──────────────────────────────────────────────┐
│ SHARED NETWORK │
│ │
│ network: proxy │
│ ├── traefik │
│ ├── app │
│ └── other-service │
│ │
│ Traefik resolves app by container name │
│ on the shared network. │
│ │
│ If app is on a different network: │
│ Traefik cannot reach it. │
│ 404 returned to client. │
│ │
└──────────────────────────────────────────────┘
Security Boundary
┌──────────────────────────────────────────────┐
│ DOCKER SOCKET │
│ │
│ Traefik needs the socket to discover │
│ containers. │
│ │
│ Direct mount: │
│ /var/run/docker.sock → full API access │
│ │
│ Socket proxy: │
│ traefik → proxy:2375 → docker.sock │
│ proxy exposes only CONTAINERS + NETWORKS │
│ │
│ Limits blast radius if Traefik compromised. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Docker provider | providers.docker=true |
| Default exposure | exposedByDefault=false (explicit opt-in) |
| Shared network | providers.docker.network=proxy |
| ACME storage | acme.json, chmod 600 |
| HTTP challenge | Port 80, no wildcard |
| TLS-ALPN challenge | Port 443, no wildcard |
| DNS challenge | DNS API, wildcard support |
| Certificate resolver label | traefik.http.routers.<name>.tls.certresolver |
| HTTP redirect | entrypoints.web.http.redirections.entrypoint.to=websecure |
| Testing | Use Let’s Encrypt staging server |
Key takeaways:
- Traefik discovers containers through labels, not a config file. The
traefik.enable=truelabel opts a container into routing, and thetraefik.http.routers.*labels define the rule and TLS resolver. Traefik watches the Docker event stream and reconfigures itself when containers start and stop . - The container must be on the same network as Traefik. The
providers.docker.networksetting defines the default network, and the container must be attached to it. A container on a different network is invisible to Traefik, producing a 404 . - The
acme.jsonfile must be persisted and protected. It contains the ACME account key and the issued certificates. Losing it means re-issuing every certificate, which counts against Let’s Encrypt rate limits. The file must havechmod 600. - The challenge type must match the network topology. HTTP-01 needs port 80, TLS-ALPN-01 needs port 443, and DNS-01 needs provider credentials. DNS-01 is the only challenge that supports wildcard certificates .
- The Docker socket is a privileged interface. The direct mount gives Traefik full Docker API access. A socket proxy that exposes only the container and network endpoints limits the damage if Traefik is compromised .
- Use the staging server during testing. The production Let’s Encrypt server has rate limits that can lock a domain out for a week after repeated failures. The staging server has much higher limits and is the correct target for initial configuration .
Remember: Traefik with the Docker provider is a dynamic reverse proxy: the routing configuration is the set of labels on the container, and the certificate configuration is the ACME resolver attached to the router. The container must be on the shared network, the labels must include the host rule and the resolver, and the acme.json file must persist across restarts. The Docker socket is a security risk that a socket proxy mitigates. The challenge type is determined by which ports are reachable from the internet. Get these details right, and a new service is a container with three labels and a DNS record. Get them wrong, and the service is a 404 or a certificate error with no obvious cause.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!