All Deployment guides Deployment

SSL Certificates with Docker and Reverse Proxies

Docker containers typically don’t handle SSL themselves — a reverse proxy in front terminates TLS and forwards decrypted traffic to the container. This guide covers the three most common approaches.

Approach 1: Nginx reverse proxy with manual certificate

The simplest approach for small deployments. Get a certificate from GetHTTPS and mount it into an Nginx container.

docker-compose.yml

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs/fullchain.pem:/etc/ssl/fullchain.pem:ro
      - ./certs/privkey.pem:/etc/ssl/privkey.pem:ro
    depends_on:
      - app

  app:
    image: your-app:latest
    expose:
      - "3000"

nginx.conf

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate     /etc/ssl/fullchain.pem;
    ssl_certificate_key /etc/ssl/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://app:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$host$request_uri;
}

To renew: Replace the files in ./certs/ with new ones from GetHTTPS, then docker compose exec nginx nginx -s reload.

Approach 2: Traefik with automatic Let’s Encrypt

Traefik is a reverse proxy built for containers. It can automatically obtain and renew Let’s Encrypt certificates.

docker-compose.yml

services:
  traefik:
    image: traefik:v3.0
    command:
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email=you@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/acme/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-acme:/acme

  app:
    image: your-app:latest
    labels:
      - "traefik.http.routers.app.rule=Host(`yourdomain.com`)"
      - "traefik.http.routers.app.tls.certresolver=letsencrypt"
      - "traefik.http.services.app.loadbalancer.server.port=3000"

volumes:
  traefik-acme:

Traefik handles everything: certificate issuance, renewal, and HTTPS termination. No manual certificate management needed.

Approach 3: Caddy (zero-config HTTPS)

Caddy automatically obtains and renews Let’s Encrypt certificates with zero configuration.

docker-compose.yml

services:
  caddy:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
    depends_on:
      - app

  app:
    image: your-app:latest
    expose:
      - "3000"

volumes:
  caddy-data:

Caddyfile

yourdomain.com {
    reverse_proxy app:3000
}

That’s the entire config. Caddy gets a Let’s Encrypt certificate and renews it automatically.

Which approach to choose?

Nginx + manual certTraefikCaddy
SetupMedium (config + cert files)Medium (labels)Minimal
Auto-renewal❌ (manual)
FlexibilityHighHighMedium
Learning curveLow (familiar)MediumLow
Best forSmall deployments, full controlDynamic containers, microservicesSimple sites, minimal config

Renewing certificates in Docker

Approach 1 (Nginx + manual):

# Replace cert files in your local ./certs/ directory
cp new-fullchain.pem ./certs/fullchain.pem
cp new-privkey.pem ./certs/privkey.pem

# Reload Nginx inside the container (no restart needed)
docker compose exec nginx nginx -s reload

Approach 2 (Traefik) & 3 (Caddy):

Automatic — they handle renewal internally. Certificates are stored in Docker volumes (traefik-acme or caddy-data) and persist across container restarts.

Docker Swarm: using secrets

For Docker Swarm deployments, use Docker secrets instead of bind-mounting certificate files:

# Create secrets from your cert files
docker secret create ssl_cert fullchain.pem
docker secret create ssl_key privkey.pem
# In docker-compose.yml (deploy mode)
services:
  nginx:
    image: nginx:alpine
    secrets:
      - ssl_cert
      - ssl_key
    configs:
      - source: nginx_conf
        target: /etc/nginx/conf.d/default.conf

secrets:
  ssl_cert:
    external: true
  ssl_key:
    external: true

Secrets are mounted at /run/secrets/ssl_cert and /run/secrets/ssl_key inside the container — encrypted at rest and in transit.

Common mistakes

Baking certificates into Docker images

# ❌ NEVER do this
COPY fullchain.pem /etc/ssl/fullchain.pem
COPY privkey.pem /etc/ssl/privkey.pem

Certificates expire every 90 days. Baking them into the image means rebuilding and redeploying every 60 days. Always mount certificates as volumes or secrets.

Forgetting to expose port 80

Port 80 is needed for:

  • HTTP → HTTPS redirect
  • Let’s Encrypt HTTP-01 challenge (for Traefik/Caddy auto-renewal)
ports:
  - "80:80"    # Don't forget this
  - "443:443"

Not persisting ACME data

Traefik and Caddy store their Let’s Encrypt certificates and account keys in volumes. Without persistence, they re-request certificates on every container restart — and will hit rate limits:

volumes:
  traefik-acme:    # MUST be a named volume, not anonymous
  caddy-data:

Frequently asked questions

Can my app container handle SSL directly?

You can, but it’s not recommended. The reverse proxy pattern (terminate TLS at the edge, forward plain HTTP internally) is standard because: the app doesn’t need to know about certificates, renewal doesn’t require app restarts, and you centralize TLS configuration.

How do I handle SSL for multiple containers?

With Traefik or Caddy, add labels/config for each service — they’ll automatically get separate certificates. With Nginx, add multiple server blocks in the config.

Should I use GetHTTPS or Traefik’s built-in ACME?

Use Traefik/Caddy’s built-in ACME for production — it handles renewal automatically. Use GetHTTPS when you need a certificate for the Nginx reverse proxy approach (Approach 1) or for environments where the container doesn’t have direct internet access for ACME challenges.

How do I mount certificates securely in Docker?

Mount as read-only (:ro), use Docker secrets for the private key in Swarm mode, and ensure only the reverse proxy container has access to the certificate files. Never bake certificates into a Docker image — they’ll expire and you’d need to rebuild.

Can I use GetHTTPS certificates with Kubernetes?

Yes. Create a Kubernetes TLS secret from your GetHTTPS certificate files:

kubectl create secret tls my-tls-cert --cert=fullchain.pem --key=privkey.pem

Then reference it in your Ingress resource. For automated renewal in Kubernetes, consider cert-manager with a Let’s Encrypt ClusterIssuer.

Related articles

Deployment 2026-05-08
How to Install an SSL Certificate on Nginx
Step-by-step guide to installing an SSL certificate on Nginx. Covers file upload, full server block config, TLS best practices, HTTP/2, HSTS, redirect setup, testing, and troubleshooting 6 common errors.
Getting Started 2026-05-08
How to Get a Free SSL Certificate (Step-by-Step Guide)
Get a free SSL certificate from Let's Encrypt in 5 minutes — no software to install, no account to create. Complete guide covering 4 methods, both challenge types, installation on 6 platforms, and troubleshooting.
Compare 2026-05-07
Browser-Based vs CLI ACME Clients
Compare browser-based ACME clients (like GetHTTPS) with CLI tools (Certbot, acme.sh). Understand the trade-offs in privacy, automation, and use cases.
Get a free SSL certificate in your browser
No installation, no account. Your private key never leaves your device.
Get your certificate