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

I am running an Express-based website in a Docker container based on the Node.js image. How do I use Let’s Encrypt with a container based on that image?

  • Expose container on random port on internal IP
  • Docker container only accessible by another
  • How to create a DB for MongoDB container on start up?
  • How the docker git repo is showing different tags in pull requests like status/need-review
  • Create Docker container with both Java and Node.js
  • LXC integration and auto-scaling with Mesos
  • INFO hdfs.HDFSEventSink: Writer callback called
  • docker: unauthorized: authentication required
  • How to run yum inside a docker container on a partially isolated host?
  • How do I run yesod init from a docker container?
  • docker start a container after stopping
  • Docker java:7 image apt-get update cyclic dependencies
  • 5 Solutions collect form web for “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:onbuild
    EXPOSE 3000
    CMD 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 MB
    Step 1 : FROM node:onbuild
    # Executing 3 build triggers...
    Step 1 : COPY package.json /usr/src/app/
    Step 1 : RUN npm install
     ---> Running in 981ca7cb7256
    npm info it worked if it ends with ok
    <snip>
    npm info ok
    Step 1 : COPY . /usr/src/app
     ---> cf82ea76e369
    Removing intermediate container ccd3f79f8de3
    Removing intermediate container 391d27f33348
    Removing intermediate container 1c4feaccd08e
    Step 2 : EXPOSE 3000
     ---> Running in 408ac1c8bbd8
     ---> c65c7e1bdb94
    Removing intermediate container 408ac1c8bbd8
    Step 3 : CMD node app.js
     ---> Running in f882a3a126b0
     ---> 5f0f03885df0
    Removing intermediate container f882a3a126b0
    Successfully built 5f0f03885df0
    

    Running this image works like this:

    $ docker run -d --name helloworld -p 3000:3000 exampleexpress
    $ curl 127.0.0.1:3000
    Hello 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.com
    
    docker 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:alpine
    COPY 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 exampleexpress
    docker 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=jefferya@programmerq.net
    export DNSNAME=www.example.com
    
    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" \
        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.

    You may have a look here : https://certbot.eff.org/docs/using.html?highlight=docker#running-with-docker

    Then what I personally do is :

    1. Create a Docker volume to store the certs and generate the certs with the above image
    2. Create a Docker user-defined network (https://docs.docker.com/engine/userguide/networking/#/user-defined-networks)
    3. Create an image based on nginx with your configuration (maybe this will be useful)
    4. Create a Nginx container based on your image, mount the volume in it and connect it to the network (also forward port 80 and 443 to whatever you want)
    5. I would create a container for your node.js app and connect it to the same network

    Now if you configured nginx correctly (point to the right path for the TLS certs and proxy to the right URL, like http://my-app:3210) you should have access to your app in https.

    Front end – NGINX – which listening 443 port, and proxies to beck end

    Back end – you docker container

    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)
    Docker will be the best open platform for developers and sysadmins to build, ship, and run distributed applications.