Docker Compose Nginx Reverse Proxy Certbot SSL Setup

Avatar
M

Maksudur Rahman

Software Engineer

349Views
5mRead
1Reactions

Docker Compose Nginx Reverse Proxy Certbot SSL Setup Tutorial

Deploying containerized web applications requires routing public HTTP/HTTPS traffic through a gateway proxy that terminates TLS certificates safely. Setting up Nginx with automated Let's Encrypt (Certbot) certificate issuance inside Docker Compose ensures zero-downtime certificate renewals without installing Nginx directly on the host server.

In this tutorial, you will configure an Nginx reverse proxy service with automatic Let's Encrypt SSL/TLS provisioning using Docker Compose and Webroot HTTP-01 challenge validation.

sequenceDiagram
    autonumber
    actor Client as Browser / External Client
    participant Nginx as Nginx Container (Ports 80/443)
    participant Certbot as Certbot Container
    participant LE as Let's Encrypt CA

    Note over Nginx,Certbot: Shared Volume: /var/www/certbot & /etc/letsencrypt

    Client->>Nginx: HTTP Request (Port 80)
    Nginx-->>Client: HTTP 301 Redirect -> HTTPS (Port 443)
    
    Certbot->>LE: 1. Request TLS Cert (ACME HTTP-01)
    LE->>Nginx: 2. Query /.well-known/acme-challenge/
    Nginx->>LE: 3. Serve validation token from shared webroot
    LE->>Certbot: 4. Issue SSL Certs (fullchain.pem, privkey.pem)
    Certbot->>Nginx: 5. Reload Nginx configuration
    Client->>Nginx: HTTPS Encrypted Request (Port 443)
    Nginx-->>Client: Serve TLS Encrypted Traffic

Quick Summary / Prerequisites

  • Operating System: Linux (Ubuntu 22.04 LTS / Debian 12 / AlmaLinux)

  • Required Tools: Docker 24.0+ & Docker Compose v2+

  • Prerequisites: A registered domain name pointed to your server's public IP address (A Record).

  • Skill Level: Intermediate

Step 1: Directory Structure & File Setup

Create a isolated workspace directory on your server:

mkdir -p docker-nginx-ssl/{nginx/conf.d,certbot/conf,certbot/www}
cd docker-nginx-ssl

Your final file structure will look like this:

docker-nginx-ssl/
├── docker-compose.yml
├── init-letsencrypt.sh
├── nginx/
│   └── conf.d/
│       └── app.conf
└── certbot/
    ├── conf/
    └── www/

Step 2: Writing the Docker Compose File

Create docker-compose.yml to define the Nginx reverse proxy, application service, and Certbot container.

version: '3.8'

services:
  web-app:
    image: nginx:alpine
    container_name: demo_web_app
    restart: unless-stopped
    volumes:
      - ./app_html:/usr/share/nginx/html:ro

  nginx:
    image: nginx:alpine
    container_name: reverse_proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - web-app

  certbot:
    image: certbot/certbot
    container_name: certbot_service
    restart: unless-stopped
    volumes:
      - ./certbot/conf:/etc/letsencrypt:rw
      - ./certbot/www:/var/www/certbot:rw
    entrypoint: "/bin/sh -c 'trap exit TERM; while true; do certbot renew; sleep 12d & wait $${!}; done;'"

Step 3: Configuring Nginx Reverse Proxy Blocks

Create nginx/conf.d/app.conf. Replace example.com with your domain.

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Serve ACME challenge responses for Let's Encrypt
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Redirect all HTTP traffic to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    # SSL Certificate Paths
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Security Headers & TLS Protocols
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://web-app:80;
        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;
    }
}

Step 4: Bootstrapping SSL Certificates with Dummy Certificates

Nginx will fail to start if the SSL certificate files referenced in app.conf do not exist. Use a bootstrap script to generate temporary self-signed certificates, start Nginx, request real Let's Encrypt certificates, and reload Nginx.

Create init-letsencrypt.sh:

#!/bin/bash

domains=(example.com www.example.com)
rsa_key_size=4096
data_path="./certbot"
email="[email protected]" # Set to your actual email
staging=0 # Set to 1 for testing rate limits

if [ -d "$data_path/conf/live/$domains" ]; then
  echo "Existing certificates found for $domains. Skipping initialization."
  exit 0
fi

echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
docker compose run --rm --entrypoint "\
  openssl req -x509 -nodes -newkey rsa:2048 -days 1\
    -keyout '$path/privkey.pem' \
    -out '$path/fullchain.pem' \
    -subj '/CN=localhost'" certbot

echo "### Starting Nginx container ..."
docker compose up --force-recreate -d nginx

echo "### Deleting dummy certificate for $domains ..."
docker compose run --rm --entrypoint "\
  rm -Rf /etc/letsencrypt/live/$domains && \
  rm -Rf /etc/letsencrypt/archive/$domains && \
  rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot

echo "### Requesting Let's Encrypt certificate for $domains ..."
domain_args=""
for domain in "${domains[@]}"; do
  domain_args="$domain_args -d $domain"
done

email_arg="--email $email"
if [ -z "$email" ]; then email_arg="--register-unsafely-without-email"; fi

staging_arg=""
if [ $staging != "0" ]; then staging_arg="--staging"; fi

docker compose run --rm --entrypoint "\
  certbot certonly --webroot -w /var/www/certbot \
    $staging_arg \
    $email_arg \
    $domain_args \
    --rsa-key-size $rsa_key_size \
    --agree-tos \
    --force-renewal" certbot

echo "### Reloading Nginx configuration ..."
docker compose exec nginx nginx -s reload

Make the script executable and run it:

chmod +x init-letsencrypt.sh
./init-letsencrypt.sh

Step 5: Common Errors & Troubleshooting (Gotchas)

  • Error 1: nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/.../fullchain.pem": No such file or directory

  • Cause: Starting Nginx before generating dummy certificates or requesting Let's Encrypt certificates.

  • Fix: Run init-letsencrypt.sh to populate dummy certificates prior to bringing up the production Nginx container.

  • Error 2: Failed authorization procedure. 404 NOT FOUND for /.well-known/acme-challenge/...

    • Cause: Nginx webroot location in app.conf does not match Certbot's shared volume/var/www/certbot.

    • Fix: Verify both Nginx and Certbot containers map./certbot/www to/var/www/certbot.

  • Error 3: There were too many requests of a given type (Rate Limit Exceeded)

    • Cause: Requesting real certificates repeatedly during testing (5 failures per hour per domain limit).

    • Fix: Set staging=1 in init-letsencrypt.sh while testing configuration changes.

  • Pro-Tips & Performance Best Practices

    1. Automatic Certificate Renewal: The Certbot container entrypoint in docker-compose.yml runs certbot renew every 12 days automatically in the background.

    2. HTTP/2 Optimization: Enable http2 on the 443 listen directive to reduce connection overhead and latency for multiplexed requests.

    3. OCSP Stapling: Add OCSP stapling directives to app.conf to improve TLS handshake speeds:

      ssl_stapling on;
      ssl_stapling_verify on;
      resolver 8.8.8.8 8.8.4.4 valid=300s;

    Next Steps

    Now that your containerized Nginx reverse proxy routes encrypted HTTPS traffic automatically, add security headers like Strict-Transport-Security (HSTS) or connect upstream services like Laravel, Next.js, or FastAPI containers over internal Docker networks.

    Recommended Resources & Courses

    React to this article