Nginx docker as reverse proxy and letsencrypt

Intro

Last week I bought the cheapest OVH VPS to test out self hosted services (owncloud, kanboard…). Most services provide docker containers which make it painless to build and run them from a new server. All I did was install docker, download the container images and run them.

It sounds simple and it is until it isn’t, which of course was due to my inexperience. This post is intended to record my progress.

Stack

My current stack contains a nginx docker acting as a reverse proxy to multiple subdomains; each service are run inside a separate docker. For example, work.lttviet.com is just a Kanboard docker. The nginx also forwards all http request to https.

DNS

I had my domain A record point to my server ip. I ran into some problems when using Cloudflare’s HTTP proxy with letsencrypt, so I had to disable the former.

letsencrypt

letsencrypt has an official docker container but it isn’t advisable to use according to the document1, so I installed letsencrypt package from official ubuntu repo.

sudo apt-get install letsencrypt

Create strong 2048-bit or higher dhparams2

sudo openssl dhparam -out dhparam.pem 2048

nginx

Add a volume /etc/letsencrypt to nginx so nginx can serve challenges created by letsencrypt. My docker-compose.yml config is as following:

version: '2'
services:
  nginx:
    build:
      context: ./nginx
    container_name: nginx
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
    ports:
      - "80:80"
      - "443:443"

My Dockerfile for nginx is

FROM nginx:alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY dhparam.pem /etc/nginx/ssl/dhparam.pem

In my nginx.conf, I add a server block

server {
    location ~ /.well-known {
        root /etc/letsencrypt/webrootauth;
        default_type "text/plain";
    }
}

Now /etc/letsencrypt/ can be read and modified by both nginx docker and letsencrypt.

Running letsencrypt

Create dir /etc/letsencrypt/webrootauth

sudo mkdir -p /etc/letsencrypt/webrootauth

Run letsencrypt

sudo letsencrypt certonly -c letsencrypt.ini

My letsencrypt.ini file

rsa-key-size = 2048
email = hello@world.com
domains = hello.com
text = True
agree-tos = True
authenticator = webroot
webroot-path = /etc/letsencrypt/webrootauth
keep-until-expiring = True

nginx.conf again

If everything go well, you will have your certificates at /etc/letsencrypt/live/yourdomain.com/

Modify nginx.conf, here I redirect http to https and forward traffic to kanboard docker

#https://blog.danivovich.com/2016/01/28/lets-encrypt-your-docker/
server {
    listen 80;
    listen [::]:80;
    server_name hello.com

    location ~ /.well-known {
        root /etc/letsencrypt/webrootauth;
        default_type "text/plain";
    }

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

server {
    listen 443 ssl default deferred;
    server_name hello.com

    ssl_certificate /etc/letsencrypt/live/hello.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/hello.com/privkey.pem;

    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 180m;
    ssl_session_tickets off;

    ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    ssl_prefer_server_ciphers on;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";

    ssl_stapling on;
    ssl_stapling_verify on;

    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";

    location ~ /.well-known {
        root /etc/letsencrypt/webrootauth;
        default_type "text/plain";
    }

    location / {
        proxy_pass http://kanboard:80;
        proxy_redirect     off;
        proxy_set_header   Host $host;
        proxy_set_header   X-NginX-Proxy true;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Host $server_name;
        proxy_read_timeout 900;
    }

systemd service and timer

Create a job to run letsencrypt for renewing the ssl certificate on the first day of every month.

#/etc/systemd/system/letsencrypt.service
[Unit]
Description=letsencrypt renew

[Service]
Type=oneshot
WorkingDirectory=/home/me
ExecStart=/usr/bin/letsencrypt certonly -c letsencrypt.ini
ExecStartPost=/usr/local/bin/docker-compose restart

[Install]
WantedBy=multi-user.target
#/etc/systemd/system/letsencrypt.timer
[Unit]
Description=letsencrypt renew timer

[Service]
OnCalendar=*-*-01 01:00:00
Unit=letsencrypt.service

[Install]
WantedBy=multi-user.target

References: