Traefik in Docker gives you automatic SSL and host-based routing: you declare a domain in container labels and Traefik requests a Let's Encrypt certificate for it, then renews it on its own. This guide installs Traefik on a Linux VPS, configures the Docker provider and an HTTP-01 certificate resolver, and routes a test app by labels only.
Before you start
You need:
- A Linux VPS running Ubuntu 22.04/24.04, Debian 12 or AlmaLinux/Rocky 8–9, with root or sudo access.
- Docker Engine and the Compose plugin installed. If not, follow Install Docker and Docker Compose on a Linux VPS.
- A domain or subdomain whose A record points to your server's public IP (we use
203.0.113.10andwhoami.example.comin the examples). - TCP ports 80 and 443 open in your firewall. Let's Encrypt validates over port 80, so it must be reachable from the internet.
- A real email address for the ACME account.
Warning: Traefik needs read access to the Docker socket to see container labels. That socket is effectively root on the host. Mount it read-only, never expose the Traefik dashboard publicly without authentication, and keep the container image updated.
How Traefik Docker automatic SSL works
Traefik has two configuration layers. The static configuration (entrypoints, providers, certificate resolvers) is read once at startup from traefik.yml or from command-line flags. The dynamic configuration comes from the Docker provider: Traefik watches the Docker API and rebuilds its routers and services every time a container starts or stops.
When a container carries a label such as traefik.http.routers.whoami.rule=Host(\whoami.example.com\), Traefik creates a router for that host. If the router also has tls.certresolver=letsencrypt, Traefik asks Let's Encrypt for a certificate, answers the HTTP-01 challenge on port 80, and stores the result in acme.json. Renewals happen automatically before expiry.
Create the project directory and network
Create a folder for Traefik and a shared Docker network. Every app that Traefik should reach must join this network.
mkdir -p /opt/traefik && cd /opt/traefik
touch acme.json && chmod 600 acme.json
docker network create proxyThe acme.json file holds your private keys. Traefik refuses to start if the permissions are wider than 600, so set them before the first run.
Tip: Back up acme.json after the first certificate is issued. Restoring it avoids hitting Let's Encrypt rate limits if you rebuild the server.
Write the static configuration
Create /opt/traefik/traefik.yml. This defines the two entrypoints, the Docker provider, and the Let's Encrypt resolver.
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
exposedByDefault: false
network: proxy
certificatesResolvers:
letsencrypt:
acme:
email: you@example.com
storage: /acme.json
httpChallenge:
entryPoint: web
api:
dashboard: trueTwo settings matter most. exposedByDefault: false means a container is only published when it explicitly sets traefik.enable=true, so nothing leaks by accident. network: proxy tells Traefik which network to use when it talks to your containers.
The web entrypoint redirects all plain HTTP traffic to HTTPS, except the ACME challenge path, which Traefik handles internally.
Run Traefik with Docker Compose
Create /opt/traefik/docker-compose.yml:
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
- ./acme.json:/acme.json
networks:
- proxy
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=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
networks:
proxy:
external: trueCheck Docker Hub for the current Traefik tag before you deploy; v3 tracks the latest release in that major line.
Start it and watch the logs:
docker compose up -d
docker compose logs -f traefikYou should see Traefik report the two entrypoints and the Docker provider. Press Ctrl+C to leave the log view.
Warning: The dashboard router above is reachable by anyone who knows the hostname. Add basic auth middleware or restrict it to your own IP before you leave it running.
Route an app by labels
Now deploy a test app. Create /opt/whoami/docker-compose.yml:
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=letsencrypt"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
networks:
proxy:
external: truecd /opt/whoami && docker compose up -dThere is no ports: block — the app is never published directly. Traefik reaches it over the proxy network and proxies the request. The loadbalancer.server.port label tells Traefik which port the container listens on internally; set it to whatever your app uses (3000, 8080, 5000 and so on).
Labels are read when the container starts. After editing them, run docker compose up -d again so the container is recreated.
Check it works
Request the site and look at the response headers:
curl -I https://whoami.example.comYou should get HTTP/2 200 and no certificate warning. Confirm the certificate issuer:
echo | openssl s_client -connect whoami.example.com:443 -servername whoami.example.com 2>/dev/null | openssl x509 -noout -issuer -datesThe issuer should be Let's Encrypt and the dates should be current. You can also confirm the stored certificate:
docker compose -f /opt/traefik/docker-compose.yml exec traefik cat /acme.json | head -c 200Finally, check that HTTP redirects to HTTPS:
curl -I http://whoami.example.comA 301 with a Location: https://... header means the redirect entrypoint is working.
Troubleshooting
- Traefik returns its own 404 page — the router rule does not match the
Hostheader, or the container is not on theproxynetwork. Verify the hostname in the label matches the URL exactly, and rundocker network inspect proxyto confirm the container is attached. - The browser shows Traefik's default self-signed certificate — the certificate was never issued. Port 80 is usually blocked, or DNS does not point at
203.0.113.10yet. Checkdocker compose logs traefikforacmeerrors and confirm withdig +short whoami.example.com. - Traefik exits with a permissions error on
acme.json— the file is not mode600or it was created as a directory by an earlier mount. Runchmod 600 /opt/traefik/acme.json; if it is a directory, delete it and recreate it as an empty file. - 502 Bad Gateway — Traefik found the container but could not connect. The
loadbalancer.server.portlabel is wrong or the app binds to127.0.0.1inside the container instead of0.0.0.0. See Troubleshooting container networking issues: bridge vs host mode in Docker. - "permission denied" reading the Docker socket on AlmaLinux or Rocky — SELinux is blocking the mount. Add the
:zflag to the socket volume, or set the container to run with the socket's group.
Next steps
- Understand how Traefik finds your containers: Docker networks: bridge, host and custom networks explained.
- Move the whole stack into a single file: Docker Compose file explained: services, volumes, networks and ports.
- Keep secrets like ACME emails and API tokens out of your compose files: Manage secrets and environment variables in Docker Compose.
- Prefer a single-binary web server instead? Compare with How to install Caddy web server with automatic HTTPS.
