Traefik is a reverse proxy built for containers: instead of writing a virtual host for every application, you add labels to a container and Traefik discovers it through the Docker API, creates the route, and requests a Let's Encrypt certificate for it. When the container stops, the route disappears.
In this tutorial you will run Traefik v3 with Docker Compose on Ubuntu 24.04, redirect all HTTP traffic to HTTPS, expose a sample application on its own subdomain with an automatic certificate, protect the Traefik dashboard with a password, load balance across several replicas, and add a rate-limiting middleware.
Prerequisites
To follow this tutorial, you will need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has
sudoprivileges. - Docker Engine and the Docker Compose plugin installed from Docker's official repository. Check with
docker compose version. - A registered domain. This tutorial uses
your_domain; replace it everywhere with yours. - Two DNS
Arecords pointing to your server's public IP:traefik.your_domain(dashboard) andwhoami.your_domain(sample app). A wildcard record*.your_domainalso works. - Ports 80 and 443 reachable from the internet and not used by another web server.
Let's Encrypt validates each domain over port 80, so certificates will only be issued once DNS resolves to this server. Check with dig +short whoami.your_domain before you start.
Step 1 - Creating the shared Docker network
Traefik can only reach containers that share a network with it. Create a dedicated network that Traefik and every application you publish will join:
docker network create proxy
Confirm it exists:
docker network ls --filter name=proxy
NETWORK ID NAME DRIVER SCOPE
3f1c2b7a9d10 proxy bridge local
Keeping application containers on this network, and their databases on a separate internal network, means Traefik never has a path to your databases.
Step 2 - Generating the dashboard password
The Traefik dashboard shows every router, service and middleware, so it must not be public. You will protect it with HTTP basic authentication. Install htpasswd:
sudo apt update
sudo apt install -y apache2-utils
Create the project directory and generate a bcrypt hash for the user admin. The command prompts for the password:
sudo mkdir -p /opt/traefik
sudo chown "$USER": /opt/traefik
htpasswd -nB admin
New password:
Re-type new password:
admin:$2y$05$Qm2fN0l3cZrVbVv0bM9u4eXg0hIYJ6q2k0bq8wT1r6qS0xk4mZ5yK
Store the full admin:... line in an .env file next to the Compose file. Single quotes are important: they stop Docker Compose from interpreting the $ characters in the hash.
nano /opt/traefik/.env
TRAEFIK_DASHBOARD_USERS='admin:$2y$05$Qm2fN0l3cZrVbVv0bM9u4eXg0hIYJ6q2k0bq8wT1r6qS0xk4mZ5yK'
ACME_EMAIL=you@your_domain
Replace the hash with your own output and ACME_EMAIL with an address where Let's Encrypt can send expiry warnings. Restrict the file:
chmod 600 /opt/traefik/.env
Step 3 - Writing the Traefik Compose file
Traefik has two kinds of configuration. Static configuration (entrypoints, providers, certificate resolvers) is read at startup; here it is passed as command-line flags. Dynamic configuration (routers, services, middlewares) changes at runtime; with the Docker provider it comes from container labels.
Create the Compose file:
nano /opt/traefik/compose.yaml
services:
traefik:
image: traefik:v3.7
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
- --entrypoints.websecure.http.tls.certresolver=letsencrypt
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
- --api.dashboard=true
- --accesslog=true
- --log.level=INFO
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.your_domain`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=${TRAEFIK_DASHBOARD_USERS}
networks:
proxy:
external: true
What the important flags do:
exposedbydefault=false: Traefik ignores containers unless they carrytraefik.enable=true. Without it, every container on the host would get a route.providers.docker.network=proxy: Traefik connects to containers through theproxynetwork, even if they are attached to others as well.webandwebsecureentrypoints: port 80 only redirects to HTTPS (Let's Encrypt HTTP challenges are still answered on it), and every router on port 443 gets a certificate from theletsencryptresolver.acme.storage: certificates and the ACME account are kept in./letsencrypt/acme.json, so they survive container restarts. Traefik creates the file with the required600permissions.- Dashboard labels: the router sends
traefik.your_domainto the internalapi@internalservice, behind thedashboard-authbasic auth middleware. The dashboard is never exposed without authentication becauseapi.insecurestays at its default (false).
Replace traefik.your_domain with your real hostname. The backticks inside Host() are required.
NoteMounting the Docker socket, even read-only, gives Traefik full access to the Docker API. For production, consider placing a socket proxy that only allows read access to containers between Traefik and the socket.
Step 4 - Starting Traefik
Start the container:
cd /opt/traefik
docker compose up -d
Check that it is running and read its startup logs:
docker compose ps
docker compose logs traefik
NAME IMAGE COMMAND SERVICE STATUS PORTS
traefik traefik:v3.7 "/entrypoint.sh --pr…" traefik Up 8 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
Any level=error line mentioning ACME points to a DNS or firewall problem (see Troubleshooting). Test the HTTP to HTTPS redirect:
curl -I http://traefik.your_domain
HTTP/1.1 308 Permanent Redirect
Location: https://traefik.your_domain/
Now open https://traefik.your_domain/dashboard/ in your browser (the trailing slash matters). The browser asks for the user and password from Step 2, and the certificate is issued by Let's Encrypt. The first request can take a few seconds while the certificate is obtained.
Step 5 - Publishing an application with labels
Deploy traefik/whoami, a tiny web server that prints details about the request and the container that handled it. Each application gets its own directory and Compose file:
sudo mkdir -p /opt/whoami
sudo chown "$USER": /opt/whoami
nano /opt/whoami/compose.yaml
services:
whoami:
image: traefik/whoami
restart: unless-stopped
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.whoami.rule=Host(`whoami.your_domain`)
- traefik.http.routers.whoami.entrypoints=websecure
- traefik.http.services.whoami.loadbalancer.server.port=80
networks:
proxy:
external: true
The three routing labels are all you need for any application:
routers.whoami.ruledecides which requests reach this container.routers.whoami.entrypoints=websecureattaches the router to HTTPS, which also triggers the certificate request.services.whoami.loadbalancer.server.porttells Traefik which port the container listens on. It is the container port, not a published host port: application containers do not need aports:section at all.
Start it:
cd /opt/whoami
docker compose up -d
Send a request:
curl https://whoami.your_domain
Hostname: 5a3e1c9b7f21
IP: 127.0.0.1
IP: 172.18.0.3
RemoteAddr: 172.18.0.2:49812
GET / HTTP/1.1
Host: whoami.your_domain
X-Forwarded-For: 203.0.113.45
X-Forwarded-Proto: https
X-Real-Ip: 203.0.113.45
Traefik added the X-Forwarded-* headers, so the application sees the real client IP and knows the original request was HTTPS. The new router and service also appear on the dashboard under HTTP.
Step 6 - Load balancing across replicas
When several containers belong to the same Compose service, Traefik registers all of them as servers of one service and balances requests across them in round robin. Scale whoami to three replicas:
cd /opt/whoami
docker compose up -d --scale whoami=3
Send a few requests and look at the Hostname line, which is the container ID:
for i in 1 2 3 4 5 6; do curl -s https://whoami.your_domain | grep Hostname; done
Hostname: 5a3e1c9b7f21
Hostname: 91d0c4e2a6b8
Hostname: e7f25a0b3c44
Hostname: 5a3e1c9b7f21
Hostname: 91d0c4e2a6b8
Hostname: e7f25a0b3c44
No Traefik configuration changed: it picked up the new containers from Docker events. Scaling back down with --scale whoami=1 removes them just as quickly. To keep traffic away from a replica that is running but not healthy, add a HEALTHCHECK to the image or a healthcheck: block in Compose; Traefik skips containers whose health status is unhealthy.
Step 7 - Adding a rate-limiting middleware
Middlewares modify requests before they reach the service. They are declared with labels and then attached to a router. Add a rate limit of 20 requests per second per client IP, with bursts of up to 50, to whoami. Open the Compose file:
nano /opt/whoami/compose.yaml
Add these two lines to the labels list to define the middleware:
- traefik.http.middlewares.whoami-ratelimit.ratelimit.average=20
- traefik.http.middlewares.whoami-ratelimit.ratelimit.burst=50
And this line to attach it to the router:
- traefik.http.routers.whoami.middlewares=whoami-ratelimit
Apply the change. Compose recreates the containers because their labels changed:
docker compose up -d --scale whoami=3
Send 100 requests in parallel and count the status codes. The requests above the limit get 429 Too Many Requests:
seq 100 | xargs -P 50 -I{} curl -s -o /dev/null -w "%{http_code}\n" https://whoami.your_domain | sort | uniq -c
57 200
43 429
To attach several middlewares to a router, list them separated by commas, for example traefik.http.routers.whoami.middlewares=whoami-ratelimit,dashboard-auth. Middlewares defined on one container are usable from any other, so you can define common ones (security headers, authentication) once on the Traefik container.
Troubleshooting
The browser shows TRAEFIK DEFAULT CERT: Traefik could not obtain a Let's Encrypt certificate. Run docker compose logs traefik | grep -i acme in /opt/traefik. The usual causes are a DNS record that does not point to this server yet, or port 80 blocked by a firewall in front of the server.
404 page not found for your application: no router matches. Check that the container has traefik.enable=true, that the Host() rule matches the exact hostname, and that the container is on the proxy network (docker network inspect proxy).
502 Bad Gateway: Traefik found the container but cannot connect to it. The loadbalancer.server.port label is usually wrong; it must be the port the application listens on inside the container.
Gateway Timeout with a container on several networks: Traefik picked an IP on a network it is not part of. Make sure --providers.docker.network=proxy is set, or add the label traefik.docker.network=proxy to that container.
Firewall rules seem ignored: ports published by Docker bypass UFW because Docker manages its own iptables rules. Only publish ports on the Traefik container, and let application containers stay reachable exclusively through the proxy network.
Conclusion
You now have Traefik v3 handling HTTPS for your Docker containers: new applications only need a few labels and the proxy network to get a route and a Let's Encrypt certificate, replicas are load balanced automatically, and the dashboard is protected with a password.
As next steps, you can define shared middlewares such as security headers on the Traefik container, put a Docker socket proxy between Traefik and the Docker API, and switch to a DNS challenge if you need wildcard certificates or servers that are not reachable on port 80.
