← Back to Tutorials

Reverse Proxy Multiple Docker Services with Traefik (Automatic HTTPS)

traefikdockerreverse proxyhttpslet's encryptdocker composeself-hostingdevops

Reverse Proxy Multiple Docker Services with Traefik (Automatic HTTPS)

This tutorial shows how to put multiple Dockerized services behind a single Traefik reverse proxy with automatic HTTPS (Let’s Encrypt). You’ll learn the “why” and the “how”, run real commands, and end with a working setup you can extend safely.


What you’re building (high level)

You will run:

Routing will be configured via Docker labels on each container, so Traefik discovers services dynamically.


Prerequisites

1) A server reachable from the internet

You need a VPS or server with a public IP. This tutorial assumes Linux (Ubuntu/Debian-like), but it works similarly elsewhere.

2) DNS records for your domains

Create DNS A (or AAAA) records pointing to your server IP:

You can use any domain you control. Replace example.com throughout.

3) Open firewall ports

Traefik must be reachable on:

Example with UFW:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status

4) Install Docker and Docker Compose plugin

On Ubuntu:

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verify:

docker version
docker compose version

Why Traefik for multiple Docker services?

A reverse proxy sits in front of your apps and:

Traefik is particularly good for Docker because it can watch the Docker API and automatically configure routes based on container labels. You don’t have to manually edit proxy configs every time you add a service.


Choose an HTTPS challenge type (HTTP-01 vs DNS-01)

Let’s Encrypt needs to validate you control the domain.

HTTP-01 challenge (used in this tutorial)

DNS-01 challenge (not used here)

We’ll use HTTP-01 because it’s simplest.


Directory layout

Create a working directory:

mkdir -p ~/traefik-docker
cd ~/traefik-docker

We’ll create:


Create the ACME storage file (important)

Traefik stores issued certificates in a file. It must have strict permissions.

mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

Why chmod 600?


The complete Docker Compose setup

Create compose.yml:

nano compose.yml

Paste the following (replace domains and email):

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    command:
      # Enable Docker provider (watch containers/labels)
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false

      # EntryPoints: HTTP and HTTPS
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443

      # Redirect all HTTP -> HTTPS
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https

      # Let's Encrypt (ACME) via HTTP-01 challenge on entrypoint web (port 80)
      - --certificatesresolvers.le.acme.email=admin@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

      # (Optional but recommended) Logs
      - --log.level=INFO
      - --accesslog=true

      # (Optional) Traefik dashboard/API (we will secure it with a router rule)
      - --api.dashboard=true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      # Read Docker socket so Traefik can discover containers
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # Persist certs
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      # Enable Traefik for this container (dashboard router)
      - traefik.enable=true

      # Router for Traefik dashboard
      - traefik.http.routers.traefik.rule=Host(`traefik.example.com`)
      - traefik.http.routers.traefik.entrypoints=websecure
      - traefik.http.routers.traefik.tls=true
      - traefik.http.routers.traefik.tls.certresolver=le

      # The dashboard is served by Traefik's internal service "api@internal"
      - traefik.http.routers.traefik.service=api@internal

      # IMPORTANT: Add auth in real deployments (see section below)
      # - traefik.http.routers.traefik.middlewares=traefik-auth

  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=true
      - traefik.http.routers.whoami.tls.certresolver=le
      - traefik.http.services.whoami.loadbalancer.server.port=80

  nginx:
    image: nginx:1.27-alpine
    container_name: nginx
    restart: unless-stopped
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.nginx.rule=Host(`nginx.example.com`)
      - traefik.http.routers.nginx.entrypoints=websecure
      - traefik.http.routers.nginx.tls=true
      - traefik.http.routers.nginx.tls.certresolver=le
      - traefik.http.services.nginx.loadbalancer.server.port=80

networks:
  proxy:
    name: proxy

Explanation of the key parts

--providers.docker.exposedbydefault=false

This is a safety feature: Traefik will not expose every container automatically. Only containers with traefik.enable=true are routed publicly.

EntryPoints

Traefik uses entrypoints like named “listeners”. Routers attach to entrypoints.

HTTP → HTTPS redirect

These two lines:

force all HTTP traffic to redirect to HTTPS. This is convenient and also helps avoid accidentally serving insecure content.

ACME resolver le

The resolver name le is referenced in labels:

That tells Traefik: “If this router needs TLS, use the le resolver to obtain a certificate.”

Docker labels: routers and services

Traefik concepts:

In Docker, labels define these objects.

Example:


Start everything

From ~/traefik-docker:

docker compose up -d

Check status:

docker compose ps
docker logs traefik --tail=200

You should see Traefik starting, discovering containers, and attempting ACME certificate issuance when you first access a hostname.


Test the routes

1) Test HTTP redirect to HTTPS

Run from your local machine (or the server):

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

You should see a 301 or 308 redirect to https://whoami.example.com.

2) Test HTTPS and certificate issuance

Now:

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

If DNS is correct and ports are open, Traefik should obtain a certificate and you’ll see HTTP/2 200 or similar.

Open in a browser:

For whoami, you’ll see request details (host, headers, IP). For nginx, you’ll see the default Nginx welcome page.


Understanding how Traefik picks the right container

When a request comes in:

  1. Client connects to :443 (entrypoint websecure).
  2. Traefik reads SNI / Host header and matches routers:
    • Host whoami.example.com → router whoami
    • Host nginx.example.com → router nginx
  3. Router forwards to its configured service (load balancer).
  4. Traefik proxies the request over the Docker network proxy to the container’s internal port.

Because all services share the same Docker network (proxy), Traefik can reach them by container IP internally without exposing their ports to the host.


Securing the Traefik dashboard (do not skip in real use)

The dashboard is useful but should not be publicly open without protection.

Option A: Basic Auth middleware (simple)

Install apache2-utils to generate a password hash:

sudo apt-get update
sudo apt-get install -y apache2-utils

Generate credentials (pick a username):

htpasswd -nb admin 'your-strong-password'

This prints something like:

admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/

Now add a middleware label to Traefik service. Edit compose.yml and under traefik: labels, add:

      - traefik.http.middlewares.traefik-auth.basicauth.users=admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/
      - traefik.http.routers.traefik.middlewares=traefik-auth

Be careful with special characters. If you run into parsing issues, you can wrap the whole label value in quotes.

Apply changes:

docker compose up -d

Now visit:

You should be prompted for credentials.

Option B: IP allowlist (good additional control)

If you only want access from your IP, you can add:

      - traefik.http.middlewares.traefik-ipallow.ipallowlist.sourcerange=203.0.113.10/32
      - traefik.http.routers.traefik.middlewares=traefik-auth,traefik-ipallow

(Replace with your real public IP.)


Adding a new service (pattern you will reuse)

To add another Docker service behind Traefik:

  1. Attach it to the proxy network.
  2. Add labels:
    • traefik.enable=true
    • router rule (usually Host)
    • entrypoints websecure
    • TLS + certresolver
    • service port

Example with a fictional app listening on port 3000:

  myapp:
    image: yourorg/myapp:latest
    restart: unless-stopped
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.myapp.rule=Host(`myapp.example.com`)
      - traefik.http.routers.myapp.entrypoints=websecure
      - traefik.http.routers.myapp.tls=true
      - traefik.http.routers.myapp.tls.certresolver=le
      - traefik.http.services.myapp.loadbalancer.server.port=3000

Then:

docker compose up -d

No Traefik restart is required beyond container recreation; Traefik watches Docker events and updates routing dynamically.


Common troubleshooting (with real checks)

1) “My certificate isn’t issued”

Check Traefik logs:

docker logs traefik --tail=300

Common causes:

Verify DNS:

dig +short whoami.example.com A

Verify ports are listening:

sudo ss -tulpn | grep -E ':80|:443'

You should see docker-proxy / traefik bound.

2) “404 Not Found” from Traefik

A Traefik 404 usually means no router matched the request.

Check:

Inspect labels:

docker inspect whoami --format '{{json .Config.Labels}}' | jq

(Install jq if needed: sudo apt-get install -y jq.)

3) “Bad Gateway” (502)

This means Traefik matched a router but can’t reach the backend.

Common causes:

Check container is healthy/running:

docker ps
docker logs nginx --tail=100

Check network attachment:

docker network inspect proxy | jq '.[0].Containers | keys'

4) Let’s Encrypt rate limits

If you repeatedly request certs for the same domain while debugging, you can hit rate limits.

Mitigation:

To use staging, add:

      - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory

After you confirm routing works, remove it and delete acme.json (so it re-issues production certs):

docker compose down
rm -f letsencrypt/acme.json
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json
docker compose up -d

Operational tips (keeping it stable)

Updating images safely

Pull and recreate:

docker compose pull
docker compose up -d
docker image prune -f

Backups

Back up at least:

Example:

tar -czf traefik-backup.tar.gz compose.yml letsencrypt/acme.json

Don’t publish service ports

Notice we did not use ports: for whoami or nginx. Only Traefik publishes ports 80/443. This reduces attack surface.


You can define a middleware that adds common security headers and apply it to routers.

Add labels to the traefik service (or any container) to define a middleware:

      - traefik.http.middlewares.secure-headers.headers.framedeny=true
      - traefik.http.middlewares.secure-headers.headers.contenttypenosniff=true
      - traefik.http.middlewares.secure-headers.headers.referrerpolicy=no-referrer
      - traefik.http.middlewares.secure-headers.headers.permissionspolicy=geolocation=(), microphone=()
      - traefik.http.middlewares.secure-headers.headers.stsseconds=31536000
      - traefik.http.middlewares.secure-headers.headers.stsincludesubdomains=true
      - traefik.http.middlewares.secure-headers.headers.stspreload=true

Then apply to a router, e.g. whoami:

      - traefik.http.routers.whoami.middlewares=secure-headers

Recreate:

docker compose up -d

Note: HSTS (stsseconds) tells browsers to prefer HTTPS for a long time. Only enable it when you are sure HTTPS is working reliably.


Optional: Route multiple apps on one domain using paths

Host-based routing is simplest, but you can also do:

Example router rule:

- traefik.http.routers.app1.rule=Host(`example.com`) && PathPrefix(`/app1`)

Be aware many apps assume they run at / and may need base-path configuration or middleware to strip prefixes.


Full verification checklist

  1. DNS records point to your server IP:
    dig +short whoami.example.com A
  2. Ports open and Traefik listening:
    sudo ss -tulpn | grep -E ':80|:443'
  3. Containers running:
    docker compose ps
  4. HTTP redirects to HTTPS:
    curl -I http://nginx.example.com
  5. HTTPS works and cert is valid:
    curl -I https://nginx.example.com
  6. Traefik logs show successful ACME issuance:
    docker logs traefik --tail=200 | grep -i acme

Conclusion

You now have a working Traefik reverse proxy that:

From here, you can add:

If you share your target domains and the list of services/ports you want to expose, I can adapt the compose file and labels to your exact setup.