The Docker TLS Problem
Docker containers are ephemeral. Every time you rebuild, your certificates disappear. You need a strategy for getting certificates into containers without baking secrets into images.
There are three common patterns:
- Volume mount certificates from the host
- Reverse proxy terminates TLS in front of your app container
- Init container or entrypoint script generates certs on startup
Pattern 1: Volume-Mounted Certificates
The simplest approach. Generate your certificate, then mount it into the container.
Generate a test certificate at getaCert.com, then:
# docker-compose.yml
services:
web:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./certs/server.pem:/etc/ssl/certs/server.pem:ro
- ./certs/server.key:/etc/ssl/private/server.key:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
# nginx.conf
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/ssl/certs/server.pem;
ssl_certificate_key /etc/ssl/private/server.key;
location / {
root /usr/share/nginx/html;
}
}
The :ro flag mounts certificates read-only. Never put private keys in your Docker image.
Pattern 2: Reverse Proxy with Traefik
Traefik handles TLS termination and automatic certificate management:
# docker-compose.yml
services:
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.le.acme.email=you@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
ports:
- "443:443"
- "80:80"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
webapp:
image: your-app:latest
labels:
- "traefik.http.routers.webapp.rule=Host(`example.com`)"
- "traefik.http.routers.webapp.tls.certresolver=le"
volumes:
letsencrypt:
Traefik automatically gets Let's Encrypt certificates. For development with self-signed certs, use a file provider instead.
Pattern 3: Self-Signed Certs for Development
For local development, generate a self-signed certificate as part of your Docker setup:
# Dockerfile
FROM python:3.12-slim
RUN apt-get update && apt-get install -y openssl
# Generate self-signed cert at build time (dev only!)
RUN openssl req -x509 -newkey rsa:2048 -nodes \
-keyout /etc/ssl/private/dev.key \
-out /etc/ssl/certs/dev.pem \
-days 365 -subj "/CN=localhost"
COPY . /app
WORKDIR /app
CMD ["gunicorn", "--certfile=/etc/ssl/certs/dev.pem", \
"--keyfile=/etc/ssl/private/dev.key", \
"-b", "0.0.0.0:443", "app:app"]
This is fine for development but never use self-signed certificates generated at build time in production.
A better approach for development is to generate the cert once with getaCert.com and mount it as a volume.
Trusting Self-Signed Certificates
When one container needs to talk to another over TLS with a self-signed cert, you need to add the CA certificate to the client container's trust store:
# Client container
FROM python:3.12-slim
COPY ca-cert.pem /usr/local/share/ca-certificates/myca.crt
RUN update-ca-certificates
For Python applications using requests:
import requests
# Option 1: Point to the CA cert
response = requests.get('https://other-service:443', verify='/path/to/ca-cert.pem')
# Option 2: Use REQUESTS_CA_BUNDLE environment variable
# Set in docker-compose.yml:
# environment:
# - REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
For Node.js:
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
ca: fs.readFileSync('/path/to/ca-cert.pem')
});
fetch('https://other-service:443', { agent });
Docker Compose with Multiple TLS Services
A real-world example with nginx terminating TLS for multiple backend services:
services:
proxy:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./certs:/etc/nginx/certs:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- api
- frontend
api:
build: ./api
# No TLS -- nginx handles it
expose:
- "8000"
frontend:
build: ./frontend
expose:
- "3000"
The backend services communicate in plaintext inside the Docker network. Only the nginx proxy needs the certificate.
Certificate Renewal in Docker
For production, certificates expire and need renewal. Common strategies:
Let's Encrypt with certbot sidecar:
services:
certbot:
image: certbot/certbot
volumes:
- certs:/etc/letsencrypt
- certbot-www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h; done'"
nginx:
image: nginx:alpine
volumes:
- certs:/etc/letsencrypt:ro
- certbot-www:/var/www/certbot:ro
volumes:
certs:
certbot-www:
ACME with getaCert.com:
Use our ACME endpoint with certbot or acme.sh for certificate issuance and renewal.
Security Checklist
- Never bake private keys into Docker images
- Use
:romounts for certificates - Don't commit certificates to git (add
*.pem,*.key,*.p12to.gitignore) - Use Docker secrets for Swarm deployments instead of environment variables
- Rotate certificates before expiry -- automate with certbot or ACME
Next Steps
- Generate a self-signed certificate for Docker development
- Set up mTLS between containers
- Learn about Kubernetes TLS for orchestrated deployments