Finnian Anderson

My hackerspace

SSL with Docker Swarm, Let's Encrypt and Nginx

A couple of weeks ago, Let's Encrypt announced that support for wildcard certificates was coming in Jan 2018 which got me and my devops friends very excited. Currently with LE, you have to specify all the domains (including www) you want to include in the certificate which is really annoying. With wildcard certificates, this limitation will be gone and you'll be able to create one certificate for all the different subdomains. 🙌

Getting SSL to work with Docker and Let's Encrypt has been one of my short term goals recently. I started researching and found that there are some convoluted ways of doing it which involve tying in lots of other services into your stack which you don't need. 👎

I've found a very simple way of doing it using Docker volumes. It must be noted that this method only works if the nginx service is deployed to a manager node. The reason for this is that we have to store the certificates somewhere, so putting them on a manager is the only reliable way to do that. We can then deploy our nginx service to that manager so it can access the certs. 💪

If you want to deploy multiple replicas of nginx (prevented by the mode: global property in compose), you must make sure that all the certificates are on all the managers. This is because Docker doesn't share volumes around the swarm, they're attached to each individual host.

How do we do it?

Step 1: generate certificates

Run this docker command on your manager(s) so that they can obtain the certificates from Let's Encrypt. Make sure to substitute your email and domain below.

docker run --rm \  
  -p 443:443 -p 80:80 --name letsencrypt \
  -v "/etc/letsencrypt:/etc/letsencrypt" \
  -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
  certbot/certbot certonly -n \
  -m "YOUR_EMAIL" \
  -d example.com \
  --standalone --agree-tos

Step 2: deploy the stack

# docker-compose.yml

version: '3.2'

services:  
  nginx:
    image: nginx:stable-alpine
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
      - /usr/share/nginx/html:/usr/share/nginx/html
      - ${PWD}/nginx.conf:/etc/nginx/nginx.conf
    deploy:
      mode: global
      placement:
        constraints:
          - node.role == manager
    ports:
      - 80:80
      - 443:443
# nginx.conf

user nginx;  
worker_processes 1;

error_log /var/log/nginx/error.log warn;  
pid /var/run/nginx.pid;


events {  
  worker_connections 1024;
}


http {  
  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

  access_log /dev/stdout main;
  sendfile on;
  keepalive_timeout 65;

  server {
    # redirect from http to https
    listen 80;
    server_name  _;
    return 301 https://$host$request_uri;
  }

  server {
    listen              443 ssl;
    server_name         example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location ^~ /.well-known/ {
        # allow LE to validate the domain
        root   /usr/share/nginx/html;
        allow all;
    }

    location / {
       # do your thing
    }
  }
}

That's it! Once you deploy the stack, you'll be able to access your service over HTTPS 👊. You can see an example of this in production at subr.pw. You just got SSL for free on your Docker swarm 🐳 🙌.

Step 3: configure automatic renewal

The certs from LE expire after 90 days which means that your visitors will get an error if they try to access your site after this time. Don't worry though, LE have made it super easy to renew the certs via a little utility called certbot. There is a version of this for Docker too, so renewing your certificates is as simple as:

docker run --rm --name letsencrypt \  
    -v "/etc/letsencrypt:/etc/letsencrypt" \
    -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
    -v "/usr/share/nginx/html:/usr/share/nginx/html" \
    certbot/certbot:latest \
    renew --quiet

I would suggest putting this into a crontab to run periodically (don't forget this needs to be on all your managers!). Here's an example to renew any of the certs if necessary at 00:19 & 12:19 each day:

19 0,12 * * * /root/certbot_updateall.sh  

Conclusion

This method seems to work really well and the only limitation I can see is that in order to generate a new certificate, you have to take nginx offline so that it doesn't hog ports 80 & 443 (we need to use them for validation). Note that this doesn't apply to renewing the certificates, only adding a new one. If anyone has a nice way of getting around this, let me know below or on Twitter @developius ❤️

Finnian Anderson

Node.js, MongoDB, Express, PHP, MySQL, Raspberry Pi, Arduino and other such things. Sailing instructor and Docker fan!

Suffolk, UKhttps://finnian.io

Subscribe to Finnian Anderson

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!