Docker Compose Nginx Reverse Proxy Certbot SSL Setup
Maksudur Rahman
Software Engineer
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 TrafficQuick 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-sslYour 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 reloadMake the script executable and run it:
chmod +x init-letsencrypt.sh
./init-letsencrypt.shStep 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.shto 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.confdoes not match Certbot's shared volume/var/www/certbot.Fix: Verify both Nginx and Certbot containers map
./certbot/wwwto/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=1ininit-letsencrypt.shwhile testing configuration changes.
Pro-Tips & Performance Best Practices
Automatic Certificate Renewal: The Certbot container entrypoint in
docker-compose.ymlrunscertbot renewevery 12 days automatically in the background.HTTP/2 Optimization: Enable
http2on the 443 listen directive to reduce connection overhead and latency for multiplexed requests.OCSP Stapling: Add OCSP stapling directives to
app.confto 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.