Containerized Application with SSL

This drives me nuts so I more or less favour cloud builds now to get away from having to setup HTTPS. The problem is, its just so necessary.

So I am documenting here how to do this using docker compose, nginx and certbot. One resource that helped me greatly was this excellent tutorial:

https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71

So here is how we do it:

You are going to need somewhere to host this. I used Linode.

Once you have a cheap instance spun up (Ubuntu 20ish should do), run the following:

sudo apt update && sudo apt upgrade -y

sudo apt install docker-io docker-compose

This will get you the magical docker.

Now, create yourself a docker-compose.yml that looks like this:

version: "3"



services:

  yourapplication:

    container_name: yourapplication

    image: yourapplication/yourapplication:latest





  nginx:

    image: nginx

    volumes:

      - ./data/certbot/conf:/etc/letsencrypt

      - ./data/certbot/www:/var/www/certbot

      - ./nginx.conf:/etc/nginx/conf.d/default.conf



    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"



    ports:

      - "80:80"

      - "443:443"



  certbot:

    image: certbot/certbot

    volumes:

      - ./data/certbot/conf:/etc/letsencrypt

      - ./data/certbot/www:/var/www/certbot

    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

There is a lot going on here. I am assuming you have some application you want to give SSL to. So replace yourapplication with that. Expose port 80 or whatever port you are going to tell nginx to forward requests to.

./nginx.conf:/etc/nginx/conf.d/default.conf

The above line maps a file in your current directory to a directory in the nginx container. Lets make that file!

sudo nano nginx.conf

Paste in the following:

server {

    listen 80;

    server_name YOURDOMAIN;

    location / {

        return 301 https://$host$request_uri;

    }

    location /.well-known/acme-challenge/ {

        root /var/www/certbot;

    }

}



server {

    listen                  443 ssl;

    listen                  [::]:443 ssl;

    server_name             YOURDOMAIN;



    ssl_certificate /etc/letsencrypt/live/YOURDOMAIN/fullchain.pem;

    ssl_certificate_key /etc/letsencrypt/live/YOURDOMAIN/privkey.pem;



    include /etc/letsencrypt/options-ssl-nginx.conf;

    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;



    location / {

        proxy_pass "http://YOURAPPLICATIONCONTAINERNAME:THEPORTYOUEXPOSED";

        proxy_http_version 1.1;

        proxy_set_header Upgrade $http_upgrade;

        proxy_set_header Connection "upgrade";

        proxy_set_header Host $host;

    }



    error_page   500 502 503 504  /50x.html;



}

So basically you are saying (I think)

Hey, nginx, if anyone navigates to YOURDOMAIN using http, redirect it to https. And if anyone shows up there, you can find the SSL stuff in this location to provide a cert and forward that to the docker application at http://YOURAPPLICATIONCONTAINERNAME:THEPORTYOUEXPOSED

Justin Lillico

There is a good chance I am misunderstanding this, but I had a crack!

So as the tutorial I am basing this on mentioned, there is a bit of a chicken or egg situation in that nginx needs the cert to start and the cert needs nginx to be obtained. So they have this neat little script to take care of business:

#!/bin/bash



if ! [ -x "$(command -v docker-compose)" ]; then

  echo 'Error: docker-compose is not installed.' >&2

  exit 1

fi



domains=(example.org www.example.org)

rsa_key_size=4096

data_path="./data/certbot"

email="" # 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 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 up --force-recreate -d nginx

echo



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





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 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 exec nginx nginx -s reload

I know its a monster, so you can use the below command to create it on your linux box without copy and pasting:

curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh > init-letsencrypt.sh

In any case, pay attention to these lines:

domains=(example.org www.example.org)

email="" # Adding a valid address is strongly recommended

Add your email and a space delimited list of the domains you are verifying. You are probably only doing one and that is okay!

Now, make the file executable and run it!

chmod +x init-letsencrypt.sh

./init-letsencrypt.sh

Once it has done its thing, you should be ready to fully launch this sucker!

docker-compose up -d

Test all is well. If I have messed anything up, let me know!

links

social