| |

Docker 88 🐳 Reverse Proxying Docker Containers with Automatic SSL/TLS via Traefik & Let’s Encrypt

⏱️ Estimated read: 14 min | |

Traefik is a reverse proxy that discovers Docker containers through the Docker socket and routes traffic to them based on labels attached to the container itself. The routing configuration is not a separate file that must be edited when a service is added; it is a set of traefik.http.routers.* and traefik.http.services.* labels on the container, and Traefik watches the Docker event stream to reconfigure itself when containers start and stop . Let’s Encrypt integration is built into Traefik through its ACME client: the certificatesResolvers configuration defines an email, a storage file, and a challenge type, and a router label attaches the resolver to the route. Traefik requests the certificate on the first request, stores it in the acme.json file, and renews it automatically before expiration .

The two pieces fit together through the router’s TLS configuration. A router with tls.certresolver=myresolver tells Traefik to obtain a certificate for the host in the router rule and to terminate TLS for that route. The entryPoints.web and entryPoints.websecure entries define the ports, and the HTTP-to-HTTPS redirect is configured on the web entrypoint . The result is a container that is reachable at https://app.example.com with a valid certificate, and the only configuration required is the label set and a shared Docker network.

This chapter covers the Docker provider configuration and its security implications, the ACME certificate resolver and the three challenge types, the label set that routes a container, the shared network requirement that lets Traefik reach the container, and the common failure modes that produce 404s and certificate errors. It assumes familiarity with Docker Compose and DNS basics.

Key point: Traefik routes to containers on a shared Docker network. The container must be on the same network as Traefik, the traefik.enable=true label must be present, and the router rule must match the host. The certificate resolver is attached with tls.certresolver, and the acme.json file must be persisted with chmod 600 or the certificates are lost on restart.


Why Traefik’s Docker provider changes the reverse proxy model

The configuration lives on the container, not in the proxy. A traditional reverse proxy (Nginx, Apache) requires a configuration file that lists every upstream server. Adding a service means editing the file and reloading the proxy. Traefik’s Docker provider inverts this: the container declares its own routing rules through labels, and Traefik discovers them by watching the Docker API . The proxy configuration is distributed across the containers it serves.

The Docker socket is a privileged interface. Traefik needs access to /var/run/docker.sock to list containers and watch events. The socket is the control channel for the Docker daemon, and access to it is effectively root access on the host . The recommended mitigation is a socket proxy that exposes only the CONTAINERS and NETWORKS API endpoints, limiting the damage if Traefik is compromised . The :ro mount flag on the socket is a partial mitigation, but it does not prevent API calls that mutate Docker state.

The ACME client is built in, but the challenge type determines the architecture. HTTP-01 requires port 80 to be reachable from the internet, because Let’s Encrypt validates the domain by fetching a file from http://example.com/.well-known/acme-challenge/. TLS-ALPN-01 requires port 443. DNS-01 requires API credentials for the DNS provider and supports wildcard certificates because the validation is a DNS TXT record, not an HTTP request . The choice depends on the network topology: a public server with port 80 open uses HTTP-01; a private server behind a firewall uses DNS-01; a server with only port 443 open uses TLS-ALPN-01.

The trade-off. Traefik’s label-based configuration is dynamic and requires no reload, but it spreads routing logic across the deployment. A service that is not on the shared network, or that lacks the traefik.enable label, is silently invisible to the proxy. The Docker socket is a security risk that must be mitigated. The ACME challenge type must match the network topology, or certificate issuance fails. The convenience is real, but it depends on a few configuration details that are easy to get wrong.


a. The Docker provider and the shared network

Traefik’s static configuration enables the Docker provider with providers.docker=true. The exposedByDefault=false setting is the recommended default: containers are not routed unless they have the traefik.enable=true label . Without it, every container on the host is exposed, including databases and internal services.

The network setting tells Traefik which Docker network to use when connecting to containers. A container that is not on that network cannot be reached by Traefik, even if the labels are correct. The common pattern is to create a dedicated network (docker network create proxy or traefik-public) and attach both Traefik and the backend containers to it .

# docker-compose.yaml
services:
  traefik:
    image: traefik:v3.0
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedByDefault=false"
      - "--providers.docker.network=proxy"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"
    networks:
      - proxy

  app:
    image: my-app:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`app.example.com`)"
      - "traefik.http.routers.app.entrypoints=websecure"
      - "traefik.http.routers.app.tls.certresolver=myresolver"
      - "traefik.http.services.app.loadbalancer.server.port=8080"
    networks:
      - proxy

networks:
  proxy:
    name: proxy

The loadbalancer.server.port label is required when the container exposes multiple ports or no ports. Traefik uses the lowest exposed port if there is only one, but the explicit label removes the ambiguity .


b. The ACME certificate resolver and the three challenges

The certificatesResolvers block defines the Let’s Encrypt configuration. The email is the account contact, the storage is the path to the acme.json file, and the challenge block selects the validation method .

command:
  - "--certificatesresolvers.myresolver.acme.email=admin@example.com"
  - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
  - "--certificatesresolvers.myresolver.acme.httpChallenge.entryPoint=web"

The acme.json file must be persisted in a volume and must have chmod 600 permissions. Traefik creates the file with the correct permissions on first write, but if the file is created manually with different permissions, the ACME client refuses to use it . The file contains the account private key and the issued certificates; losing it means re-issuing every certificate, which counts against Let’s Encrypt’s rate limits.

HTTP-01 is the simplest challenge. It requires port 80 to be reachable from the internet. Traefik serves the challenge response on the web entrypoint, and the HTTP-to-HTTPS redirect is compatible with the challenge because Traefik handles the challenge path before the redirect . The limitation is that HTTP-01 cannot issue wildcard certificates.

TLS-ALPN-01 uses port 443 instead of port 80. It is useful when port 80 is blocked but port 443 is open. The configuration is tlsChallenge=true instead of httpChallenge .

DNS-01 requires API credentials for the DNS provider. It supports wildcard certificates and works when neither port 80 nor port 443 is reachable from the internet, because the validation is a DNS TXT record. The configuration includes the provider name and the environment variables for the credentials .


c. The label set and the common failure modes

A routed container needs three labels at minimum: traefik.enable=true, a router rule with a Host matcher, and a service port label. A router with a certificate resolver needs tls.certresolver. The entrypoint label restricts the router to HTTPS; without it, the router may match both HTTP and HTTPS, depending on the entrypoint configuration .

LabelPurpose
traefik.enable=trueOpt the container into routing
traefik.http.routers.<name>.rule=Host(...)Match the host header
traefik.http.routers.<name>.entrypoints=websecureHTTPS only
traefik.http.routers.<name>.tls.certresolver=<name>Issue/renew certificate
traefik.http.services.<name>.loadbalancer.server.port=<port>Target port

The most common failure is a 404. It means Traefik received the request but no router matched. The causes are a missing traefik.enable label, a host rule that does not match the requested domain, or the container not being on the shared network. The Traefik dashboard shows the routers and their rules, which is the fastest way to diagnose a 404 .

The second most common failure is a certificate error. It means the ACME challenge failed. The causes are port 80 not being reachable (HTTP-01), port 443 not being reachable (TLS-ALPN-01), or DNS credentials missing (DNS-01). The Traefik logs show the ACME error, and the Let’s Encrypt staging server should be used during testing to avoid rate limits .


Complete Example Session

# ============================================
# PART 1: DOCKER COMPOSE WITH TRAEFIK
# ============================================
services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedByDefault=false"
      - "--providers.docker.network=proxy"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.email=admin@example.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.myresolver.acme.httpChallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"
    networks:
      - proxy

  app:
    image: my-app:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`app.example.com`)"
      - "traefik.http.routers.app.entrypoints=websecure"
      - "traefik.http.routers.app.tls.certresolver=myresolver"
      - "traefik.http.services.app.loadbalancer.server.port=8080"
    networks:
      - proxy

networks:
  proxy:
    name: proxy

# ============================================
# PART 2: ACME STORAGE PERMISSIONS
# ============================================
mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

# ============================================
# PART 3: DNS CHALLENGE FOR PRIVATE SERVERS
# ============================================
# command:
#   - "--certificatesresolvers.myresolver.acme.dnsChallenge=true"
#   - "--certificatesresolvers.myresolver.acme.dnsChallenge.provider=cloudflare"
# environment:
#   - "CF_API_EMAIL=..."
#   - "CF_API_KEY=..."

# ============================================
# PART 4: TLS-ALPN CHALLENGE (PORT 443 ONLY)
# ============================================
# command:
#   - "--certificatesresolvers.myresolver.acme.tlsChallenge=true"

# ============================================
# PART 5: STAGING SERVER FOR TESTING
# ============================================
# command:
#   - "--certificatesresolvers.myresolver.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"

# ============================================
# PART 6: SOCKET PROXY (SECURITY)
# ============================================
# services:
#   socket-proxy:
#     image: tecnativa/docker-socket-proxy
#     environment:
#       - CONTAINERS=1
#       - NETWORKS=1
#     volumes:
#       - "/var/run/docker.sock:/var/run/docker.sock:ro"
#   traefik:
#     command:
#       - "--providers.docker.endpoint=tcp://socket-proxy:2375"

# ============================================
# PART 7: DASHBOARD WITH BASIC AUTH
# ============================================
# labels:
#   - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
#   - "traefik.http.routers.dashboard.service=api@internal"
#   - "traefik.http.routers.dashboard.middlewares=auth"
#   - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$..."

# ============================================
# PART 8: VERIFICATION
# ============================================
curl -I https://app.example.com
# HTTP/2 200
# certificate valid

# ============================================
# PART 9: TROUBLESHOOTING 404
# ============================================
# Check: traefik.enable=true label present
# Check: container on proxy network
# Check: Host rule matches requested domain
# Check: Traefik dashboard shows router

# ============================================
# PART 10: THE COMPLETE FLOW
# ============================================
# 1. Traefik starts, reads Docker socket.
# 2. App container starts with labels.
# 3. Traefik discovers container, creates router.
# 4. First HTTPS request triggers ACME.
# 5. Certificate stored in acme.json.
# 6. Traefik routes to app on proxy network.
# 7. Renewal is automatic.

Quick Reference

Static Configuration

DirectivePurpose
providers.docker=trueEnable Docker provider
providers.docker.exposedByDefault=falseRequire explicit opt-in
providers.docker.network=proxyDefault network
entrypoints.web.address=:80HTTP entrypoint
entrypoints.websecure.address=:443HTTPS entrypoint
entrypoints.web.http.redirections.entrypoint.to=websecureHTTP→HTTPS

ACME Resolver

DirectivePurpose
certificatesresolvers.<name>.acme.emailAccount contact
certificatesresolvers.<name>.acme.storageacme.json path
...acme.httpChallenge.entrypoint=webHTTP-01
...acme.tlsChallenge=trueTLS-ALPN-01
...acme.dnsChallenge.provider=<name>DNS-01
...acme.caServer=<url>Staging server

Container Labels

LabelPurpose
traefik.enable=trueOpt in
traefik.http.routers.<name>.rule=Host(...)Match host
traefik.http.routers.<name>.entrypoints=websecureHTTPS only
traefik.http.routers.<name>.tls.certresolver=<name>ACME resolver
traefik.http.services.<name>.loadbalancer.server.port=<port>Target port

Challenge Types

ChallengePortWildcardUse When
HTTP-0180NoPublic server, port 80 open
TLS-ALPN-01443NoPublic server, only 443 open
DNS-01NoneYesPrivate server, or wildcard

Best Practices

✅ Do This:

# Use exposedByDefault=false.                                 // ✅
"--providers.docker.exposedByDefault=false"

# Persist acme.json with correct permissions.                 // ✅
touch letsencrypt/acme.json && chmod 600 letsencrypt/acme.json

# Attach Traefik and app to the same network.                 // ✅
networks: [proxy]

# Use the staging server during testing.                      // ✅
"--certificatesresolvers.myresolver.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"

# Use a socket proxy to limit Docker API access.              // ✅
endpoint: "tcp://socket-proxy:2375"

❌ Don’t Do This:

# Expose every container by default.                          // ❌
# exposedByDefault=true (default) exposes databases.

# Create acme.json with wrong permissions.                    // ❌
# ACME client refuses to use it.

# Forget the shared network.                                  // ❌
# Traefik cannot reach the container; 404.

# Use the production Let's Encrypt server for testing.        // ❌
# Rate limits lock the domain for a week.

Common Pitfalls

PitfallWhy It HappensFix
404 from TraefikNo router matchesCheck traefik.enable, network, Host rule
Certificate not issuedChallenge failedCheck port reachability, logs
acme.json permissions errorFile not 600chmod 600
Traefik cannot reach containerNot on shared networkAttach both to proxy
Rate-limited by Let’s EncryptProduction server used for testingUse staging server

Real-World Examples

1. Basic Traefik Compose

image: traefik:v3.0
command: ["--providers.docker=true", "--providers.docker.exposedByDefault=false"]

2. HTTP Challenge

"--certificatesresolvers.myresolver.acme.httpChallenge.entrypoint=web"

3. DNS Challenge

"--certificatesresolvers.myresolver.acme.dnsChallenge.provider=cloudflare"

4. App Labels

- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.tls.certresolver=myresolver"

5. Target Port

- "traefik.http.services.app.loadbalancer.server.port=8080"

6. HTTP Redirect

"--entrypoints.web.http.redirections.entrypoint.to=websecure"

7. Staging ACME

"--certificatesresolvers.myresolver.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"

8. Socket Proxy

"--providers.docker.endpoint=tcp://socket-proxy:2375"

9. Shared Network

networks: [proxy]

10. ACME Storage

"--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"

Visual

Traefik Discovery Flow

┌──────────────────────────────────────────────┐
│  DOCKER HOST                                 │
│                                              │
│  Traefik container                           │
│    watches /var/run/docker.sock              │
│       │                                      │
│       ▼                                      │
│  App container starts with labels            │
│    traefik.enable=true                       │
│    Host(`app.example.com`)                   │
│       │                                      │
│       ▼                                      │
│  Traefik creates router + service            │
│       │                                      │
│       ▼                                      │
│  Request → router → service → container      │
│                                              │
│  No config file edit. Labels are config.     │
│                                              │
└──────────────────────────────────────────────┘

ACME Challenge

┌──────────────────────────────────────────────┐
│  CERTIFICATE ISSUANCE                        │
│                                              │
│  First HTTPS request to app.example.com      │
│       │                                      │
│       ▼                                      │
│  Traefik has no cert → ACME                  │
│       │                                      │
│       ▼                                      │
│  Let's Encrypt challenges domain             │
│       │                                      │
│       ├── HTTP-01: fetch :80/.well-known     │
│       ├── TLS-ALPN-01: TLS handshake :443    │
│       └── DNS-01: check TXT record           │
│       │                                      │
│       ▼                                      │
│  Certificate issued, stored in acme.json     │
│       │                                      │
│       ▼                                      │
│  Renewed automatically before expiry         │
│                                              │
└──────────────────────────────────────────────┘

Network Requirement

┌──────────────────────────────────────────────┐
│  SHARED NETWORK                              │
│                                              │
│  network: proxy                              │
│    ├── traefik                               │
│    ├── app                                   │
│    └── other-service                         │
│                                              │
│  Traefik resolves app by container name      │
│  on the shared network.                      │
│                                              │
│  If app is on a different network:           │
│    Traefik cannot reach it.                  │
│    404 returned to client.                   │
│                                              │
└──────────────────────────────────────────────┘

Security Boundary

┌──────────────────────────────────────────────┐
│  DOCKER SOCKET                               │
│                                              │
│  Traefik needs the socket to discover        │
│  containers.                                 │
│                                              │
│  Direct mount:                               │
│    /var/run/docker.sock → full API access    │
│                                              │
│  Socket proxy:                               │
│    traefik → proxy:2375 → docker.sock        │
│    proxy exposes only CONTAINERS + NETWORKS  │
│                                              │
│  Limits blast radius if Traefik compromised. │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Docker providerproviders.docker=true
Default exposureexposedByDefault=false (explicit opt-in)
Shared networkproviders.docker.network=proxy
ACME storageacme.json, chmod 600
HTTP challengePort 80, no wildcard
TLS-ALPN challengePort 443, no wildcard
DNS challengeDNS API, wildcard support
Certificate resolver labeltraefik.http.routers.<name>.tls.certresolver
HTTP redirectentrypoints.web.http.redirections.entrypoint.to=websecure
TestingUse Let’s Encrypt staging server

Key takeaways:

  • Traefik discovers containers through labels, not a config file. The traefik.enable=true label opts a container into routing, and the traefik.http.routers.* labels define the rule and TLS resolver. Traefik watches the Docker event stream and reconfigures itself when containers start and stop .
  • The container must be on the same network as Traefik. The providers.docker.network setting defines the default network, and the container must be attached to it. A container on a different network is invisible to Traefik, producing a 404 .
  • The acme.json file must be persisted and protected. It contains the ACME account key and the issued certificates. Losing it means re-issuing every certificate, which counts against Let’s Encrypt rate limits. The file must have chmod 600 .
  • The challenge type must match the network topology. HTTP-01 needs port 80, TLS-ALPN-01 needs port 443, and DNS-01 needs provider credentials. DNS-01 is the only challenge that supports wildcard certificates .
  • The Docker socket is a privileged interface. The direct mount gives Traefik full Docker API access. A socket proxy that exposes only the container and network endpoints limits the damage if Traefik is compromised .
  • Use the staging server during testing. The production Let’s Encrypt server has rate limits that can lock a domain out for a week after repeated failures. The staging server has much higher limits and is the correct target for initial configuration .

Remember: Traefik with the Docker provider is a dynamic reverse proxy: the routing configuration is the set of labels on the container, and the certificate configuration is the ACME resolver attached to the router. The container must be on the shared network, the labels must include the host rule and the resolver, and the acme.json file must persist across restarts. The Docker socket is a security risk that a socket proxy mitigates. The challenge type is determined by which ports are reachable from the internet. Get these details right, and a new service is a container with three labels and a DNS record. Get them wrong, and the service is a 404 or a certificate error with no obvious cause.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!