Tevin Jeffrey
Tevin Jeffrey

Thoughts and stories.

Tevin Jeffrey
Author

My name is Tevin Jeffrey and I'm a Software Engineer. This is my blog about android, software architecture, user interfaces, user experience and sometimes video games.

Share


Twitter


Tevin Jeffrey

How to setup NGINX with automatic HTTPS in Docker

Tevin JeffreyTevin Jeffrey

In this article you'll learn how to setup NGINX with automatic SSL/TLS certificate creation/renewal with Docker. We will create a service utilizing the jwilder/nginx-proxy image and it's Let's Encrypt companion image create this service.

When your container is added to the docker engine, this service will automatically generate the appropriate HTTP and HTTPS NGINX configurations. Multi-host networking is outside of the scope of this article.

See Automated Nginx Reverse Proxy for Docker for why you might want to use this.

Prerequisites

It goes without saying, any container you wish to be proxied by NGINX must be connected to the same subnet as NGINX. Additionally, any domain you aim to use must have its DNS records setup to point to the host machine.

Let's get started

It is important to note that by default, Docker Compose version 2 isolates independent projects from each other by creating a separate network named [projectname]_default using the bridge driver. On the other hand, containers created with docker run will be connected to the default bridge network. You can use the --net flag to override this behavior.

It's not necessary, but to illustrate a point let's first create a network for all of our containers to connect to.

$ docker network create nginx-net
86acd4edf102a2a5f9aaa6ff2638e5372eeffd9dd39070ee97ee1ac252a29492  

Let's verify that the network was indeed created.

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE  
10e0a639b99c        bridge              bridge              local  
a36328b4394c        host                host                local  
86acd4edf102        nginx-net           bridge              local  
320ca3e56cf0        none                null                local  

Let's begin by starting the container for the nginx-proxy using this command.

$ docker run --name nginx-proxy -p "80:80" -p "443:443" \
    -v "/etc/nginx/vhost.d" -v "/usr/share/nginx/html" \
    -v "/etc/nginx/certs:/etc/nginx/certs:ro" \
    -v "/var/run/docker.sock:/tmp/docker.sock:ro" \
    --net "nginx-net" -d jwilder/nginx-proxy

This creates a container named nginx-proxy that listens externally on port 80 and 443 of the host machine. It connects to the nginx-net network we created and mounts /var/run/docker.sock for service discovery. This socket is necessary for nginx-proxy to know when and what containers started and stopped in order to reconfigure itself.

The :ro at end of a path tells docker to mount the volume as read-only. Here, nginx-proxy only needs the ability to read certificates from /etc/nginx/certs.

Now let's create a container for automatic certificate creation/renewal utilizing jrcs/letsencrypt-nginx-proxy-companion. As it's name suggest it's a companion to nginx-proxy.

$ docker run --name "letsencrypt-nginx-proxy" \
    --volumes-from "nginx-proxy" \
    -v "/etc/nginx/certs:/etc/nginx/certs" \
    -v "/var/run/docker.sock:/var/run/docker.sock:ro" \
    --net "nginx-net" -d jrcs/letsencrypt-nginx-proxy-companion

This command creates a container named letsencrypt-nginx-proxy that mounts all volumes from the nginx-proxy container into itself. By declaring the volume /etc/nginx/certs:/etc/nginx/certs on this new container we get both read and write access to the /etc/nginx/certs directory.

To confirm both container are up and running with docker ps

$ docker ps
CONTAINER ID        IMAGE                                    COMMAND                  CREATED             STATUS              PORTS                                      NAMES  
0201227422b3        jrcs/letsencrypt-nginx-proxy-companion   "/bin/bash /app/entry"   2 seconds ago       Up 1 seconds                                                   letsencrypt-nginx-proxy  
07eedfe456e6        jwilder/nginx-proxy                      "/app/docker-entrypoi"   3 seconds ago       Up 2 seconds        0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   nginx-proxy

If you'd like to use Docker Compose, here's a docker-compose.yml packaging both images to create this service.

$ cat docker-compose.yml

version: '2'  
services:  
  nginx-proxy:
    image: jwilder/nginx-proxy
    container_name: nginx-proxy
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - '/etc/nginx/vhost.d'
      - '/usr/share/nginx/html'
      - '/etc/nginx/certs:/etc/nginx/certs:ro' 
      - '/var/run/docker.sock:/tmp/docker.sock:ro'

  letsencrypt-nginx-proxy:
    container_name: letsencrypt-nginx-proxy
    image: 'jrcs/letsencrypt-nginx-proxy-companion'
    volumes:
      - '/etc/nginx/certs:/etc/nginx/certs'
      - '/var/run/docker.sock:/var/run/docker.sock:ro'
    volumes_from:
      - nginx-proxy

networks:  
  default:
    external:
      name: nginx-net

Bring it up with:

$ docker-compose up -d
Creating nginx-proxy  
Creating letsencrypt-nginx-proxy  

You can optionally view container logs with:

$ docker-compose logs -f

Here's my logs if you need a sanity check.

Configuring your containers

To set up a container so that our NGINX+LetsEncrypt service can automatically discover and generate proxy reverse configurations, the target container must first set some environment variables.

The VIRTUAL_HOST=yourdomain.tld environment variable is required for nginx-proxy to be able to determine if the container needs to be proxied. The value of VIRTUAL_HOST should be a fully qualified domain (FQDM) pointing to your host. It will be used in the server_name directive in the generated NGINX config.

The containers being proxied must expose the port to be proxied, either by using the EXPOSE directive in their Dockerfile or by using the --expose flag to docker run or docker create.

If your container exposes multiple ports, you'll need to give an extra hint to nginx-proxy in form of the VIRTUAL_PORT env. Currently there's no support for multiple port mappings. Keep an eye on this PR if you're interested.

For HTTPS, you need two additional environment variables, LETSENCRYPT_EMAIL and
LETSENCRYPT_HOST. Let's Encrypt needs your email to contact you if they need to and the host, or course, to know what domain to issue the certificate for. LETSENCRYPT_HOST in most cases will be the same as your VIRTUAL_HOST

There are far more configuration options than what I described here. For more information, check out their respective Github pages.
nginx-proxy and Let's Encrypt Companion

Pulling it all together, if you have a container defined like this:

$ docker run --name blog -d -e VIRTUAL_HOST=blog.example.com \
    --expose 2368 gold/ghost

The nginx-proxy container will generate a nginx configuration like the on below just a few seconds after you run the command above.

....
upstream blog.example.com {  
    # blog
    server 172.18.0.5:2368;
}
server {  
    server_name blog.example.com;
    listen 80;
    location / {
        proxy_pass http://blog.example.com;
    }
}

If multiple containers made use of the same VIRTUAL_HOST then the upstream block would look more like this.

upstream blog.example.com {  
    # blog
    server 172.18.0.5:2368;
    server 172.18.0.6:2368;
    server 172.18.0.7:2368;
    server 172.18.0.8:2368;
}

This makes use of NGINX's built in load balancing which defaults to a round-robin scheduler.

For a configuration that makes you use of automatic Let's Encrypt certificates, you must set the LETSENCRYPT_EMAIL and
LETSENCRYPT_HOST environment variables like this:

$ docker run --name blog -d -e VIRTUAL_HOST=blog.example.com \
    -e LETSENCRYPT_HOST=blog.example.com \
    -e LETSENCRYPT_EMAIL=sysadmin@example.com \
    --net nginx-net \
    --expose 2368 gold/ghost

If all goes well in the Let's Encrypt dance, nginx-proxy will generate and reload NGINX with a config file looking roughly like this:

...
upstream blog.example.com {  
    # blog
    server 172.18.0.5:2368;
}
server {  
    server_name blog.example.com;
    listen 80 ;
    access_log /var/log/nginx/access.log vhost;
    return 301 https://$host$request_uri;
}
server {  
    server_name blog.example.com;
    listen 443 ssl http2 ;
    access_log /var/log/nginx/access.log;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-....... truncated';
    ssl_prefer_server_ciphers on;
    ssl_session_timeout 5m;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    ssl_certificate /etc/nginx/certs/blog.example.com.crt;
    ssl_certificate_key /etc/nginx/certs/blog.example.com.key;
    ssl_dhparam /etc/nginx/certs/blog.example.com.dhparam.pem;
    location / {
        proxy_pass http://blog.example.com;
    }
}
...

By default, it generates a configuration that redirects HTTP traffic to HTTPS. It also uses a Strong 2048 bit Diffie-Hellman Group (for having an A+ Rate on the Qualsys SSL Server Test) that was created when the container first launched.

The SSL cipher configuration is based on mozilla nginx intermediate profile which should provide compatibility with clients back to Firefox 1, Chrome 1, IE 7, Opera 5, Safari 1, Windows XP IE8, Android 2.3, Java 7. The configuration also enables HSTS, and SSL session caches.

For a real world example of this service in action, read my other post about setting up a Ghost blog in Docker

Tevin Jeffrey
Author

Tevin Jeffrey

My name is Tevin Jeffrey and I'm a Software Engineer. This is my blog about android, software architecture, user interfaces, user experience and sometimes video games.

Comments