Skip to content

TutorialsWeb servers

How to run Traefik v3 as a Docker reverse proxy with Let’s Encrypt

Run Traefik v3 with Docker Compose, route containers by labels, get Let’s Encrypt certificates automatically and keep the dashboard and Docker socket safe.

  • Intermediate
  • 40 min read
  • Updated

Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Create the shared proxy network
  3. Step 2 — Create the Traefik project
  4. Step 3 — Start Traefik and read the logs
  5. Step 4 — Allow web traffic in the firewall
  6. Step 5 — Publish an app with labels
  7. Step 6 — Attach apps from other guides
  8. Step 7 — Protect the dashboard (optional)
  9. Step 8 — Understand the Docker socket risk
  10. Back up and restore
  11. Update Traefik
  12. Troubleshooting
  13. 404 page not found
  14. The browser warns about a TRAEFIK DEFAULT CERT certificate
  15. permissions 644 for /letsencrypt/acme.json are too open, please use 600
  16. Bad Gateway or Gateway Timeout
  17. client version 1.24 is too old
  18. Bind for 0.0.0.0:80 failed: port is already allocated
  19. Next steps

Traefik is a reverse proxy built for containers. Instead of a configuration file listing every site, it watches Docker and reads labels on your containers: a container with the right labels gets a route, an HTTPS certificate from Let's Encrypt and traffic, and the route disappears when the container stops. This guide runs Traefik v3 with Docker Compose on a Linux server, redirects HTTP to HTTPS, obtains certificates with the HTTP challenge, publishes an example app with labels, shows how to attach apps from other guides, optionally protects the dashboard, and explains the Docker socket risk, logs, backups and updates.

Prerequisites

Traefik Labs does not publish minimum hardware requirements for Traefik Proxy. The figures below are a conservative starting point for Traefik alone; add what your apps need.

ResourceMinimum (official)Suggested starting point
CPUNot published1 vCPU, shared with your apps
RAMNot published256 MB free for Traefik
DiskNot published1 GB free for the image, certificates and logs

Step 1 — Create the shared proxy network

Traefik reaches your apps over a Docker network that it shares with them. Create it once; every app that Traefik should publish joins it:

Bash
docker network create proxy
docker network ls

Step 2 — Create the Traefik project

Create the project directory and a folder for the certificate store:

Bash
sudo mkdir -p /opt/traefik
sudo chown $USER:$USER /opt/traefik
cd /opt/traefik
mkdir letsencrypt

Put the email address for your Let's Encrypt account in .env:

.env
# /opt/traefik/.env
ACME_EMAIL[email protected]
Bash
chmod 600 /opt/traefik/.env

Create /opt/traefik/compose.yaml. The current major version is Traefik v3; the v3.7 tag follows the 3.7 patch releases:

YAML
services:
  traefik:
    image: traefik:v3.7
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    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.web.http.redirections.entrypoint.permanent=true"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.le.acme.email=${ACME_EMAIL:?set ACME_EMAIL in .env}"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
      - "--log.level=INFO"
      - "--accesslog=true"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy

networks:
  proxy:
    name: proxy
    external: true

What the flags do:

  • Docker provider — providers.docker lets Traefik read container labels. exposedbydefault=false means a container is ignored unless it has the label traefik.enable=true, so nothing is published by accident. providers.docker.network=proxy tells Traefik which network to use when a container is on several.
  • Entry points — web listens on port 80 and permanently redirects every request to websecure on port 443.
  • Certificate resolver — le requests certificates from Let's Encrypt with the HTTP-01 challenge on the web entry point and stores them in /letsencrypt/acme.json, which is the ./letsencrypt folder on the host.
  • Ports — Traefik is the one container that publishes ports to the internet, on 80 and 443.

Step 3 — Start Traefik and read the logs

Bash
docker compose up -d
docker compose ps
docker compose logs -f traefik

docker compose ps should show the container as running with 0.0.0.0:80->80/tcp and 0.0.0.0:443->443/tcp. The log shows the configuration being loaded and the Docker provider connecting. Stop following the log with Ctrl+C. Test the entry points from the server:

Bash
curl -I http://203.0.113.10
curl -kI https://203.0.113.10

The HTTP request returns a permanent redirect to HTTPS. The HTTPS request (with -k, because no real certificate exists yet) returns 404, Traefik's answer when no route matches. That is expected until you add an app.

Step 4 — Allow web traffic in the firewall

Allow SSH, HTTP and HTTPS in ufw (install it first on Debian with sudo apt install ufw):

Bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Be aware that Docker's documentation states that published container ports bypass ufw, because Docker routes the traffic before ufw's rules apply. Ports 80 and 443 of Traefik are reachable even without these rules, and a rule cannot close them. That is why apps behind Traefik publish no ports at all.

Step 5 — Publish an app with labels

The traefik/whoami image is a tiny web server that prints request details, ideal for a first test. Create /opt/whoami/compose.yaml:

Bash
sudo mkdir -p /opt/whoami
sudo chown $USER:$USER /opt/whoami
cd /opt/whoami
YAML
services:
  whoami:
    image: traefik/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.certresolver=le"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"

networks:
  proxy:
    name: proxy
    external: true

The labels define a router named whoami that matches the hostname, listens on the HTTPS entry point and gets its certificate from the le resolver, and a service that sends traffic to port 80 inside the container. Start it and test:

Bash
docker compose up -d
curl -I https://whoami.example.com
curl https://whoami.example.com

The first certificate request takes a few seconds. The HTTPS request then returns HTTP/2 200, and the second command prints the request headers as the container sees them, including X-Forwarded-For and X-Forwarded-Proto: https. Follow docker compose logs -f traefik in /opt/traefik if the certificate does not arrive.

Most app guides in this library publish their app on a 127.0.0.1 port for Caddy or Nginx. With Traefik, make three changes to the app's compose.yaml: remove the ports: entry, add the proxy network, and add labels with a unique router name and the app's container port. For example, for an app listening on port 5678 inside its container:

YAML
services:
  app:
    # keep image, environment and volumes from the app's guide
    networks:
      - default
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls.certresolver=le"
      - "traefik.http.services.myapp.loadbalancer.server.port=5678"

networks:
  proxy:
    name: proxy
    external: true

List default as well: once a service names its networks, Compose no longer attaches it to the project's default network, and the app would lose its connection to its own database. Only the web-facing service needs the proxy network and labels; databases stay on default. Router and service names must be unique across all projects on the server. WebSocket connections work through Traefik without extra settings.

Step 7 — Protect the dashboard (optional)

The API and dashboard are disabled unless you enable them. If you want the dashboard, publish it on its own hostname with basic authentication; never use api.insecure, which opens it without a login. Generate a bcrypt password hash and double every $, so that Compose does not treat it as a variable:

Bash
sudo apt install apache2-utils
htpasswd -nB admin | sed -e 's/\$/\$\$/g'

Then edit the traefik service in /opt/traefik/compose.yaml. Add - "--api.dashboard=true" as a new line at the end of the command list, and add a labels: key at the same level as command, ports and volumes. Paste the output of the htpasswd command into the last label:

YAML
services:
  traefik:
    # command, ports and volumes stay as in Step 2
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=le"
      - "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"

Run docker compose up -d, add a DNS record for traefik.example.com and open https://traefik.example.com/dashboard/. To limit access further, add an IP allow list middleware such as traefik.http.middlewares.dashboard-ip.ipallowlist.sourcerange=198.51.100.7/32 and list both middlewares, separated by a comma.

Step 8 — Understand the Docker socket risk

Traefik reads labels through the Docker API, which is why /var/run/docker.sock is mounted. Traefik's documentation warns that unrestricted access to the Docker API is a security concern: an attacker who takes over Traefik could control Docker and, through it, the host. The :ro flag does not change that; it only stops the container from replacing the socket file.

Reduce the risk in layers:

  • keep Traefik on a current v3 release (see the update section),
  • keep no-new-privileges and do not add other privileges to the Traefik container,
  • for stronger isolation, run a socket proxy such as the one Traefik's documentation mentions, allow it only read requests for containers, and point Traefik at it with --providers.docker.endpoint=tcp://socket-proxy:2375 instead of mounting the socket.

Back up and restore

All state lives in /opt/traefik: compose.yaml, .env and letsencrypt/acme.json with your certificates and ACME account. The app projects keep their own labels, so back them up with the apps. Because acme.json belongs to root with mode 600, use sudo:

Bash
sudo mkdir -p /opt/backups
sudo tar czf /opt/backups/traefik-$(date +%F).tar.gz -C /opt traefik

To restore on a new server with Docker installed, recreate the network, unpack the archive and start Traefik:

Bash
docker network create proxy
sudo tar xzf /opt/backups/traefik-2026-10-09.tar.gz -C /opt
cd /opt/traefik
docker compose up -d

Keep the archive private, because it contains private keys, and copy it off the server.

Update Traefik

Read the release notes on Traefik's GitHub releases page first, take a backup, then pull and recreate:

Bash
cd /opt/traefik
docker compose pull
docker compose up -d
docker compose logs --tail 50 traefik

The v3.7 tag receives patch releases. To move to a newer minor version, change the tag (for example to the next v3.x listed on the releases page) after reading the migration notes in Traefik's documentation. Apps keep running during the update; only the proxy restarts for a moment.

Troubleshooting

404 page not found

Traefik answered, but no router matched. Check that the container has traefik.enable=true (required because exposedbydefault=false), that the Host rule contains exactly the hostname you requested, and that the container runs and is on the proxy network: docker network inspect proxy lists the attached containers.

The browser warns about a TRAEFIK DEFAULT CERT certificate

Traefik could not get a Let's Encrypt certificate and serves its built-in self-signed one. Read docker compose logs traefik in /opt/traefik for the ACME error. Usual causes are DNS records that do not point at the server, an AAAA record without working IPv6, or port 80 blocked by an external firewall. Repeated failures count against Let's Encrypt's limit of 5 failed validations per hostname per hour.

permissions 644 for /letsencrypt/acme.json are too open, please use 600

The certificate store has the wrong mode. Fix it with sudo chmod 600 /opt/traefik/letsencrypt/acme.json and restart Traefik with docker compose restart traefik.

Bad Gateway or Gateway Timeout

Traefik matched the router but cannot reach the container. Make sure the container is on the proxy network and that loadbalancer.server.port is the port the app listens on inside the container, not a host port. If the app is on several networks, the providers.docker.network=proxy flag, or the label traefik.docker.network=proxy, tells Traefik which one to use.

client version 1.24 is too old

An older Traefik release cannot negotiate the API version with a current Docker Engine. Traefik 3.6.1 added automatic API version negotiation; update to a current v3 tag as described above.

Bind for 0.0.0.0:80 failed: port is already allocated

Another web server, such as Nginx, Apache or Caddy, already uses port 80 or 443 on the host. Find it with sudo ss -tlpn 'sport = :80', then stop and disable it before starting Traefik.

Next steps

Frequently asked questions

Why use Traefik instead of Caddy or Nginx?

Traefik reads container labels from Docker and updates its routes on its own when containers start or stop, so you never edit a proxy config file. If most of your apps run as containers and you like that model, Traefik fits well; for a few apps, Caddy on the host is simpler.

Do my apps still need published ports with Traefik?

No. Apps join a shared Docker network with Traefik, and Traefik connects to their container port over that network. Only Traefik publishes ports 80 and 443, which keeps everything else off the internet.

Is mounting the Docker socket read-only enough?

No. The :ro flag only stops the container from replacing the socket file; API calls through it can still control Docker and therefore the host. Keep Traefik updated and, for stronger isolation, put a socket proxy in front of the Docker API that allows only read requests.

Should I enable the Traefik dashboard?

Only if you use it. The API and dashboard are off by default. If you turn the dashboard on, publish it through its own HTTPS router with authentication, never with api.insecure on an open port.

Can Traefik get wildcard certificates?

Yes, but only with the DNS challenge, which needs your DNS provider’s API credentials. This guide uses the HTTP challenge, which issues one certificate per hostname and needs port 80 reachable.

Sources

מחולל סיסמאות

Please confirm