SSL/TLS Certificates in Docker Containers

How to configure SSL certificates in Docker containers. Covers self-signed certs for development, mounting certificates as volumes, Docker Compose TLS, and Traefik/nginx reverse proxy patterns.


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:

  1. Volume mount certificates from the host
  2. Reverse proxy terminates TLS in front of your app container
  3. 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 :ro mounts for certificates
  • Don't commit certificates to git (add *.pem, *.key, *.p12 to .gitignore)
  • Use Docker secrets for Swarm deployments instead of environment variables
  • Rotate certificates before expiry -- automate with certbot or ACME

Next Steps


More in Guides