Traefik is a reverse proxy that configures itself from the platform it runs on. With the Docker provider, Traefik watches the Docker API, and every container you start with the right labels gets a route, a load balancer entry and a Let's Encrypt certificate, without editing or reloading a proxy configuration file. In this tutorial you will run Traefik v3 with Docker Compose on Ubuntu 24.04, publish a test service over HTTPS with an automatic certificate, redirect HTTP to HTTPS, add a middleware, and expose the Traefik dashboard behind a password.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, such as 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 domain name. This tutorial uses
your_domain, with two DNS A records pointing to your server's public IP:whoami.your_domainandtraefik.your_domain. Let's Encrypt can only issue certificates once these records resolve. - Ports 80 and 443 free on the server (stop any Nginx or Apache already using them).
Step 1 - Opening the firewall
Let's Encrypt validates your domains over port 80, and clients will connect on 443. Allow both, together with SSH, in UFW:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Check the rules:
sudo ufw status
Status: active
To Action From
-- ---- ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
...
NoteDocker publishes container ports with its own iptables rules, which take effect before UFW. Any port you publish with
ports:in Compose is reachable from the Internet even if UFW does not allow it. In this setup only Traefik publishes ports; the application containers are reached exclusively through Traefik.
Step 2 - Creating the proxy network and project directory
Traefik and the containers it routes to must share a Docker network. Create an external network that all your Compose projects can join:
docker network create proxy
Create a directory for the Traefik configuration:
mkdir -p ~/traefik
cd ~/traefik
Step 3 - Writing the static configuration
Traefik has two kinds of configuration. The static configuration is read once at startup and defines entry points (listening ports), providers and certificate resolvers. The dynamic configuration (routers, services, middlewares) can change at runtime; here it comes from Docker labels.
Create the static configuration file:
nano traefik.yml
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
providers:
docker:
exposedByDefault: false
network: proxy
certificatesResolvers:
letsencrypt:
acme:
email: your_email@your_domain
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
api:
dashboard: true
log:
level: INFO
accessLog: {}
What each section does:
entryPoints:weblistens on port 80 and permanently redirects every request towebsecureon 443. Settingtls.certResolveronwebsecuremeans every router attached to it gets a certificate from theletsencryptresolver automatically.providers.docker:exposedByDefault: falsemakes Traefik ignore containers unless they carry thetraefik.enable=truelabel, so you never publish a database by accident.network: proxytells Traefik which network to use to reach containers attached to several networks.certificatesResolvers: uses the ACME HTTP-01 challenge on thewebentry point and stores certificates inacme.json. Replaceyour_email@your_domainwith a real address; Let's Encrypt rejects placeholder domains and uses it for expiry warnings.api.dashboard: enables the dashboard. It is not published anywhere until you add a router for it in Step 5.
Tipwhile testing, add
caServer: https://acme-staging-v02.api.letsencrypt.org/directoryunderacme:to use Let's Encrypt's staging environment. Its certificates are not trusted by browsers, but its rate limits are much higher. Remove the line and deleteletsencrypt/acme.jsonbefore going to production.
Step 4 - Creating a password for the dashboard
The dashboard shows your entire routing configuration, so protect it with HTTP basic authentication. The htpasswd tool is part of apache2-utils:
sudo apt install -y apache2-utils
Generate a bcrypt hash for a user called admin. Docker Compose treats $ as the start of a variable, so every $ in the hash must be doubled when it goes into a label:
htpasswd -nB admin | sed -e 's/\$/\$\$/g'
Enter a strong password twice. The output looks like this:
admin:$$2y$$05$$F61l3S0w5Yf6GKIULvOOC.8/I/N.5Ku9BsxC7a/rQi7cIFU28DPGm
Copy the whole line; you will paste it in the next step.
Step 5 - Running Traefik with Docker Compose
Create the Compose file:
nano compose.yaml
services:
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml: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=admin:$$2y$$05$$replace_with_your_hash"
networks:
proxy:
external: true
Replace traefik.your_domain with your hostname and the whole admin:... value with the line generated in Step 4.
The key parts:
- Traefik reads its static configuration from
/etc/traefik/traefik.yml, which is where the file is mounted. - The Docker socket is mounted read-only. Traefik only needs to read container metadata, but be aware that access to the Docker socket is powerful: never expose Traefik's own API without authentication.
- The
letsencryptdirectory persistsacme.jsonacross container restarts, so certificates are not requested again every time. - The labels define a router that answers for
traefik.your_domainon HTTPS, sends requests to the built-inapi@internalservice (the dashboard), and passes them through thedashboard-authbasic auth middleware first. - The
traefik:v3tag follows the latest v3 release. Pin a specific tag such astraefik:v3.7if you want upgrades to happen only when you change the file.
Start Traefik:
docker compose up -d
Check the logs for configuration errors:
docker logs traefik
... INF Traefik version 3.7.13 built on ...
... INF Starting provider *docker.Provider
... INF Starting provider *acme.Provider
... INF Testing certificate renew... acmeCA=https://acme-v02.api.letsencrypt.org/directory providerName=letsencrypt.acme
Traefik creates letsencrypt/acme.json with 600 permissions, which it requires. Open https://traefik.your_domain/dashboard/ in a browser (the trailing slash matters). After entering the credentials you should see the dashboard, with the dashboard@docker router listed under HTTP Routers. The first request may take a few seconds while the certificate is issued.
Step 6 - Publishing a service through Traefik
To route a new service, you only need to start a container on the proxy network with the right labels. Use traefik/whoami, a tiny web server that echoes request details, as a test backend. Create a separate project directory to show that any Compose project can use Traefik:
mkdir -p ~/whoami
cd ~/whoami
nano compose.yaml
services:
whoami:
image: traefik/whoami
container_name: 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 container publishes no ports. Traefik reaches it on port 80 inside the proxy network, as declared by the loadbalancer.server.port label. That label is required when the image does not declare an exposed port or exposes several.
Start it:
docker compose up -d
Test it from any machine:
curl https://whoami.your_domain
Hostname: 124376c0b6d5
IP: 127.0.0.1
IP: 172.18.0.2
RemoteAddr: 172.18.0.3:48512
GET / HTTP/1.1
Host: whoami.your_domain
User-Agent: curl/8.5.0
X-Forwarded-For: 203.0.113.10
X-Forwarded-Proto: https
X-Real-Ip: 203.0.113.10
The X-Forwarded-* headers show that the request passed through Traefik over HTTPS. Check the HTTP to HTTPS redirect as well:
curl -I http://whoami.your_domain
HTTP/1.1 301 Moved Permanently
Location: https://whoami.your_domain/
Finally, confirm the certificate was issued by Let's Encrypt:
curl -vI https://whoami.your_domain 2>&1 | grep -E 'subject:|issuer:'
* subject: CN=whoami.your_domain
* issuer: C=US; O=Let's Encrypt; CN=R12
Step 7 - Adding a middleware
Middlewares modify requests or responses between the router and the service: authentication, headers, redirects, rate limiting and more. As an example, limit each client IP to an average of 50 requests per second with bursts up to 100. Add these labels to the whoami service in ~/whoami/compose.yaml:
- "traefik.http.middlewares.whoami-ratelimit.ratelimit.average=50"
- "traefik.http.middlewares.whoami-ratelimit.ratelimit.burst=100"
- "traefik.http.routers.whoami.middlewares=whoami-ratelimit"
The first two labels define the middleware, the third attaches it to the router. Several middlewares can be chained with commas, and they run in the order listed. Recreate the container so Docker applies the new labels:
cd ~/whoami
docker compose up -d
Traefik picks up the change within a second, without restarting. In the dashboard, the whoami@docker router now lists whoami-ratelimit@docker under its middlewares. Clients that exceed the limit receive 429 Too Many Requests.
Troubleshooting
404 page not foundfor your domain: no router matched the request. Check that the container hastraefik.enable=true, that theHost()rule matches the exact hostname, and that the container is on theproxynetwork. The dashboard's HTTP Routers page shows what Traefik actually loaded.Gateway TimeoutorBad Gateway: the router matched but Traefik cannot reach the container. Usually the container is on a different network orloadbalancer.server.portpoints to the wrong port.service "whoami" error: port is missingin the logs: the image does not expose a port. Add thetraefik.http.services.<name>.loadbalancer.server.portlabel.- The browser shows
TRAEFIK DEFAULT CERT: certificate issuance failed. Look forUnable to obtain ACME certificateindocker logs traefik. The usual causes are DNS records that do not point to this server yet, port 80 blocked by a firewall in front of the server, or Let's Encrypt rate limits after repeated attempts (use the staging server while debugging). client version 1.24 is too oldin the logs: an old Traefik release cannot talk to a recent Docker Engine. Pull a current image withdocker compose pulland rundocker compose up -d.
Conclusion
You now have Traefik v3 running as the single entry point on ports 80 and 443, redirecting HTTP to HTTPS, requesting Let's Encrypt certificates on demand, and routing any container that joins the proxy network and declares its hostname in labels. The dashboard is available behind basic authentication.
As next steps, add Traefik to your real applications by giving their Compose services the same kind of labels, define shared middlewares such as security headers in a file provider, or switch to the DNS-01 challenge to obtain wildcard certificates.
