How to use Let's Encrypt with Docker container based on the Node.js image How to use Let's Encrypt with Docker container based on the Node.js image docker docker

How to use Let's Encrypt with Docker container based on the Node.js image


The first thing I've done is to create a simple express-based docker image.

I am using the following app.js, taken from express's hello world example in their docs:

var express = require('express');var app = express();app.get('/', function (req, res) {  res.send('Hello World!');});app.listen(3000, function () {  console.log('Example app listening on port 3000!');});

I also ended up with the following packages.json file after running their npm init in the same doc:

{  "name": "exampleexpress",  "version": "1.0.0",  "description": "",  "main": "app.js",  "scripts": {    "test": "echo \"Error: no test specified\" && exit 1"  },  "author": "",  "license": "ISC",  "dependencies": {    "express": "^4.14.0"  }}

I've created the following Dockerfile:

FROM node:onbuildEXPOSE 3000CMD node app.js

Here's the output when I do my docker build step. I've removed most of the npm install output for brevity sake:

$ docker build -t exampleexpress .Sending build context to Docker daemon 1.262 MBStep 1 : FROM node:onbuild# Executing 3 build triggers...Step 1 : COPY package.json /usr/src/app/Step 1 : RUN npm install ---> Running in 981ca7cb7256npm info it worked if it ends with ok<snip>npm info okStep 1 : COPY . /usr/src/app ---> cf82ea76e369Removing intermediate container ccd3f79f8de3Removing intermediate container 391d27f33348Removing intermediate container 1c4feaccd08eStep 2 : EXPOSE 3000 ---> Running in 408ac1c8bbd8 ---> c65c7e1bdb94Removing intermediate container 408ac1c8bbd8Step 3 : CMD node app.js ---> Running in f882a3a126b0 ---> 5f0f03885df0Removing intermediate container f882a3a126b0Successfully built 5f0f03885df0

Running this image works like this:

$ docker run -d --name helloworld -p 3000:3000 exampleexpress$ curl 127.0.0.1:3000Hello World!

We can clean this up by doing: docker rm -f helloworld


Now, I've got my very basic express-based website running in a Docker container, but it doesn't yet have any TLS set up. Looking again at the expressjs docs, the security best practice when using TLS is to use nginx.

Since I want to introduce a new component (nginx), I'll do that with a second container.

Since nginx will need some certificates to work with, let's go ahead and generate those with the letsencrypt client. The letsencrypt docs on how to use letsencrypt in Docker can be found here: http://letsencrypt.readthedocs.io/en/latest/using.html#running-with-docker

Run the following commands to generate the initial certificates. You will need to run this on a system that is connected to the public internet, and has port 80/443 reachable from the letsencrypt servers. You'll also need to have your DNS name set up and pointing to the box that you run this on:

export LETSENCRYPT_EMAIL=<youremailaddress>export DNSNAME=www.example.comdocker run --rm \    -p 443:443 -p 80:80 --name letsencrypt \    -v "/etc/letsencrypt:/etc/letsencrypt" \    -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \    quay.io/letsencrypt/letsencrypt:latest \    certonly -n -m $LETSENCRYPT_EMAIL -d $DNSNAME --standalone --agree-tos

Make sure to replace the values for LETSENCRYPT_EMAIL and DNSNAME. The email address is used for expiration notifications.


Now, let's set up an nginx server that will make use of this newly generated certificate. First, we'll need an nginx config file that is configured for TLS:

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 {        listen       80;        server_name  _;        return 301 https://$host$request_uri;    }    server {        listen              443 ssl;        #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;        server_name         www.example.com;        ssl_certificate     /etc/letsencrypt/live/www.example.com/fullchain.pem;        ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;        ssl_ciphers         HIGH:!aNULL:!MD5;        location ^~ /.well-known/ {            root   /usr/share/nginx/html;            allow all;        }        location / {            proxy_set_header Host $host;            proxy_set_header X-Real-IP $remote_addr;            proxy_pass http://expresshelloworld:3000;        }    }}

We can put this config file into our own custom nginx image with the following Dockerfile:

FROM nginx:alpineCOPY nginx.conf /etc/nginx/nginx.conf

This can be build with the following command: docker build -t expressnginx .

Next, we'll create a custom network so we can take advantage of Docker's service discovery feature:

docker network create -d bridge expressnet

Now, we can fire up the helloworld and nginx containers:

docker run -d \    --name expresshelloworld --net expressnet exampleexpressdocker run -d -p 80:80 -p 443:443 \    --name expressnginx --net expressnet \    -v /etc/letsencrypt:/etc/letsencrypt \    -v /usr/share/nginx/html:/usr/share/nginx/html \    expressnginx

Double check that nginx came up properly by taking a look at the output of docker logs expressnginx.

The nginx config file should redirect any requests on port 80 over to port 443. We can test that by running the following:

curl -v http://www.example.com/

We should also, at this point, be able to make a successful TLS connection, and see our Hello World! response back:

curl -v https://www.example.com/

Now, to set up the renewal process. The nginx.conf above has provisions for the letsencrypt .well-known path for the webroot verification method. If you run the following command, it will handle renewal. Normally, you'll run this command on some sort of cron so that your certs will be renewed before they expire:

export LETSENCRYPT_EMAIL=me@example.comexport DNSNAME=www.example.comdocker 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" \    quay.io/letsencrypt/letsencrypt:latest \    certonly -n --webroot -w /usr/share/nginx/html -d $DNSNAME --agree-tos


There are many ways to achieve this depending on your setup. One popular way is to setup nginx in front of your Docker container, and handle the certificates entirely within your nginx config.

The nginx config can contain a list of 'usptreams' (your Docker containers) and 'servers' which essentially map requests to particular upstreams. As part of that mapping you can also handle SSL.

You can use certbot to help you set this up.


I've recently implemented https with let's encrypt using nginx. I'm listing the challenges I've faced, and the way I've implemented step-by-step here.

Challenge:

  1. Docker file system is ephemeral. That means after each time you make a build the certificates that are stored or if generated inside the container, will vanish. So it's very tricky to generate certificates inside the container.

Steps to overcome it:

Below guide is independent of kind of the app you have, as it only involves nginx and docker.

  • First install nginx on you server (not on container, but directly on the server.) You can follow this guide to generate certificate for your domain using certbot.
  • Now stop this nginx server and start the build of your app. Install nginx on your container and open port 80, 443 on your docker container. (if using aws open on ec2 instance also as by default aws open only port 80)

  • Next run your container and mount the volumes that contain certificate file directly on the container. I've answered a question here on how to do the same.

  • This will enable https on your app. Incase you are not able to observe, and are using chrome try clearing dns cache for chrome

Auto renewal process :

  • Let's encrypt certificates are valid only for 3 months. In the above guide steps to configure auto renewal is also setup. But you've to stop and restart your container every 3 months atleast to make sure the certificates mounted on your docker container are up to date. (You will have to restart the nginx server we set up in the first step to make the renewal happen smoothly)