Setup SSL with Certbot + Nginx in a Dockerized App
Med Marrouchi
Posted on October 6, 2024
If you've ever struggled with setting up SSL in a Docker environment, you're not alone. SSL can be an intimidating hurdle for many, but it's crucial to secure your application, especially when it's exposed to the internet. In this post, I'll guide you through adding Nginx and Certbot for Let's Encrypt SSL generation in a Dockerized setup. This allows you to automatically renew certificates and keep your environment secure with minimal hassle.
Let's dive in!
Prerequisites
- Docker and Docker Compose installed on your machine.
- Basic understanding of Docker Compose and Nginx.
- A domain name pointing to your server.
In this example, we are using Nginx as a reverse proxy and Certbot to manage SSL certificates. Below, you'll find the docker-compose.yml
, shell script for auto-reloading Nginx, and necessary configuration files to set up everything.
Docker Compose Configuration
First, let me show you the Docker Compose configuration to set up Nginx and Certbot.
# docker-compose.yml
services:
nginx:
container_name: nginx
image: nginx:latest
restart: unless-stopped
env_file: .env
networks:
- your-app-network # Update this with your application service network
ports:
- 80:80
- 443:443
depends_on:
- your-app # Your application service
volumes:
- ./nginx/secure/:/etc/nginx/templates/
- /etc/localtime:/etc/localtime:ro
- ./nginx/certbot/conf:/etc/letsencrypt
- ./nginx/certbot/www:/var/www/certbot
- ./nginx/99-autoreload.sh:/docker-entrypoint.d/99-autoreload.sh # Script to autoreload Nginx when certs are renewed
certbot:
image: certbot/certbot
volumes:
- ./nginx/certbot/conf:/etc/letsencrypt
- ./nginx/certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" # Renew certificates every 12 hours
This Docker Compose file defines two services:
- Nginx: Acts as a reverse proxy and serves requests to your backend.
- Certbot: Takes care of generating and renewing SSL certificates using Let's Encrypt.
The certbot
service runs in an infinite loop, renewing certificates every 12 hours. Certificates are stored in a shared volume (./nginx/certbot/conf
), allowing Nginx to access the latest certificate files.
Nginx Configuration
Nginx serves as the reverse proxy, handling both HTTP and HTTPS traffic. For the initial request, Certbot needs HTTP (port 80) to complete the domain verification process.
# default.conf.template
server {
listen 80;
server_name ${APP_DOMAIN};
location / {
return 301 https://$host$request_uri;
}
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
server {
listen 443 ssl;
server_name ${APP_DOMAIN};
server_tokens off;
client_max_body_size 20M;
ssl_certificate /etc/letsencrypt/live/${APP_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${APP_DOMAIN}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Url-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://my-app:3000; // Your app service name
}
}
In the configuration file above, Nginx does the following:
- Redirects HTTP requests to HTTPS to ensure secure communication.
- Handles SSL termination and proxies requests to your backend service (e.g.,
my-app:3000
).
Auto-Reloading Nginx Configuration
After the SSL certificates are renewed, Nginx should be reloaded to apply the updated certificates. To automate this process, add a simple auto-reload script:
# 99-autoreload.sh
#!/bin/sh
while :; do
# Optional: Instead of sleep, detect config changes and only reload if necessary.
sleep 6h
nginx -t && nginx -s reload
done &
This script runs inside the Nginx container and reloads Nginx every 6 hours, or whenever the certificate is renewed.
Environment Variables
Create an .env
file to store your domain name and email address for Certbot registration:
# .env file
APP_DOMAIN=your-domain.com
SSL_EMAIL=contact@your-domain.com
Initial SSL Certificate Generation
Before Nginx can serve HTTPS traffic, you need to generate the initial SSL certificate. Use the following bash script (init-letsencrypt.sh) to generate the SSL certificate:
#!/bin/bash
# Source the .env file
if [ -f .env ]; then
export $(grep -v '^#' .env | xargs)
fi
if ! [ -x "$(command -v docker compose)" ]; then
echo 'Error: docker compose is not installed.' >&2
exit 1
fi
domains=(${APP_DOMAIN:-example.com})
rsa_key_size=4096
data_path="./nginx/certbot"
email="${SSL_EMAIL:-hello@example.com}" # Adding a valid address is strongly recommended
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits
if [ -d "$data_path" ]; then
read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
exit
fi
fi
if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
echo "### Downloading recommended TLS parameters ..."
mkdir -p "$data_path/conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf >"$data_path/conf/options-ssl-nginx.conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem >"$data_path/conf/ssl-dhparams.pem"
echo
fi
echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
docker compose -f "docker-compose.yml" run --rm --entrypoint "\
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
-keyout '$path/privkey.pem' \
-out '$path/fullchain.pem' \
-subj '/CN=localhost'" certbot
echo
echo "### Starting nginx ..."
docker compose -f "docker-compose.yml" up --force-recreate -d nginx
echo
echo "### Deleting dummy certificate for $domains ..."
docker compose -f "docker-compose.yml" run --rm --entrypoint "\
rm -Rf /etc/letsencrypt/live/$domains && \
rm -Rf /etc/letsencrypt/archive/$domains && \
rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo
echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
domain_args="$domain_args -d $domain"
done
# Select appropriate email arg
case "$email" in
"") email_arg="--register-unsafely-without-email" ;;
*) email_arg="--email $email" ;;
esac
# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi
docker compose -f "docker-compose.yml" 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
#echo "### Reloading nginx ..."
docker compose -f "docker-compose.yml" exec nginx nginx -s reload
Summary
In summary, the configuration provided above sets up Nginx as a reverse proxy for your Dockerized application, with Let's Encrypt SSL certificates automatically managed by Certbot. This setup ensures a secure connection to your application without the headache of manual SSL renewals.
Final Notes
To bring up your environment the first time, use:
chmod a+x init-letsencrypt.sh
./init-letsencrypt.sh
The following times you can bring up your environment with the usual docker compose command:
docker-compose up -d
Make sure your domain points to your server and that ports 80 and 443 are open to allow access to HTTP and HTTPS traffic.
If you run into any issues or have suggestions for improvements, let me know in the comments below! I'm happy to help troubleshoot or expand on specific topics.
Posted on October 6, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.