Docker & DevOps

Traefik in Docker: automatic SSL and routing by container labels

Run Traefik as a Docker container, get free Let's Encrypt certificates automatically, and route traffic to your apps by adding a few labels to each container.

AdvancedLinux server6 min read

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.10 and whoami.example.com in 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.

Bash
mkdir -p /opt/traefik && cd /opt/traefik
touch acme.json && chmod 600 acme.json
docker network create proxy

The 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.

YAML
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: true

Two 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:

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
      - ./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: true

Check 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:

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

You 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:

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=letsencrypt"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"

networks:
  proxy:
    external: true
Bash
cd /opt/whoami && docker compose up -d

There 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:

Bash
curl -I https://whoami.example.com

You should get HTTP/2 200 and no certificate warning. Confirm the certificate issuer:

Bash
echo | openssl s_client -connect whoami.example.com:443 -servername whoami.example.com 2>/dev/null | openssl x509 -noout -issuer -dates

The issuer should be Let's Encrypt and the dates should be current. You can also confirm the stored certificate:

Bash
docker compose -f /opt/traefik/docker-compose.yml exec traefik cat /acme.json | head -c 200

Finally, check that HTTP redirects to HTTPS:

Bash
curl -I http://whoami.example.com

A 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 Host header, or the container is not on the proxy network. Verify the hostname in the label matches the URL exactly, and run docker network inspect proxy to 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.10 yet. Check docker compose logs traefik for acme errors and confirm with dig +short whoami.example.com.
  • Traefik exits with a permissions error on acme.json — the file is not mode 600 or it was created as a directory by an earlier mount. Run chmod 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.port label is wrong or the app binds to 127.0.0.1 inside the container instead of 0.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 :z flag to the socket volume, or set the container to run with the socket's group.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.