Managing Let’s Encrypt SSL Certificates for Nginx Running in Docker (nginx_prod)

This guide covers how to properly manage Let’s Encrypt SSL certificates when running Nginx inside Docker containers, addressing common challenges and providing automated solutions.

Prerequisites

Before starting, ensure you have:

  • Docker and Docker Compose installed
  • A domain pointing to your server
  • Root or sudo access on your server

Installation

1. Install Certbot

On Ubuntu/Debian:

sudo apt update
sudo apt install certbot

On CentOS/RHEL/Fedora:

sudo dnf install certbot  # Fedora
sudo yum install certbot   # CentOS/RHEL

Using Snap (universal):

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

2. Verify Installation

certbot --version

Understanding the Challenge

When Nginx runs in Docker, several issues arise:

  • Port Conflict: Certbot’s standalone mode needs port 80, but Dockerized Nginx typically uses it
  • File Access: Certificates are stored in /etc/letsencrypt/live/ but need to be accessible to the container
  • Automation: Renewals must account for Docker container lifecycle

Solution: Standalone Mode with Docker Integration

Step 1: Prepare Directory Structure

Create directories for SSL certificates that will be mounted in your Docker container:

sudo mkdir -p /etc/nginx/ssl
sudo chmod 755 /etc/nginx/ssl

Step 2: Stop Nginx Container

Temporarily stop your Nginx container to free port 80:

docker stop nginx_prod
# Or if using docker-compose:
docker-compose down

Step 3: Obtain SSL Certificate

Run Certbot in standalone mode:

sudo certbot certonly \
  --standalone \
  -d your.domain.com \
  -d www.your.domain.com \
  --email your.email@example.com \
  --agree-tos \
  --non-interactive \
  --preferred-challenges http

For multiple domains:

sudo certbot certonly \
  --standalone \
  -d domain1.com \
  -d www.domain1.com \
  -d domain2.com \
  -d www.domain2.com \
  --email your.email@example.com \
  --agree-tos \
  --non-interactive

Step 4: Copy Certificates to Docker Mount Point

Copy the certificates to your Docker-accessible directory:

sudo cp /etc/letsencrypt/live/your.domain.com/fullchain.pem /etc/nginx/ssl/your.domain.com.crt
sudo cp /etc/letsencrypt/live/your.domain.com/privkey.pem /etc/nginx/ssl/your.domain.com.key

# Set proper permissions
sudo chmod 644 /etc/nginx/ssl/your.domain.com.crt
sudo chmod 600 /etc/nginx/ssl/your.domain.com.key

Step 5: Update Docker Configuration

Ensure your Docker Compose file mounts the SSL directory:

version: '3.8'
services:
  nginx:
    image: nginx:alpine
    container_name: nginx_prod
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - /etc/nginx/ssl:/etc/nginx/ssl:ro  # Mount SSL certificates
    restart: unless-stopped

Step 6: Configure Nginx for SSL

Update your Nginx configuration to use the certificates:

server {
    listen 80;
    server_name your.domain.com www.your.domain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your.domain.com www.your.domain.com;

    ssl_certificate /etc/nginx/ssl/your.domain.com.crt;
    ssl_certificate_key /etc/nginx/ssl/your.domain.com.key;
    
    # Modern SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # Your application configuration
    location / {
        # Your proxy_pass or static file serving configuration
    }
}

Step 7: Start Nginx Container

docker start nginx_prod
# Or with docker-compose:
docker-compose up -d

Automation: Automated Certificate Renewal

Create Renewal Script

Create a script to handle the entire renewal process:

sudo nano /usr/local/bin/renew-ssl.sh
#!/bin/bash

DOMAIN="your.domain.com"
CONTAINER_NAME="nginx_prod"
SSL_DIR="/etc/nginx/ssl"

# Stop nginx container
echo "Stopping Nginx container..."
docker stop $CONTAINER_NAME

# Renew certificate
echo "Renewing SSL certificate..."
certbot renew --quiet --standalone

# Copy renewed certificates
echo "Copying certificates..."
cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem $SSL_DIR/$DOMAIN.crt
cp /etc/letsencrypt/live/$DOMAIN/privkey.pem $SSL_DIR/$DOMAIN.key

# Set permissions
chmod 644 $SSL_DIR/$DOMAIN.crt
chmod 600 $SSL_DIR/$DOMAIN.key

# Start nginx container
echo "Starting Nginx container..."
docker start $CONTAINER_NAME

echo "SSL renewal completed successfully!"

Make it executable:

sudo chmod +x /usr/local/bin/renew-ssl.sh

Set Up Cron Job

Add to root’s crontab for automatic renewal:

sudo crontab -e

Add this line (runs every day at 3 AM, but only renews if needed):

0 3 * * * /usr/local/bin/renew-ssl.sh >> /var/log/ssl-renewal.log 2>&1

Test the Renewal Process

Test renewal without waiting for expiration:

sudo /usr/local/bin/renew-ssl.sh

Alternative: Using Docker Volumes

For a more integrated approach, you can mount the Let’s Encrypt directory directly:

version: '3.8'
services:
  nginx:
    image: nginx:alpine
    container_name: nginx_prod
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    restart: unless-stopped

Then reference certificates directly in Nginx config:

ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;

Troubleshooting

Common Issues

Port 80 already in use:

# Check what's using port 80
sudo netstat -tlnp | grep :80
# Or with ss
sudo ss -tlnp | grep :80

Permission denied errors:

# Fix Let's Encrypt permissions
sudo chmod -R 755 /etc/letsencrypt
sudo chmod -R 644 /etc/letsencrypt/archive
sudo chmod -R 600 /etc/letsencrypt/archive/*/privkey*.pem

Certificate not found:

# List available certificates
sudo certbot certificates

Verification

Check if SSL is working properly:

# Test SSL configuration
openssl s_client -connect your.domain.com:443 -servername your.domain.com

# Check certificate expiration
openssl x509 -in /etc/nginx/ssl/your.domain.com.crt -text -noout | grep "Not After"

Best Practices

  1. Regular Monitoring: Set up monitoring to alert you if certificate renewal fails
  2. Backup Certificates: Include /etc/letsencrypt in your backup strategy
  3. Test Renewals: Regularly test the renewal process in staging
  4. Multiple Domains: Use SAN certificates for multiple subdomains
  5. Security Headers: Add security headers to your Nginx configuration

Security Considerations

Always use strong SSL configurations:

# Add security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/your.domain.com.crt;

This approach provides a robust, automated solution for managing SSL certificates with Dockerized Nginx while maintaining security and ease of maintenance.

Tags:
Table of Contents