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 sudo privileges.
  • 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_domain and traefik.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
...

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: web listens on port 80 and permanently redirects every request to websecure on 443. Setting tls.certResolver on websecure means every router attached to it gets a certificate from the letsencrypt resolver automatically.
  • providers.docker: exposedByDefault: false makes Traefik ignore containers unless they carry the traefik.enable=true label, so you never publish a database by accident. network: proxy tells Traefik which network to use to reach containers attached to several networks.
  • certificatesResolvers: uses the ACME HTTP-01 challenge on the web entry point and stores certificates in acme.json. Replace your_email@your_domain with 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.

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 letsencrypt directory persists acme.json across container restarts, so certificates are not requested again every time.
  • The labels define a router that answers for traefik.your_domain on HTTPS, sends requests to the built-in api@internal service (the dashboard), and passes them through the dashboard-auth basic auth middleware first.
  • The traefik:v3 tag follows the latest v3 release. Pin a specific tag such as traefik:v3.7 if 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 found for your domain: no router matched the request. 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. The dashboard's HTTP Routers page shows what Traefik actually loaded.
  • Gateway Timeout or Bad Gateway: the router matched but Traefik cannot reach the container. Usually the container is on a different network or loadbalancer.server.port points to the wrong port.
  • service "whoami" error: port is missing in the logs: the image does not expose a port. Add the traefik.http.services.<name>.loadbalancer.server.port label.
  • The browser shows TRAEFIK DEFAULT CERT: certificate issuance failed. Look for Unable to obtain ACME certificate in docker 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 old in the logs: an old Traefik release cannot talk to a recent Docker Engine. Pull a current image with docker compose pull and run docker 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.