Let’s Encrypt With Docker

Let’s Encrypt is spreading the world with a good news : it has never been that cheap and easy to setup HTTPS on your website. In this blog post we’re going to see how to integrate it with Docker.

About Let’s Encrypt

If you’ve already enabled HTTPS on a public website, you certainly had to :

  1. pay a fair amount of money
  2. follow instructions on emails, deal with dashboards, maybe even scan and send some documents

With Let’s Encrypt, no more!

One of the reasons why certificates are so expensive is that they may involve manual work from the certificate authority end, this is especially true when you purchase extended validation certificates.

Let’s Encrypt is a certificate authority which focuses on domain validation, they automated the whole process and made some specifications around it. They even open-sourced a client implementing the specs, and last but not the least : their services are completely free.

isrg-keysLet’s Encrypt intermediate authorities (such as Let’s Encrypt Authority X3) are here to issue and manage certificates. Since they’ve received cross-signatures from IdenTrust, Let’s Encrypt is pretty much compatible with all major browsers.

Ready for the challenge?

To get a certificate for your domain, you need to prove to the certificate authority that you own the domain, we call this a challenge.

With Lets Encrypt, there are 3 different types of challenges you can choose. In this tutorial we’re going for the HTTP challenge.

The HTTP challenge can be completed by putting a given ressource under a well-known location (http://my.example.org/.well-known/acme-challenge/{random_token}).

Fortunately most of it will be automated by Certbot, we’ll basically only have to tweak the webserver configuration a bit.

The setup

Here I’m going to assume a single server setup. If you run a cluster the following instructions would have to be adapted, but the idea and the Docker images remain the same.

Docker images

We’re going to need 3 Docker images.

1) A web application

The web application we want to enable HTTPS for.

As an example I’ll use dockercloud/hello-world. It’s just “hello world” as a web page, mainly useful for testing. But this should typically be your web application image. The only requirement for it is to accept HTTP requests (or a related protocol like FastCGI).

2) Certbot

This is the open-source client for ACME compliant CAs we’ve talked about earlier. We’re gonna use it to manage our certificates and solve the HTTP challenge.

From a Debian Jessie image, we can install the “letsencrypt” package (available in the backports repo).

FROM debian:jessie-backports

RUN apt-get update \
  && apt-get install -y letsencrypt -t jessie-backports \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /etc/letsencrypt/live/my.example.org \
  && openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout /etc/letsencrypt/live/my.example.org/privkey.pem \
    -out /etc/letsencrypt/live/my.example.org/fullchain.pem \
    -subj /CN=my.example.org

The last command is generating a self-signed certificate, I’ll get back into this in the next section.

3) A proxy

A proxy will sit on both ports 80 and 443.

The HTTPS traffic (port 443) will get forwarded to the web application, while the HTTP traffic (port 80) will match one of those scenarios :

  1. The path starts by /.well-known/acme-challenge, it is Let’s Encrypt trying to validate the HTTP challenge. A webroot managed by Certbot will be served.
  2. The path doesn’t start by /.well-known/acme-challenge, this might be a regular visitor or a SEO bot. It is important to catch that traffic and make a permanent redirection to its HTTPS equivalent.

Nginx is a perfect fit for this, we can add a custom config to the official image.

FROM nginx:stable
COPY app.conf /etc/nginx/conf.d/

The app.conf would look like this :

server {
  listen 80;
  server_name my.example.org;

  location /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
  }

  location / {
    return 301 https://$host$request_uri;
  }
}

server {
  listen 443 ssl;

  ssl_certificate certs/live/my.example.org/fullchain.pem;
  ssl_certificate_key certs/live/my.example.org/privkey.pem;

  server_name my.example.org;

  location / {
    proxy_pass http://my.example.org;
  }
}

Nginx won’t boot if the certificate / private key are missing, that’s why we previously created a “dummy” certificate in the Certbot image. It’ll be used until the real Let’s Encrypt certificate is generated. This is of course a hacky solution for the sake of simplicity in this tutorial. On a real-world setup you’d have to come up with an intermediate Nginx config which doesn’t use HTTPS for the app but supports the HTTP challenge, then once your certificate is generated you could use the final config.

Docker Compose

Docker Compose provide a simple way to describe and run a multi-container application.

Here is a working docker-compose.yml for our setup :

version: '2'

services:
  proxy:
    image: my/nginx
    volumes:
      - letsencrypt_certs:/etc/nginx/certs
      - letsencrypt_www:/var/www/letsencrypt
    links:
      - app:my.example.org
    ports:
      - "80:80"
      - "443:443"
    restart: always

  letsencrypt:
    image: my/letsencrypt
    command: /bin/true
    volumes:
      - letsencrypt_certs:/etc/letsencrypt
      - letsencrypt_www:/var/www/letsencrypt

  app:
    image: dockercloud/hello-world
    restart: always

volumes:
  letsencrypt_certs: ~
  letsencrypt_www: ~

Nothing fancy here, we’re just linking our containers together. There are also 2 named volumes :

  • “letsencrypt_certs”, where certificates will be put.
  • “letsencrypt_www”, for the HTTP challenge.

First time generation

When running the app for the first time, you’d have to generate a certificate. This can be done like this :

docker-compose run --rm letsencrypt \
  letsencrypt certonly --webroot \
  --email me@example.org --agree-tos \
  -w /var/www/letsencrypt -d my.example.org

Then you have to reload nginx :

docker-compose kill -s SIGHUP proxy

Renewal

Let’s Encrypt certificates are valid for 3 months, they’d have to be renewed periodically with the following command :

docker-compose run --rm letsencrypt letsencrypt renew

After this command you also have to reload Nginx, as shown previously. It renews certificates which are expiring in less than 30 days, you’d typically want to set it as a cron (running every week for example).

Going further

That’s pretty much it for this tutorial! You can find the full setup here and a live example here.

This was a quick walkthrough, mainly extracted from how I run this blog. It can however be improved, here are a few leads.

Development

In order to have a development environment as close as possible to production. You should also enable HTTPS there, you could remove the self-signed certificate creation from the Certbot image and instead put it as the default command in your development docker-compose.yml file.

Nginx configuration

In this tutorial I’ve kept the nginx configuration simple, but next to the proxy_pass directive you should probably add X-Forwarded-For and X-Forwarded-Proto headers, for more security you could also consider disabling weak ciphers and use Diffie-Hellman key exchanges.

Deployment

If you have high-availabilty / zero-downtime constraints for your application, the setup should be adapted.

The main bottleneck being the static configuration inside the nginx container, you could integrate a tool like docker-gen. As it’s a vast topic I’ll probably make a dedicated blog post about Docker deployments.