How to Deploy Traefik v3 as a Docker Reverse Proxy with Automatic HTTPS (Let’s Encrypt)

Overview

Traefik v3 is a modern reverse proxy and load balancer that integrates smoothly with Docker, automatically discovering containers and routing traffic to them. In this guide, you will deploy Traefik v3 as a Docker reverse proxy with automatic HTTPS using Let’s Encrypt via the DNS challenge (Cloudflare as an example). The setup is fast, reproducible, and ideal for hosting multiple apps on a single server with clean domain names and valid TLS certificates.

Prerequisites

Before you start, you will need: a Linux server (Ubuntu 22.04/24.04 works great) with root or sudo access, Docker and Docker Compose installed, a domain name you control (e.g., example.com), ports 80 and 443 open to the server, and an API token from your DNS provider (Cloudflare in this tutorial) with permission to manage DNS (Zone:DNS:Edit). Create A or AAAA records for the subdomains you plan to use (e.g., traefik.example.com, app.example.com) pointing to your server’s public IP.

Step 1: Create a dedicated Docker network

Why: Keeping a shared “proxy” network lets Traefik see and route to other containers without exposing them on the host. Run:

docker network create proxy

Step 2: Prepare your working directory

Create a folder for Traefik and a place to store Let’s Encrypt certificates. The ACME store must be persistent across restarts.

mkdir -p ~/traefik/letsencrypt
cd ~/traefik

Step 3: Create a .env file for secrets

To avoid hardcoding tokens in your Compose file, use a .env file in the same directory. Replace values with your own.

CF_DNS_API_TOKEN=your_cloudflare_dns_token_here
[email protected]
TRAEFIK_DOMAIN=traefik.example.com
APP_DOMAIN=app.example.com

Step 4: Write docker-compose.yml

The Compose file below launches Traefik v3 and a sample “whoami” app to test routing and TLS. It enables the Docker provider, sets HTTP to HTTPS redirection, configures Let’s Encrypt with the Cloudflare DNS challenge, and exposes the dashboard on a secure subdomain.

version: "3.9"

services:
  traefik:
   image: traefik:v3.1
   container_name: traefik
   command:
    - --providers.docker=true
    - --providers.docker.exposedbydefault=false
    - --entryPoints.web.address=:80
    - --entryPoints.websecure.address=:443
    - --entryPoints.web.http.redirections.entryPoint.to=websecure
    - --entryPoints.web.http.redirections.entryPoint.scheme=https
    - --certificatesResolvers.le.acme.dnsChallenge.provider=cloudflare
    - --certificatesResolvers.le.acme.email=${LETSENCRYPT_EMAIL}
    - --certificatesResolvers.le.acme.storage=/letsencrypt/acme.json
    - --log.level=INFO
    - --api.dashboard=true
   environment:
    - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
   ports:
    - "80:80"
    - "443:443"
   volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - ./letsencrypt:/letsencrypt
   networks:
    - proxy
   restart: unless-stopped
   labels:
    - "traefik.enable=true"
    - "traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_DOMAIN}`)"
    - "traefik.http.routers.dashboard.entrypoints=websecure"
    - "traefik.http.routers.dashboard.tls.certresolver=le"
    - "traefik.http.routers.dashboard.service=api@internal"
    - "traefik.http.middlewares.secure.headers.stsSeconds=31536000"
    - "traefik.http.middlewares.secure.headers.stsIncludeSubdomains=true"
    - "traefik.http.middlewares.secure.headers.stsPreload=true"
    - "traefik.http.middlewares.secure.headers.contentTypeNosniff=true"
    - "traefik.http.middlewares.secure.headers.browserXssFilter=true"

  whoami:
   image: traefik/whoami:latest
   networks:
    - proxy
   labels:
    - "traefik.enable=true"
    - "traefik.http.routers.who.rule=Host(`${APP_DOMAIN}`)"
    - "traefik.http.routers.who.entrypoints=websecure"
    - "traefik.http.routers.who.tls.certresolver=le"
    - "traefik.http.routers.who.middlewares=secure@docker"
    - "traefik.http.services.who.loadbalancer.server.port=80"
   restart: unless-stopped

networks:
  proxy:
   external: true

Step 5: Start Traefik and verify certificates

Bring the stack up and watch the logs for the ACME flow. Traefik will create a DNS TXT record via the API token and obtain certificates automatically.

docker compose up -d
docker logs -f traefik

On first successful issuance, an acme.json file will be created under the letsencrypt folder. Set strict permissions on it to keep private keys safe.

chmod 600 ./letsencrypt/acme.json

Step 6: Test the routes

Open https://traefik.example.com to see the dashboard and https://app.example.com to reach the whoami test application. Both should show a valid TLS lock in your browser. If you use Cloudflare proxy (orange cloud), DNS challenge still works because the validation is done via DNS records, not HTTP.

Optional: Protect the dashboard

Do not leave the dashboard open. You can add basic auth using a middleware. Create a bcrypt or Apache MD5 hash and replace the placeholder below. For a quick hash, you can use the “htpasswd” utility.

labels:
- "traefik.http.middlewares.dash-auth.basicauth.users=admin:$$apr1$$replace$$hashvaluehere"
- "traefik.http.routers.dashboard.middlewares=dash-auth@docker,secure@docker"

Adding your own apps

To publish another container behind Traefik, attach it to the “proxy” network and add three labels: a Host rule for your domain, the HTTPS entrypoint, and the certresolver. Optionally apply the “secure” headers middleware. Example:

labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`blog.example.com`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=le"
- "traefik.http.routers.blog.middlewares=secure@docker"

Troubleshooting

Port 80/443 already in use: Stop any existing web servers (e.g., Nginx, Apache) or change their ports. Traefik must bind to 80 and 443 to handle redirection and TLS termination.

Certificate issuance fails: Check Traefik logs for ACME errors. Verify the Cloudflare token has Zone:DNS:Edit on the correct zone, and that your environment variable is loaded by Compose. If you are using multiple zones, the token must cover them too.

404 or 502 errors: Ensure your container has traefik.enable=true, the Host rule matches your requested domain, DNS records resolve to your server, and the service port label points to the right container port.

Slow or failed DNS propagation: Give it a minute after creating DNS records. If using split DNS on a LAN, make sure internal resolvers return the public IP.

Why DNS challenge?

The DNS challenge avoids opening custom ports for validation and supports wildcard certificates. It is reliable behind CDNs, in NAT environments, and when you want to cover many subdomains without adding each one by hand.

What you achieved

You now have a production-ready Traefik v3 reverse proxy on Docker with automatic HTTPS, security headers, and a pattern you can reuse for any containerized app. Add services, set Host rules, and Traefik will handle routing and certificate management for you. This setup cuts down on boilerplate, centralizes TLS, and keeps your stack maintainable as it grows.

Comments