DEV Community

Agustin Navcevich
Agustin Navcevich

Posted on • Originally published at Medium on

NGINX server with SSL certificates with Let’s Encrypt in Docker

One of the problems I’ve been facing lately was to create a service that was served by SSL/TLS protocol. Most of the guides that can be found online show you some simple steps of installing a service without HTTPS listening in port 80 and go no further. For this reason,, is that I came up with this guide on how to serve a service through nginx that is served through HTTPS and that certificates are managed by Let’s Encrypt.

Let’s Encypt’s Certbot in a Docker Container

Before we can execute the Certbot command that installs a new certificate, we need to run a very basic instance of Nginx so that our domain is accessible over HTTP.

In order for Let’s Encrypt to issue you a certificate, an ACME Challenge Request is performed:

  1. You issue a command to the Certbot agent
  2. Certbot informs Let’s Encrypt that you want an SSL/TLS certificate
  3. Let’s Encrypt sends the Certbot agent a unique token
  4. The Certbot agent places the token at an endpoint on your domain that looks like: http://{domain}/.well-known/acme-challenge/{token}
  5. If the token at the endpoint matches the token that was sent to the Certbot agent from the Let’s Encrypt CA, the challenge request was successful and Let’s Encrypt knows that you are in control of the domain.

This basic instance of Nginx will only ever be run for the first time that you request a certificate from Let’s Encrypt. It’s a basic instance because it doesn’t even need to have a default page. It just needs to give write permissions to the Certbot agent so that it can place a token at an endpoint for the challenge request and that’s all.

We can’t configure a single instance of Nginx because the first instance of Nginx will only be configured for HTTP since we do not have an SSL/TLS certificate yet. Once we have the SSL/TLS certificate, we can configure SSL/TLS on the full production version of the site. If we then need to renew a certificate between 60 and 90 days after the first certificate was issued, the subsequent challenge requests will be performed on the production version of our site running on Nginx, and so we won’t ever have to run the basic instance of Nginx again.

Obtaining the Let’s Encrypt SSL/TLS Certificate

We need to create a docker compose that does the following:

  • Pulls the latest version of Nginx from the Docker registry
  • Exposes port 80 on the container to port 80 on the host, which means that requests to your domain on port 80 will be forwarded to nginx running in the Docker container
  • Maps the nginx configuration file that we will create in the next step to the configuration location in the Nginx container. When the container starts, it will load our custom configuration
  • Maps the Let’s Encrypt location to the default location of Nginx in the container.
  • Creates a default Docker network
# docker-compose.yml

services:

letsencrypt-nginx-container:
    container\_name: 'letsencrypt-nginx-container'
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    networks:
      - docker-network

networks:
  docker-network:
    driver: bridge

Then, create the configuration file for nginx

# nginx.conf
server {
    listen 80;
    listen [::]:80;
    server\_name {domains};
    location ~ /.well-known/acme-challenge {
        allow all;
        root /usr/share/nginx/html;
    }
    root /usr/share/nginx/html;
    index index.html;
}

The nginx configuration file does the following:

  • Listens for requests on port 80 for URLs in the domain
  • Gives the Certbot agent access to ./well-known/acme-challenge
  • Sets the default root and file

Before running the Certbot command, spin up a Nginx container in Docker to ensure the temporary Nginx site is up and running

sudo docker-compose up -d

Then, open up a browser and visit the domain to ensure that the Docker container is up and running and accessible.

We’re almost ready to execute the Certbot command. But before we do, you need to be aware that Let’s Encrypt has rate limits. Most notably, there’s a limit of 20 issued certificates per 7 days. So if you exceeded 20 requests and are having a problem with generating your certificate for whatever reason, you could run into trouble. Therefore, it’s always wise to run your commands with a — staging parameter which will allow you to test if your commands will execute properly before running the actual commands.

Run the staging command for issuing a new certificate:

sudo docker run -it --rm \
-v /docker-volumes/etc/letsencrypt:/etc/letsencrypt \
-v /docker-volumes/var/lib/letsencrypt:/var/lib/letsencrypt \
-v /docker/letsencrypt-docker-nginx/src/letsencrypt/letsencrypt-site:/data/letsencrypt \
-v "/docker-volumes/var/log/letsencrypt:/var/log/letsencrypt" \
certbot/certbot \
certonly --webroot \
--register-unsafely-without-email --agree-tos \
--webroot-path=/data/letsencrypt \
--staging \
-d {domain}

Issue a new Let’s Encrypt Certificate with Certbot and Docker in Staging Mode

The command does the following:

  • Run docker in interactive mode so that the output is visible in terminal
  • If the process is finished close, stop and remove the container
  • Map 4 volumes from the server to the Certbot Docker Container:
  • The Let’s Encrypt Folder where the certificates will be saved
  • Lib folder
  • Map our html and other pages in our site folder to the data folder that let’s encrypt will use for challenges.
  • Map a logging path for possible troubleshooting if needed
  • For staging, we’re not specifying an email address
  • We agree to terms of service
  • Specify the webroot path
  • Run as staging
  • Issue the certificate to be valid for the A record and the CNAME record

You can also get some additional information about certificates for your domain by running the Certbot certificates command:

sudo docker run --rm -it --name certbot \
-v /docker-volumes/etc/letsencrypt:/etc/letsencrypt \
-v /docker-volumes/var/lib/letsencrypt:/var/lib/letsencrypt \
-v /docker/letsencrypt-docker-nginx/src/letsencrypt/letsencrypt-site:/data/letsencrypt \
certbot/certbot \
--staging \
certificates

Get Additional Information with the Certbot Certificates Command

If the staging command executed successfully, execute the command to return a live certificate

First, clean up staging artifacts:

sudo rm -rf /docker-volumes/

And then request a production certificate: (note that it’s a good idea to supply your email address so that Let’s Encrypt can send expiry notifications)

sudo docker run -it --rm \
-v /docker-volumes/etc/letsencrypt:/etc/letsencrypt \
-v /docker-volumes/var/lib/letsencrypt:/var/lib/letsencrypt \
-v /docker/letsencrypt-docker-nginx/src/letsencrypt/letsencrypt-site:/data/letsencrypt \
-v "/docker-volumes/var/log/letsencrypt:/var/log/letsencrypt" \
certbot/certbot \
certonly --webroot \
--email youremail@domain.com --agree-tos --no-eff-email \
--webroot-path=/data/letsencrypt \
-d {domain}

If everything ran successfully, run a docker-compose down command to stop the temporary Nginx site

cd /docker/letsencrypt-docker-nginx/src/letsencrypt

sudo docker-compose down

Set up Your Production Site to Run in a Nginx Docker Container

Let’s start with the docker-compose.yml file

# docker-compose.yml

version: '3.1'

services:

  production-nginx-container:
    container\_name: 'production-nginx-container'
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./production.conf:/etc/nginx/conf.d/default.conf
      - ./production-site:/usr/share/nginx/html
      - ./dh-param/dhparam-2048.pem:/etc/ssl/certs/dhparam-2048.pem
      - /docker-volumes/etc/letsencrypt/live/{domain}/fullchain.pem:/etc/letsencrypt/live/{domain}/fullchain.pem
      - /docker-volumes/etc/letsencrypt/live/{domain}/privkey.pem:/etc/letsencrypt/live/{domain}/privkey.pem
    networks:
      - docker-network

networks:
  docker-network:
    driver: bridge

The docker-compose does the following:

  • Allows ports 80 and 443
  • Maps the production Nginx configuration file into the container
  • Maps the production site content into the container
  • Maps a 2048 bit Diffie–Hellman key exchange file into the container
  • Maps the public and private keys into the container
  • Sets up a docker network

Next, create the Nginx configuration file for the production site

production.conf

# production.conf

server {
    listen 80;
    listen [::]:80;
    server\_name {domain};

    location / {
        rewrite ^ https://$host$request\_uri? permanent;
    }

    #for certbot challenges (renewal process)
    location ~ /.well-known/acme-challenge {
        allow all;
        root /data/letsencrypt;
    }
}

#https://ohhaithere.com
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server\_name {domain};

    server\_tokens off;

    ssl\_certificate /etc/letsencrypt/live/{domain}/fullchain.pem;
    ssl\_certificate\_key /etc/letsencrypt/live/{domain}/privkey.pem;

    ssl\_buffer\_size 8k;

    ssl\_dhparam /etc/ssl/certs/dhparam-2048.pem;

    ssl\_protocols TLSv1.2 TLSv1.1 TLSv1;
    ssl\_prefer\_server\_ciphers on;

    ssl\_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

    ssl\_ecdh\_curve secp384r1;
    ssl\_session\_tickets off;

    # OCSP stapling
    ssl\_stapling on;
    ssl\_stapling\_verify on;
    resolver 8.8.8.8;

    return 301 https://{domain}$request\_uri;
}

#https://{domain}
server {
    server\_name {domain};
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server\_tokens off;

    ssl on;

    ssl\_buffer\_size 8k;
    ssl\_dhparam /etc/ssl/certs/dhparam-2048.pem;

    ssl\_protocols TLSv1.2 TLSv1.1 TLSv1;
    ssl\_prefer\_server\_ciphers on;
    ssl\_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

    ssl\_ecdh\_curve secp384r1;
    ssl\_session\_tickets off;

    # OCSP stapling
    ssl\_stapling on;
    ssl\_stapling\_verify on;
    resolver 8.8.8.8 8.8.4.4;

    ssl\_certificate /etc/letsencrypt/live/{domain}/fullchain.pem;
    ssl\_certificate\_key /etc/letsencrypt/live/{domain}/privkey.pem;

    root /usr/share/nginx/html;
    index index.html;
}

Generate a 2048 bit DH Param file

sudo openssl dhparam -out /docker/letsencrypt-docker-nginx/src/production/dh-param/dhparam-2048.pem 2048

Copy your site content into the mapped directory:

/docker/letsencrypt-docker-nginx/src/production/production-site/

Spin up the production site in a Docker container:

sudo docker-compose up -d

If you open up a browser and point to HTTP, you should see that the site loads correctly and will automatically redirect to HTTPS

How to Renew Let’s Encrypt SSL Certificates with Certbot and Docker

Earlier, we placed the following section in the production Nginx configuration file:

location ~ /.well-known/acme-challenge {
    allow all;
    root /usr/share/nginx/html;
}

The production site’s docker-compose file then maps a volume into the Nginx container that can be used for challenge requests:

production-nginx-container:
    container\_name: 'production-nginx-container'
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      #other mapped volumes...
      #for certbot challenges
      - /docker-volumes/data/letsencrypt:/data/letsencrypt
    networks:
      - docker-network

This effectively allows Certbot to perform a challenge request. It’s important to note that certbot challenge requests will be performed using port 80 over HTTP, so ensure that you enable port 80 for your production site.

All that’s left to do is to set up a cron job that will execute a certbot command to renew Let’s Encrypt SSL certificates.

Finally — Set Up a Cron Job to Automatically Renew Let’s Encrypt SSL/TLS Certificates

It’s a good idea to run a daily cron job that attempts to renew Let’s Encrypt SSL certificates. It doesn’t matter how many times this command is executed as nothing will happen unless your certificate is due for renewal.

To add a crontab, run the following commands:

sudo crontab -e

Place the following at the end of the file, then close and save it.

0 0 \* \* \* docker run --rm -it --name certbot -v "/docker-volumes/etc/letsencrypt:/etc/letsencrypt" -v "/docker-volumes/var/lib/letsencrypt:/var/lib/letsencrypt" -v "/docker-volumes/data/letsencrypt:/data/letsencrypt" -v "/docker-volumes/var/log/letsencrypt:/var/log/letsencrypt" certbot/certbot renew --webroot -w /data/letsencrypt --quiet && docker kill --signal=HUP production-nginx-container

The above command will run every night at 00:00. If the certificates are due for renewal, the certificates will renew. Additionally, the Nginx configuration and renewed certificates will reload by executing the signal command at the end of the cron command.

Please share any thoughts or comments you have. Feel free to ask and correct me if I’ve made some mistakes.

Latest comments (0)