Nginx and LetsEncrypt via webroot

First of all, I will say that there is an Nginx plugin for the LetsEncrypt system which will configure your domains and everything, so you should look into that if you want something fully automated.  In this quick intro guide I am looking at the webroot method, as that reflects my Docker based setup.

First of all, I create a Docker container from the standard Nginx image:

docker run \
    -d \
    --restart=always \
    --name ingress-nginx \
    -p 80:80 \
    -p 443:443 \
    -v /storage/nginx/config/:/etc/nginx \
    -v /etc/letsencrypt:/etc/letsencrypt:ro \

Lets break that down a little:

  • -d - detach this container, don't run it interactively
  • --restart=always - always attempt a restart of this container
  • --name ingress-nginx - give it a useful name
  • -v /storage/nginx/config/:/etc/nginx - give it a volume for persistent storage of configuration data, I'm using a local directory
  • -v /etc/letsencrypt:/etc/letsencrypt:ro - give it access to the hosts LetsEncrypt configuration folder
  • nginx:1.13 - use the official Nginx image, version 1.13.x

If Nginx doesn't populate the configuration volume with a default set, then we need to go and grab some from a temporary Nginx container:

$ docker run -i -t --name temp-nginx nginx:1.13 
$ docker exec -i -t temp-nginx /bin/bash
root@99d7552dabc1:/# tar zcvf nginx.tgz /etc/nginx/*
root@99d7552dabc1:/# exit
$ cd /storage/nginx/config
$ docker cp temp-nginx:/nginx.tgz .
$ tar zxvf nginx.tgz
$ docker rm -f temp-nginx

My nginx.conf file is very simple:

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/;

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  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/sites/*.conf;

In /etc/nginx/sites/ (which is /storage/nginx/config/sites on the Docker host) I have several configuration files which look like this:

server {
        listen 80;
        listen 443 ssl;
        server_name     my_domain www.my_domain;

        ssl_certificate         /etc/letsencrypt/live/my_domain/fullchain.pem;
        ssl_certificate_key     /etc/letsencrypt/live/my_domain/privkey.pem;

        location ^~ /.well-known/acme-challenge/ {
                alias /etc/nginx/acme-challenge/.well-known/acme-challenge/;

        location / {
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_redirect off;
                proxy_http_version 1.1;
                proxy_pass http://internalserver:8080;

If Nginx fails to start with this configuration, its probably because we don't have a certificate file yet - just comment out the relevant SSL portions of the site config and try to start Nginx again.

To create a LetsEncrypt certificate for this domain is simple, first you need to install certbot on the host (I am using Ubuntu 16.04 as the host):

$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install certbot

Once certbot is installed, the following command will generate a LetsEncrypt certificate for a domain:

$ sudo certbot \
    certonly \
    --webroot \
    --agree-tos \
    --no-eff-email \
    --email \
    -w /storage/nginx/config/acme-challenge/ \
    -d \

Lets break that down a little:

  • certonly - generate the certificate but not install it anywhere - the generated certificate (and its relevant supporting files) gets saved in /etc/letsencrypt/... as we shall see in a moment
  • --webroot - use the webroot method of verification for this request
  • --agree-tos - agree to the ACME terms of service
  • --no-eff-email - don't share your email address with the EFF, this is entirely up to you
  • --email - the email address to use to generate the certificate and to use for correspondence about the certificate
  • -w /storage/nginx/config/acme-challenge/ - the root folder in which the challenge folder structure can be stored.  The challenge will not be directly saved in here, instead it will be saved under .well-known/acme-challenge/ within this folder.
  • -d - the domain to generate the cert for.  You can have as many of these as you wish, so long as the challenge can be correctly read by the ACME servers.  In this example, I include the www subdomain as well.

Running the above command will generate a certificate for me to use in Nginx - the certificates, including private key and chains, are stored in /etc/letsencrypt/live/ where my Dockerised Nginx container can access them via the second volume.

A quick restart of the Nginx container (uncommenting the SSL portions of the site configuration file if you previous commented them out) will pick up the new certificates!

Because certbot stores the relevant configuration options you used to create the original certificate request (its in /etc/letsencrypt/renewal if you want a peek) then renewing your certificates is really really simple:

$ certbot renew

certbot will go off and renew any domains that are in the renewal window - and ignore the ones which are not.  You can automate this to run daily via crontab:

$ sudo crontab -e

30 5 * * * /usr/bin/certbot renew --quiet

This crontab configuration will run certbot daily at 5.30am, in quite mode so you don't get any non-error output sent to you.