Traefik Reverse Proxy with Automatic Let's Encrypt TLS - 夜莺博客

Traefik Reverse Proxy with Automatic Let's Encrypt TLS

Traefik earns its place in a container estate by doing one thing nginx does not: it
discovers services from Docker labels and requests TLS certificates on their behalf, so adding
a new application is a matter of adding labels rather than editing and reloading a proxy
config. This article sets up Traefik with automatic Let's Encrypt certificates, explains the
router/service/middleware model that people find confusing at first, and covers the parts that
break in production — the ACME account store, the HTTP challenge, and certificate persistence
across container restarts.

The Object Model in One Paragraph

Traefik has three concepts. An entrypoint is a listening port (80, 443). A
router matches an incoming request — by hostname or path — and attaches
middlewares that transform it, such as redirecting HTTP to HTTPS or adding
basic auth. A service is where the request ends up: a container, a set of
containers, or an external URL. Everything you configure is one of those three things, which is
why a Traefik misconfiguration is almost always a router rule that does not match any request,
or a service that points at the wrong port.

Docker Compose: Traefik Itself

version: "3.8"

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    command:
      - "--log.level=INFO"
      - "--accesslog=true"
      - "--api.dashboard=true"
      # Entrypoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.websecure.address=:443"
      # Docker provider
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=proxy"
      # ACME / Let's Encrypt
      - "--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"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"
    networks:
      - proxy

networks:
  proxy:
    name: proxy

Three settings carry most of the weight:

  • exposedbydefault=false — without it, Traefik tries to route to every container
    on the host, including databases. Turn it on explicitly and label what should be public.
  • providers.docker.network=proxy — Traefik talks to containers over this shared
    network. A container not attached to it will be routed to and time out.
  • acme.storage — pointed at a mounted volume. If you lose
    acme.json, you lose your certificates and hit Let's Encrypt rate limits
    reissuing them.

ACME Prerequisites

  • Ports 80 and 443 must be reachable from the internet. The HTTP challenge is served on port
    80 specifically, even when you only care about HTTPS.
  • DNS for every hostname must already resolve to the host, before the certificate request.
  • acme.json must be mode 600, or Traefik will refuse to write to it
    with a permissions error that is easy to miss in the logs.
mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

Adding an Application

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

networks:
  proxy:
    external: true

The two things people get wrong here: loadbalancer.server.port must be the port
inside the container (80, not 8080 on the host), and the service name in the label
(app) is arbitrary but must be consistent across the router and service labels.

Middlewares Worth Having

labels:
  # Force HTTPS on the router
  - "traefik.http.routers.app.middlewares=secHeaders@file,rateLimit@file"

# File provider — /etc/traefik/dynamic.yml, mounted read-only
http:
  middlewares:
    secHeaders:
      headers:
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        contentTypeNosniff: true
        frameDeny: true
        referrerPolicy: strict-origin-when-cross-origin
    rateLimit:
      rateLimit:
        average: 100
        burst: 50
    basicAuth:
      basicAuth:
        users:
          - "admin:$2y$05$abcdefghijklmnopqrstuv"

Generate the basic-auth hash with htpasswd -nbB admin yourpassword and escape
the $ characters as $$ when you put them in a Compose file.

Compression, Sticky Sessions and WebSockets

http:
  middlewares:
    compress:
      compress: {}
  services:
    app:
      loadBalancer:
        sticky:
          cookie:
            name: traefik_lb
            secure: true
            httpOnly: true

Traefik handles WebSocket upgrades automatically as long as you do not strip the
Upgrade and Connection headers in a custom middleware — a common
own-goal when copying nginx snippets into a headers middleware.

Verification and Troubleshooting

# Dashboard (expose it privately, not publicly)
docker logs traefik 2>&1 | tail -50
docker logs traefik 2>&1 | grep -i acme

# From the host
curl -I https://app.example.com
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

# Watch the challenge in the access log
docker logs traefik 2>&1 | grep '/.well-known/acme-challenge'
  • "no available server" — the container is not on the Traefik network, or the
    declared container port is wrong.
  • Certificate never issues — port 80 unreachable from the internet, DNS not
    resolving, or an existing acme.json entry for that domain with a stale state.
  • 404 with a matching Host header — traefik.enable=true missing,
    or the router rule has a typo in the backtick-quoted hostname.
  • Renewal failing silently — check expiry with the openssl command above on a
    schedule, not just when a user reports a browser warning.

If you run Kubernetes rather than plain Docker, the ingress-nginx equivalent is covered in
Kubernetes ingress TLS with cert-manager and Lets Encrypt, and if you are comparing layer-4 proxies, nginx stream module TCP/UDP load balancing is the throughput-oriented alternative. For high availability in front of Traefik, Keepalived VRRP and HAProxy virtual IP failover covers the floating-address layer.

原文链接:https://doc.traefik.io/traefik/https/acme/