Clean Development URLs and SSL with Docker and Nginx Proxy

When developing microservices with docker containers, I often find myself trying to remember all of the different ports exposed by each service. Fortunately, there is a way to provide memorable, clean urls for each of your service endpoints.

By leveraging an Nginx Proxy in one of your containers, we can easily assign subdomains (e.g. service-a.mydomain.com) to each of your microservice projects.

I'll also show you how to dynamically generate a development certificate and load it into the proxy so that you will be able to connect securely to your services with SSL.

Source Files

All files are available in the repository: https://github.com/christophla/blog-docker-nginx-proxy

Both Bash and PowerShell scripts are provided for Windows, OSX, and Linux.

The Docker Compose File

Jason Wilder has created a great docker image, jwilder/nginx-proxy, that contains an instance of Nginx that automatically wires up a reverse proxy with host-headers for any docker containers that include the environmental variable VIRTUAL_HOST.

In the sample code, I've included a dotnet core webapi project as a service, along with an instance of nginx-proxy. The service nginx-proxy-app sets its virtual host as myapp.nginx-proxy-app.com.

version: '3'

services:

  nginx-proxy-app:
    container_name: nginx-proxy-app
    image: nginx-proxy-app
    build: 
      context: .
      dockerfile: Dockerfile
    environment:
      - ASPNETCORE_ENVIRONMENT=development
      - REMOTE_DEBUGGING=${REMOTE_DEBUGGING}
      - VIRTUAL_HOST=myapp.nginx-proxy-app.com
    ports:
      - "5000:80"
    networks:
      - dev-network
    tty: true
    stdin_open: true

  nginx-proxy-nat:
    image: jwilder/nginx-proxy
    container_name: nginx-proxy-nat
    environment:
      - HSTS=off
      - HTTPS_METHOD=noredirect
    ports:
      - "80:80"
      - "443:443"
    networks:
      - dev-network
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./certs:/etc/nginx/certs

networks:
  dev-network:
    driver: bridge
docker-compose.yml


The Setup Script

Our script is going to have to perform several things in order to setup our project to be able to use hostnames and SSL. First, we will remove any existing certificates generated for our custom domain. This will allow us to generate a new certificate when we add or remove services (and hostnames) to our development environment. Next, we will generate a certificate and add it to the docker volume ./certs:/etc/nginx/certs so that Nginx can pick it up for SSL. Last, we will add our hostnames to the local machine etc/hosts file..

setupProxy () {

    # remove existing certificates
    echo -e "${YELLOW} Removing existings certificates... ${RESTORE}"
    sudo security delete-certificate -c $certificatePrefix /Library/Keychains/System.keychain

    # generate key
    openssl \
        genrsa \
        -out certs/$certificatePrefix.key \
        4096

    # generate csr request
    openssl \
        req \
        -new \
        -sha256 \
        -out certs/$certificatePrefix.csr \
        -key certs/$certificatePrefix.key \
        -config openssl-san.conf

    #generate certificate from csr request
    openssl \
        x509 \
        -req \
        -days 3650 \
        -in certs/$certificatePrefix.csr \
        -signkey certs/$certificatePrefix.key \
        -out certs/$certificatePrefix.crt \
        -extensions req_ext \
        -extfile openssl-san.conf

    # generate pem
    cat certs/$certificatePrefix.crt certs/$certificatePrefix.key > certs/$certificatePrefix.pem

    # install certificate
    if [ -f certs/$certificatePrefix.crt ]; then
        echo -e "${YELLOW} Installing certificate... ${RESTORE}"
        sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certs/$certificatePrefix.crt
    else
        echo -e "${RED} An error occurred while generating the certificate: certs/$certificatePrefix.crt ${RESTORE}"
    fi
    
    # Write Hosts
    addHost "myapp.nginx-proxy-app.com"

}
project-tasks.sh

Since Chrome 58, self signed wildcard certificates are not supported without Subject Alternative Names (SANs). As a result, we will will use the config file openssl-san.conf to include each of our service hostnames.

[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C=US
ST=Texas
L=Austin
O=Development
OU=Development Domain
emailAddress=admin@nginx-proxy-app.com
CN = nginx-proxy-app.com

[ req_ext ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = myapp.nginx-proxy-app.com

openssl-san.conf

The Setup Task

We are going to create a VS Code Task to make running our script easier from within the IDE.

{
    "version": "2.0.0",
    "tasks": [{
        "label": "setup",
        "type": "shell",
        "osx": {
            "command": "bash ./scripts/project-tasks.sh setup"
        },
        "presentation": {
            "echo": true,
            "reveal": "always",
            "focus": true,
            "panel": "dedicated"
        },
        "problemMatcher": [],
        "windows": {
            "command": ".\\scripts\\project-tasks.ps1 -Setup"
        }
    }]
}
tasks.json


Conclusion

At this point, you should able to launch your docker-compose project and the Nginx service will automatically wire up your other services as hostnames with SSL.


Source Code

A fully-functional project is available at:

https://github.com/christophla/blog-docker-nginx-proxy