Setup SSL with Docker, NGINX and Lets Encrypt

6 Comments

Did you ever want to secure your application with the HTTPS protocol? This guide will show you how to run your applications behind a reverse proxy and secure the communication with HTTPS by using Docker, NGINX, and Lets Encrypt.

There is a updated version of this guide in wich I will teach you how to setup an simpler and automated process. You can find it here.

Used Technologies

With these three technologies, you can create a secure environment to publish your applications to the web. NGINX will be the entry point for users from the web to access the different applications. The SSL certificates are needed to use HTTPS as a communication protocol between your server and the clients.
Docker itself will host NGINX, your applications, and a service to generate new Lets Encrypt certificates automatically.

To follow this guide, you need a domain, and you need to install docker and docker-compose for your system!

Steps

You can receive SSL certificates for any application you want with the following steps.

  1. Create your application with Docker
  2. Create a reverse proxy with NGINX
  3. Automate SSL certificates with Certbot

Create your application with Docker

The first step is to use docker-compose to create a container for your application. I will use the blog from my last post for this.

services:
    blog:
        container_name: blog
        image: node
        ports:
            - 3000:3000
        volumes:
            - ./guide-blog/:/app/
        command: bash -c "cd /app && node build"

The tasks of the different attributes are:

Now you can create the container by running:

docker-compose up -d blog

After this, you can reach your application on the host machine by entering the URL with port 3000. Don’t make your port available if you run this on a machine on the Internet because it is unsecured. To make this secure will create a reverse proxy with NGINX and put it in front of our applications.

Create a reverse proxy with NGINX

First, we will create a configuration to pass the requests to our node application via NGINX. Therefore we will make the directory nginx containing the file nginx.conf. With this file, we configure the NGINX instance.

events {
    # worker_connections  1024;
}

http {
    server_tokens off;
    charset utf-8;

    server {
        listen 80 default_server;

        server_name _;

        location / {
            proxy_pass http://blog:3000/;
        }
    }
}

The event section is needed to run NGINX. The http section is the interesting part for us. First, the server is defined to listen to all requests on port 80 and is set as a default_server for all requests to this host. After that, we say each server name is valid for requests. In a later part, you can define different domains that are used for a server.
In the location / we define that every request should be passed to our blog container with port 3000. It is important to say that the specified port is not the public but the private port. Therefore we will remove the port mapping in the docker-compose.yml file.
Additionally, we will create an NGINX container in the same file.

services:
    blog:
        container_name: blog
        image: node
        volumes:
            - ./guide-blog/:/app/
        command: bash -c "cd /app && node build"

    nginx:
        container_name: nginx
        restart: unless-stopped
        image: nginx
        ports:
            - 80:80
            - 443:443
        volumes:
            - ./nginx/nginx.conf:/etc/nginx/nginx.conf

We will make ports 80 and 443 public because these are used for HTTP and HTTPS.
The volume is used to make the configuration available inside the container and to persist any changes.

You can create and update the container by running:

docker-compose up -d

After this, you should be able to open the application by entering the URL to the host. In my case, it looks like this.

localhost-blog

Now we have setup a reverse proxy for our application. The next step will be to get SSL certificates and redirect all HTTP requests to HTTPS.

Automate SSL certificates with Certbot

Let’s Encrypt works with challenges to check if the domain and the host are eligible. For the challenges, we have to create a route called /.well-known/acme-challenge/ in the NGINX configuration. This will be part of the HTTP section, but we will move it there after switching to HTTPS, so the automated renewal works as expected.

    server {
        listen 80 default_server;

        .
        .
        .

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

As you can see, it points to the directory /var/www/certbot. Therefore, we need to add two volumes to make the challenges and the resulting certificates available in the NGINX container.

- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot

With this done, we have to recreate the container:

docker-compose up -d nginx

After configuring NGINX, we can retrieve the certificates. To do this, we will use the certbot container and run it with a set of parameters.

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes: 
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    command: certonly --webroot -w /var/www/certbot --force-renewal --email {email} -d {domain} --agree-tos

The parameters are used to automate the process without you needing to manually enter the data on every container run.

To request the certificates, we have to create the container with:

docker-compose up -d certbot

By running the command docker logs certbot you can see if everything worked out and if you received your certificate. After you receive it, you have to include the certificate in nginx.conf.

Configure HTTPS in NGINX

In the first step, we redirect all HTTP requests to HTTPS, and in the second step, we create the HTTPS section for our application:

http {
    server_tokens off;
    charset utf-8;

    # always redirect to https
    server {
        listen 80 default_server;

        server_name _;

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

    server {
        listen 443 ssl http2;
        # use the certificates
        ssl_certificate     /etc/letsencrypt/live/{domain}/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/{domain}/privkey.pem;
        server_name {domain};
        root /var/www/html;
        index index.php index.html index.htm;


        location / {
            proxy_pass http://blog:3000/;
        }

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

Another thing you can see is that we moved the challenge route to the HTTPS section. We need to do this because all new requests of Lets Encrypt will also be redirected to HTTPS.

Automated renewal with crontab

The last step is to automatically renew the certificates before they run out. A certificate has a lifetime of 90 days, and it is recommended to update them after a timespan of 60 days. Therefore, we need to rerun our certbot container every 60 days to renew the certificates. I will accomplish this by using crontab.
A crontab can be created on linux systems by running:

crontab -e

And adding a line with the following structure:

0 5 1 */2 *  /usr/local/bin/docker-compose up -f /var/docker/docker-compose.yml certbot

The command means: Run docker-compose up -d at 5 am on the first day every 2nd month.

After implementing this guide, you should have an application behind a reverse proxy accessible via the HTTPS protocol!
Thank you for reading my guide. If you have any questions feel free to shoot me an email at mail@programonaut.com, and I will try to help you!

In case you liked this post consider subscribing to my newsletter. You will also receive a free docker-compose cheat sheet!

Discussion (6)

Add Comment

Your email address will not be published. Required fields are marked *