Set Up Traefik as a Docker Reverse Proxy With Automatic HTTPS

Put Traefik in front of Docker services, let labels drive routing, and issue TLS certificates automatically without turning the setup into a giant reverse-proxy theory project.

Traefik Docker Compose Automatic HTTPS
Illustrated guide cover for Set Up Traefik as a Docker Reverse Proxy With Automatic HTTPS
Traefik • Docker Compose • Automatic HTTPS
What you learn

How to run Traefik, expose only labeled containers, redirect HTTP to HTTPS, and verify routing with a sample service.

Best for

Operators already using Docker Compose who want a cleaner path than hand-editing Nginx vhosts for every new app.

Risk to watch

The fastest way to make a mess is letting every container auto-publish or exposing the dashboard publicly without a reason.

Before you begin

  • A Linux host with Docker Engine and the Compose plugin already working.
  • A DNS name such as apps.example.com or whoami.example.com pointed at the host.
  • Ports 80 and 443 reachable from the internet if you want automatic public certificates.
  • No other service already bound to 80 or 443.

Traefik's Docker guide centers on the Docker provider, named entrypoints, and label-based routing. The safest small-operator pattern is to set providers.docker.exposedbydefault=false so a new container stays dark until you label it on purpose.

Expected outcome: one Compose stack should own your public 80/443 ports, and new apps should become routable only when you deliberately join the shared proxy network and add labels.

Step 1: Prepare the host and DNS

Create a dedicated application directory and the ACME storage file:

mkdir -p ~/apps/traefik/letsencrypt
cd ~/apps/traefik
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

Create a shared Docker network that future app stacks can join:

docker network create proxy

Point your chosen DNS name at the host before you continue. If DNS is wrong, Traefik can start normally while certificate issuance quietly fails later.

Step 2: Create the Traefik Compose stack

Save this as docker-compose.yml inside ~/apps/traefik:

services:
  traefik:
    image: traefik:v3
    container_name: traefik
    restart: unless-stopped
    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.letsencrypt.acme.email=admin@example.com
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.letsencrypt.acme.httpchallenge=true
      - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
      - --api.dashboard=true
      - --accesslog=true
    ports:
      - 80:80
      - 443:443
      - 127.0.0.1:8080:8080
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy

networks:
  proxy:
    external: true

The local-only 127.0.0.1:8080:8080 bind keeps the Traefik dashboard off the public internet by default. Reach it later through SSH port forwarding or another private admin path you already trust.

docker compose up -d
docker compose ps

Step 3: Route a sample app with labels

Add a simple demo service to prove the routing model before you front a real app:

services:
  whoami:
    image: traefik/whoami:v1.10
    container_name: 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

The important pattern is not the demo container itself. It is that routing lives with the application service. That keeps app-specific exposure rules next to the app instead of buried in a central proxy file that grows into a junk drawer.

Step 4: Verify HTTPS and the private dashboard path

Check the public route first:

curl -I https://whoami.example.com
docker logs traefik --tail 100

Then inspect the dashboard privately from the host:

curl http://127.0.0.1:8080/dashboard/

What you want to confirm:

  • HTTP redirects to HTTPS.
  • The certificate is issued successfully.
  • Only labeled containers appear in the Traefik dashboard.
  • The dashboard is reachable locally but not published on the open internet.

Step 5: Operate it safely

Join future app stacks to the external proxy network and add labels only to the services meant for inbound traffic. Internal databases, queues, and admin-only helpers should stay off the proxy network unless you have a specific reason otherwise.

Good day-two checks:

docker ps --format 'table {{.Names}}\t{{.Ports}}'
docker logs traefik --tail 50
docker network inspect proxy
Warning: do not normalize the insecure dashboard or blind wildcard exposure. Traefik is convenient, which means it can make bad habits convenient too.

Rollback and update notes

If a new app label breaks routing, remove that app from the proxy network or disable its labels first instead of tearing down the proxy that every other service depends on.

docker compose down
docker compose pull
docker compose up -d

Keep the letsencrypt/acme.json file backed up. Losing it is not fatal, but restoring it avoids unnecessary certificate churn during rebuilds.

Troubleshooting

The container starts but certificates never appear.
Check DNS first, then verify ports 80 and 443 are not blocked upstream.

The app works on HTTP but not HTTPS.
Confirm the router uses the websecure entrypoint and has a TLS certresolver label.

The dashboard is reachable publicly.
Remove any public router for the dashboard and keep the host port bound to 127.0.0.1 only.

Every new container keeps showing up unexpectedly.
Recheck providers.docker.exposedbydefault=false and make sure old test labels are not lingering in other stacks.