Reverse Proxy Multiple Docker Services with Traefik (Automatic HTTPS)
This tutorial shows how to put multiple Dockerized services behind a single Traefik reverse proxy with automatic HTTPS (Let’s Encrypt). You’ll learn the “why” and the “how”, run real commands, and end with a working setup you can extend safely.
What you’re building (high level)
You will run:
- Traefik as the edge router (reverse proxy) that:
- Listens on ports 80 (HTTP) and 443 (HTTPS)
- Automatically requests and renews Let’s Encrypt certificates
- Routes requests to the correct container based on hostnames (e.g.,
whoami.example.com,app.example.com)
- Multiple services (examples in this tutorial):
traefik/whoami(a tiny demo web service)nginx(as a second service)
Routing will be configured via Docker labels on each container, so Traefik discovers services dynamically.
Prerequisites
1) A server reachable from the internet
You need a VPS or server with a public IP. This tutorial assumes Linux (Ubuntu/Debian-like), but it works similarly elsewhere.
2) DNS records for your domains
Create DNS A (or AAAA) records pointing to your server IP:
traefik.example.com→ your server IP (optional, for Traefik dashboard)whoami.example.com→ your server IPnginx.example.com→ your server IP
You can use any domain you control. Replace example.com throughout.
3) Open firewall ports
Traefik must be reachable on:
- TCP 80 (HTTP)
- TCP 443 (HTTPS)
Example with UFW:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status
4) Install Docker and Docker Compose plugin
On Ubuntu:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify:
docker version
docker compose version
Why Traefik for multiple Docker services?
A reverse proxy sits in front of your apps and:
- Terminates TLS (HTTPS) so your apps can run plain HTTP internally
- Routes traffic by hostname/path to the correct backend
- Centralizes logging and security headers
- Avoids exposing every container port to the internet
Traefik is particularly good for Docker because it can watch the Docker API and automatically configure routes based on container labels. You don’t have to manually edit proxy configs every time you add a service.
Choose an HTTPS challenge type (HTTP-01 vs DNS-01)
Let’s Encrypt needs to validate you control the domain.
HTTP-01 challenge (used in this tutorial)
- Let’s Encrypt calls
http://yourdomain/.well-known/acme-challenge/...on port 80. - Traefik answers the challenge automatically.
- Requirements:
- Port 80 must be reachable from the internet.
- Works well for most simple VPS setups.
DNS-01 challenge (not used here)
- You create DNS TXT records via API.
- Useful if port 80 is blocked or you want wildcard certs (
*.example.com). - Requires DNS provider API credentials.
We’ll use HTTP-01 because it’s simplest.
Directory layout
Create a working directory:
mkdir -p ~/traefik-docker
cd ~/traefik-docker
We’ll create:
compose.yml(Traefik + services)letsencrypt/acme.json(certificate storage)
Create the ACME storage file (important)
Traefik stores issued certificates in a file. It must have strict permissions.
mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json
Why chmod 600?
- The ACME file contains private key material.
- Traefik will refuse to use it if it’s too permissive.
The complete Docker Compose setup
Create compose.yml:
nano compose.yml
Paste the following (replace domains and email):
services:
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
command:
# Enable Docker provider (watch containers/labels)
- --providers.docker=true
- --providers.docker.exposedbydefault=false
# EntryPoints: HTTP and HTTPS
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# Redirect all HTTP -> HTTPS
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
# Let's Encrypt (ACME) via HTTP-01 challenge on entrypoint web (port 80)
- --certificatesresolvers.le.acme.email=admin@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
# (Optional but recommended) Logs
- --log.level=INFO
- --accesslog=true
# (Optional) Traefik dashboard/API (we will secure it with a router rule)
- --api.dashboard=true
ports:
- "80:80"
- "443:443"
volumes:
# Read Docker socket so Traefik can discover containers
- /var/run/docker.sock:/var/run/docker.sock:ro
# Persist certs
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
# Enable Traefik for this container (dashboard router)
- traefik.enable=true
# Router for Traefik dashboard
- traefik.http.routers.traefik.rule=Host(`traefik.example.com`)
- traefik.http.routers.traefik.entrypoints=websecure
- traefik.http.routers.traefik.tls=true
- traefik.http.routers.traefik.tls.certresolver=le
# The dashboard is served by Traefik's internal service "api@internal"
- traefik.http.routers.traefik.service=api@internal
# IMPORTANT: Add auth in real deployments (see section below)
# - traefik.http.routers.traefik.middlewares=traefik-auth
whoami:
image: traefik/whoami:v1.10
container_name: whoami
restart: unless-stopped
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.whoami.rule=Host(`whoami.example.com`)
- traefik.http.routers.whoami.entrypoints=websecure
- traefik.http.routers.whoami.tls=true
- traefik.http.routers.whoami.tls.certresolver=le
- traefik.http.services.whoami.loadbalancer.server.port=80
nginx:
image: nginx:1.27-alpine
container_name: nginx
restart: unless-stopped
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.nginx.rule=Host(`nginx.example.com`)
- traefik.http.routers.nginx.entrypoints=websecure
- traefik.http.routers.nginx.tls=true
- traefik.http.routers.nginx.tls.certresolver=le
- traefik.http.services.nginx.loadbalancer.server.port=80
networks:
proxy:
name: proxy
Explanation of the key parts
--providers.docker.exposedbydefault=false
This is a safety feature: Traefik will not expose every container automatically. Only containers with traefik.enable=true are routed publicly.
EntryPoints
webis port 80websecureis port 443
Traefik uses entrypoints like named “listeners”. Routers attach to entrypoints.
HTTP → HTTPS redirect
These two lines:
--entrypoints.web.http.redirections.entrypoint.to=websecure--entrypoints.web.http.redirections.entrypoint.scheme=https
force all HTTP traffic to redirect to HTTPS. This is convenient and also helps avoid accidentally serving insecure content.
ACME resolver le
The resolver name le is referenced in labels:
traefik.http.routers.<router>.tls.certresolver=le
That tells Traefik: “If this router needs TLS, use the le resolver to obtain a certificate.”
Docker labels: routers and services
Traefik concepts:
- Router: matches incoming requests (Host, Path, headers) and decides where to send them.
- Service: the backend (container port(s)) to forward to.
- Middleware: optional transformations (auth, redirects, headers, rate limits).
In Docker, labels define these objects.
Example:
traefik.http.routers.whoami.rule=Host(\whoami.example.com`)`
Matches only that hostname.traefik.http.services.whoami.loadbalancer.server.port=80
Sends traffic to port 80 inside the container.
Start everything
From ~/traefik-docker:
docker compose up -d
Check status:
docker compose ps
docker logs traefik --tail=200
You should see Traefik starting, discovering containers, and attempting ACME certificate issuance when you first access a hostname.
Test the routes
1) Test HTTP redirect to HTTPS
Run from your local machine (or the server):
curl -I http://whoami.example.com
You should see a 301 or 308 redirect to https://whoami.example.com.
2) Test HTTPS and certificate issuance
Now:
curl -I https://whoami.example.com
If DNS is correct and ports are open, Traefik should obtain a certificate and you’ll see HTTP/2 200 or similar.
Open in a browser:
https://whoami.example.comhttps://nginx.example.com
For whoami, you’ll see request details (host, headers, IP). For nginx, you’ll see the default Nginx welcome page.
Understanding how Traefik picks the right container
When a request comes in:
- Client connects to
:443(entrypointwebsecure). - Traefik reads SNI / Host header and matches routers:
- Host
whoami.example.com→ routerwhoami - Host
nginx.example.com→ routernginx
- Host
- Router forwards to its configured service (load balancer).
- Traefik proxies the request over the Docker network
proxyto the container’s internal port.
Because all services share the same Docker network (proxy), Traefik can reach them by container IP internally without exposing their ports to the host.
Securing the Traefik dashboard (do not skip in real use)
The dashboard is useful but should not be publicly open without protection.
Option A: Basic Auth middleware (simple)
Install apache2-utils to generate a password hash:
sudo apt-get update
sudo apt-get install -y apache2-utils
Generate credentials (pick a username):
htpasswd -nb admin 'your-strong-password'
This prints something like:
admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/
Now add a middleware label to Traefik service. Edit compose.yml and under traefik: labels, add:
- traefik.http.middlewares.traefik-auth.basicauth.users=admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/
- traefik.http.routers.traefik.middlewares=traefik-auth
Be careful with special characters. If you run into parsing issues, you can wrap the whole label value in quotes.
Apply changes:
docker compose up -d
Now visit:
https://traefik.example.com
You should be prompted for credentials.
Option B: IP allowlist (good additional control)
If you only want access from your IP, you can add:
- traefik.http.middlewares.traefik-ipallow.ipallowlist.sourcerange=203.0.113.10/32
- traefik.http.routers.traefik.middlewares=traefik-auth,traefik-ipallow
(Replace with your real public IP.)
Adding a new service (pattern you will reuse)
To add another Docker service behind Traefik:
- Attach it to the
proxynetwork. - Add labels:
traefik.enable=true- router rule (usually Host)
- entrypoints
websecure - TLS + certresolver
- service port
Example with a fictional app listening on port 3000:
myapp:
image: yourorg/myapp:latest
restart: unless-stopped
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.myapp.rule=Host(`myapp.example.com`)
- traefik.http.routers.myapp.entrypoints=websecure
- traefik.http.routers.myapp.tls=true
- traefik.http.routers.myapp.tls.certresolver=le
- traefik.http.services.myapp.loadbalancer.server.port=3000
Then:
docker compose up -d
No Traefik restart is required beyond container recreation; Traefik watches Docker events and updates routing dynamically.
Common troubleshooting (with real checks)
1) “My certificate isn’t issued”
Check Traefik logs:
docker logs traefik --tail=300
Common causes:
- DNS doesn’t point to your server
- Port 80 blocked (HTTP-01 challenge needs it)
- Another service is already bound to port 80/443 on the host
Verify DNS:
dig +short whoami.example.com A
Verify ports are listening:
sudo ss -tulpn | grep -E ':80|:443'
You should see docker-proxy / traefik bound.
2) “404 Not Found” from Traefik
A Traefik 404 usually means no router matched the request.
Check:
- Hostname in browser matches exactly the label rule
- You’re hitting the correct entrypoint (HTTPS vs HTTP)
- Container labels are applied (container recreated after edits)
Inspect labels:
docker inspect whoami --format '{{json .Config.Labels}}' | jq
(Install jq if needed: sudo apt-get install -y jq.)
3) “Bad Gateway” (502)
This means Traefik matched a router but can’t reach the backend.
Common causes:
- Wrong internal port in
loadbalancer.server.port - Container not on the same network as Traefik
- App not listening on expected interface/port
Check container is healthy/running:
docker ps
docker logs nginx --tail=100
Check network attachment:
docker network inspect proxy | jq '.[0].Containers | keys'
4) Let’s Encrypt rate limits
If you repeatedly request certs for the same domain while debugging, you can hit rate limits.
Mitigation:
- Use Let’s Encrypt staging environment while testing.
- Then switch to production.
To use staging, add:
- --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
After you confirm routing works, remove it and delete acme.json (so it re-issues production certs):
docker compose down
rm -f letsencrypt/acme.json
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json
docker compose up -d
Operational tips (keeping it stable)
Updating images safely
Pull and recreate:
docker compose pull
docker compose up -d
docker image prune -f
Backups
Back up at least:
compose.ymlletsencrypt/acme.json(contains certs/keys)
Example:
tar -czf traefik-backup.tar.gz compose.yml letsencrypt/acme.json
Don’t publish service ports
Notice we did not use ports: for whoami or nginx. Only Traefik publishes ports 80/443. This reduces attack surface.
Optional: Add security headers (recommended)
You can define a middleware that adds common security headers and apply it to routers.
Add labels to the traefik service (or any container) to define a middleware:
- traefik.http.middlewares.secure-headers.headers.framedeny=true
- traefik.http.middlewares.secure-headers.headers.contenttypenosniff=true
- traefik.http.middlewares.secure-headers.headers.referrerpolicy=no-referrer
- traefik.http.middlewares.secure-headers.headers.permissionspolicy=geolocation=(), microphone=()
- traefik.http.middlewares.secure-headers.headers.stsseconds=31536000
- traefik.http.middlewares.secure-headers.headers.stsincludesubdomains=true
- traefik.http.middlewares.secure-headers.headers.stspreload=true
Then apply to a router, e.g. whoami:
- traefik.http.routers.whoami.middlewares=secure-headers
Recreate:
docker compose up -d
Note: HSTS (stsseconds) tells browsers to prefer HTTPS for a long time. Only enable it when you are sure HTTPS is working reliably.
Optional: Route multiple apps on one domain using paths
Host-based routing is simplest, but you can also do:
example.com/app1example.com/app2
Example router rule:
- traefik.http.routers.app1.rule=Host(`example.com`) && PathPrefix(`/app1`)
Be aware many apps assume they run at / and may need base-path configuration or middleware to strip prefixes.
Full verification checklist
- DNS records point to your server IP:
dig +short whoami.example.com A - Ports open and Traefik listening:
sudo ss -tulpn | grep -E ':80|:443' - Containers running:
docker compose ps - HTTP redirects to HTTPS:
curl -I http://nginx.example.com - HTTPS works and cert is valid:
curl -I https://nginx.example.com - Traefik logs show successful ACME issuance:
docker logs traefik --tail=200 | grep -i acme
Conclusion
You now have a working Traefik reverse proxy that:
- Automatically discovers Docker containers via labels
- Routes multiple services by hostname
- Forces HTTPS and obtains/renews Let’s Encrypt certificates automatically
- Keeps backend services private on an internal Docker network
From here, you can add:
- Authentication (Basic Auth, OAuth forward auth)
- Rate limiting
- WAF integration
- DNS-01 for wildcard certs
- Monitoring (Prometheus metrics) and centralized logs
If you share your target domains and the list of services/ports you want to expose, I can adapt the compose file and labels to your exact setup.