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 cert | Traefik | Caddy | |
|---|---|---|---|
| Setup | Medium (config + cert files) | Medium (labels) | Minimal |
| Auto-renewal | ❌ (manual) | ✅ | ✅ |
| Flexibility | High | High | Medium |
| Learning curve | Low (familiar) | Medium | Low |
| Best for | Small deployments, full control | Dynamic containers, microservices | Simple 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.