Setup SSL with Certbot + Nginx in a Dockerized App

marrouchi

Med Marrouchi

Posted on October 6, 2024

Setup SSL with Certbot + Nginx in a Dockerized App

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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

In the configuration file above, Nginx does the following:

  1. Redirects HTTP requests to HTTPS to ensure secure communication.
  2. 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 &
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The following times you can bring up your environment with the usual docker compose command:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
marrouchi
Med Marrouchi

Posted on October 6, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related