How to set up SSL certificate for Nginx Docker container

July 28, 2019, 4:45 p.m.

I have been using Docker for past few years as a dev environment. It's easy to set up, run and maintain. Recently I decided to containerise one of my projects and check if running docker on production would be as easy as running it locally.

One major difference was the way app is served. Locally I just ran dev server inside container but for production use I had to use real server with SSL certificate.

Before I was able to generate SSL certificate I had to setup my containerised app. I have added nginx container customised with my site config to the stack and configured volumes used to populate nginx container with SSL certificate.

Here's a simplified version of docker-compose.yml file I'm using (don't worry about SSL related volumes, we'll get back to them later):

version: "3.7"
services:
  app:
    # app configuration
  db:
    # db configuration
  nginx:
    build:
      context: .
      dockerfile: docker/services/nginx/Dockerfile
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/services/nginx/ssl/dh-param/dhparam.pem:/etc/ssl/certs/dhparam.pem
      - ./docker/services/nginx/ssl/etc/letsencrypt/live/www.my-domain.com/fullchain.pem:/etc/letsencrypt/live/www.my-domain.com/fullchain.pem
      - ./docker/services/nginx/ssl/etc/letsencrypt/live/www.my-domain.com/privkey.pem:/etc/letsencrypt/live/www.my-domain.com/privkey.pem

Next I had to set up SSL configuration in my site config.

server {
    listen 80;
    server_name www.my-domain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name www.my-domain.com;


    # other settings, like location blocks, error pages, etc.


    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    ssl_certificate /etc/letsencrypt/live/www.my-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.my-domain.com/privkey.pem;

    ssl_session_cache shared:le_nginx_SSL:1m;
    ssl_session_timeout 1440m;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS";
}

Almost there, next step is obtaining Let's Encrypt certificate.

To receive a certificate from Let’s Encrypt certificate authority (CA), you must pass a challenge to prove you control each of the domain names that will be listed in the certificate. A challenge is one of a list of specified tasks that only someone who controls the domain should be able to accomplish, such as:

  • Posting a specified file in a specified location on a web site (the HTTP-01 challenge)
  • Posting a specified DNS record in the domain name system (the DNS-01 challenge)

I have decided for DNS challenge. It was easier for me because I'm hosting my site on Digitalocean hence I was able to use dns-digitalocean plugin that automates the whole process. All I needed to do was to create an ini file with my access token.

# DigitalOcean API credentials used by Certbot
dns_digitalocean_token = 111222333444555666777888999000
sudo docker run -it --rm \
-v /code/docker/services/nginx/ssl/etc/letsencrypt:/etc/letsencrypt \
-v /code/docker/services/nginx/ssl/var/lib/letsencrypt:/var/lib/letsencrypt \
-v /home/user/.secrets/certbot/digitalocean.ini:/tmp/digitalocean.ini \
certbot/dns-digitalocean \
certonly --preferred-challenges dns \
--dns-digitalocean --dns-digitalocean-credentials /tmp/digitalocean.ini \
--dns-digitalocean-propagation-seconds 60 \
--email username@example.com --agree-tos --no-eff-email \
-d www.my-domain.com

Last step - DH parameters file.

sudo openssl dhparam -out /code/docker/services/nginx/ssl/dh-param/dhparam.pem 2048

And final touch - renewing the certificate. I have added below command to my crontab to run everyday.

sudo docker run -it --rm \
-v /code/docker/services/nginx/ssl/etc/letsencrypt:/etc/letsencrypt \
-v /code/docker/services/nginx/ssl/var/lib/letsencrypt:/var/lib/letsencrypt \
-v /home/user/.secrets/certbot/digitalocean.ini:/tmp/digitalocean.ini \
certbot/dns-digitalocean \
renew --preferred-challenges dns \
--dns-digitalocean --dns-digitalocean-credentials /tmp/digitalocean.ini \
--dns-digitalocean-propagation-seconds 60

Now, some explanation how it works:

  • certbot container generates SSL certificate and saves it in mounted host directory
    • DNS challenge is automatically executed by dns-digitalocean plugin
    • plugin creates TXT record required by Let's Encrypt. In order to access Digitalocean API plugin uses access token saved in ini file.
  • host directory used by certbot container is mounted in nginx container
  • certificate and DH parameters paths are used in nginx configuration file to configure SLL certificate

And this is it. In few simple steps I was able to obtain SSL certificate for my containerised nginx server.