🗓️ 03042026 2100

DOCKER TLS TERMINATION

What it is:

  • Pattern where a reverse_proxy container handles all TLS encryption/decryption
  • Backend containers communicate over plain HTTP within the Docker network
  • The proxy holds the tls_certificates and terminates HTTPS connections

Problem it solves:

  • Each backend service managing its own certificates is operational chaos
  • Certificate renewal, OCSP stapling, TLS version management — centralize it in one place
  • Multi-client Docker deployments need automated certificate management per domain

Architecture​

Client → HTTPS → [Reverse Proxy Container] → HTTP → [App Containers]
(holds certs, terminates TLS) (internal network only)
  • Proxy and app containers share a docker_networking bridge network
  • App containers are NOT exposed to the host — only the proxy binds ports 80/443
  • Internal traffic is unencrypted but isolated within Docker's virtual network

Reverse Proxy Options for Docker​

Traefik​

  • Docker-native: auto-discovers services via container labels
  • Built-in Let's Encrypt (ACME) — automatic cert provisioning and renewal
  • Built-in ocsp stapling
  • Best for: dynamic multi-client setups where services come and go

Caddy​

  • Automatic HTTPS by default — zero-config TLS for any domain
  • Built-in ACME, OCSP stapling, HTTP/3
  • Simpler config than nginx/Traefik
  • Best for: simplicity-first deployments

nginx​

  • Most widely used, battle-tested, highest performance
  • Requires manual ACME setup (Certbot sidecar or cron) and explicit OCSP stapling config
  • Best for: teams already familiar with nginx, complex routing needs

Multi-Client TLS Pattern​

For one server hosting multiple client domains:

  • Reverse proxy listens on 443, routes by domain (SNI — Server Name Indication)
  • Each client domain gets its own certificate (auto-provisioned via ACME)
  • Proxy handles renewal — no downtime, no manual intervention
  • Combine with docker_compose_profiles to manage per-client app stacks behind the shared proxy

Shared proxy, isolated backends​

  • Proxy container has no profile (always runs)
  • Per-client app containers tagged with client profile
  • Per-client docker_networking networks — proxy joins all, clients only join their own
  • Result: client-a's containers cannot reach client-b's containers

Certificate Storage​

  • Certificates and private keys need to persist across proxy container restarts
  • Mount a docker_volumes volume for the cert directory
  • Traefik: acme.json stores all certs; Caddy: $HOME/.local/share/caddy
  • Back up cert storage — losing it means re-issuing all certificates (rate limits apply)

Security Checklist​

  • TLS 1.2 minimum (disable TLS 1.0, 1.1, SSLv3)
  • Enable OCSP stapling (see ocsp)
  • HSTS header (Strict-Transport-Security) — forces HTTPS for returning visitors
  • Redirect HTTP → HTTPS at the proxy
  • Proxy container runs as non-root where possible (see dockerfile_best_practices)
  • Private keys readable only by the proxy process (file permissions on volume)
DANGER

Exposing backend containers directly to the internet: If app containers bind to host ports alongside the proxy, they bypass TLS entirely. Only the reverse proxy should bind ports 80/443. Backend containers should only be reachable via internal Docker networks.

WARNING

Certificate storage without volumes: If the proxy container is recreated without a persistent volume for certs, all certificates are lost. Let's Encrypt rate limits (50 certs/domain/week) may prevent immediate re-issuance.


References​