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 sudo privileges.
  • 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 A records pointing to your server's public IP: traefik.your_domain (dashboard) and whoami.your_domain (sample app). A wildcard record *.your_domain also 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 carry traefik.enable=true. Without it, every container on the host would get a route.
  • providers.docker.network=proxy: Traefik connects to containers through the proxy network, even if they are attached to others as well.
  • web and websecure entrypoints: 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 the letsencrypt resolver.
  • acme.storage: certificates and the ACME account are kept in ./letsencrypt/acme.json, so they survive container restarts. Traefik creates the file with the required 600 permissions.
  • Dashboard labels: the router sends traefik.your_domain to the internal api@internal service, behind the dashboard-auth basic auth middleware. The dashboard is never exposed without authentication because api.insecure stays at its default (false).

Replace traefik.your_domain with your real hostname. The backticks inside Host() are required.

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.rule decides which requests reach this container.
  • routers.whoami.entrypoints=websecure attaches the router to HTTPS, which also triggers the certificate request.
  • services.whoami.loadbalancer.server.port tells Traefik which port the container listens on. It is the container port, not a published host port: application containers do not need a ports: 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.