TLS management with Nginx, Certbot & Docker

Automating your TLS cert management is a standard deployment step. Most cloud providers offer some sort of automated cert management if you are deploying a managed app. However, if you're deploying a self-managed app, you'll have to maintain the TLS certs. This post will go through how to automate TLS certs in a self-managed dockerized application.

For this setup, we will be using nginx as the reverse proxy and certbot to automatically refresh certs from Let's Encrypt. We'll also have our nginx server handle the TLS termination. In docker-compose.yml add the following lines to configure Certbot:

    certbot:
     image: certbot/certbot:latest
     restart: "on-failure"
     volumes:
        - ./certbot/conf:/etc/letsencrypt
        - ./certbot/www:/var/www/certbot
     entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" 
     

The certs from Let's Encrypt are valid for 90 days. The key part is the entrypoint to the container, the shell command is an infinite while loop that checks for a cert renewal every 12hrs, as recommended by Let's Encrypt. Don't forget to mount the certbot volumes to our nginx container so it can access the certs created by Certbot. We'll also need to open port 443 on the container, and a command to reload nginx every 6hrs. Edit docker-compose.yml to add the following to nginx:

  nginx:
    build:
      dockerfile: (path to dockerfile)
    volumes:
      -  /static:/code/static
      -  /certbot/conf/:/etc/letsencrypt
      -  /certbot/www:/var/www/certbot
    restart: "on-failure"
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - web
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
        

Next we'll start configuring Nginx. Ensure port 443 is opened on the host machine, the nginx container will be listening on this port for incoming TLS connections. Edit the nginx conf file and add the following:

    server {
        server_name your_domain.com www.your_domain.com;
        listen 443 ssl;
        ssl_certificate /etc/letsencrypt/live/www.your_domain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/www.your_domain.com/privkey.pem;

        location / {
            proxy_pass `http://web`;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
                

If we try to run our containers right now, we'll run into a problem. That's because there is a circular dependency between certbot and nginx. Nginx needs the certs we specified in its conf to start which rely on certbot, but certbot requires nginx to be running to respond to the acme challenge for cert generation!

Monkey

We can get around this problem by creating a temporary nginx conf file:

http {
    server {
        listen 80;
        server_name your_domain.com www.your_domain.com;
       location /.well-known/acme-challenge/ {
           root /var/www/certbot;
       }
    }
}

Setup nginx to use this conf and start your containers. Now nginx will be able to respond to the acme challenge and certbot will create the certs required for TLS. Once we confirm the certs are created, we'll revert back to the original conf file, and this time when we start the containers we'll see both nginx and certbot running without errors.

Thumbs up