Combine Certbot and Nginx Into One Container

21 Jan 2025

I have had some issues with my previous Nginx and certbot install. Nginx will not use the new updated certificates from certbot unless I stop and start the docker container again. So now I’m going to combine the Nginx and certbot into one docker container and see if that will fix my issues.

Custom docker image

Let’s start with the custom docker image. Considering my research on the topic I have decided to start from the certbot image and add Nginx to it. Because it looks like it’s an easier install to use the certbot image.

nginx-img/Dockerfile

FROM certbot/certbot:v3.1.0

VOLUME /etc/letsencrypt

EXPOSE 443
EXPOSE 80

# Install some extra packages
RUN apk update && apk add openssl curl tzdata rsyslog nginx

# Copy nginx-certbot.conf for ACME challenge folder, will be used in server {} config
COPY nginx-certbot.conf /etc/nginx/

# Disable imklog in rsyslog, due to no access to /proc/kmsg in container
RUN sed -i '/imklog/s/^/#/' /etc/rsyslog.conf

# Make sure certbot folder exists
RUN mkdir -p /var/www/certbot

# Add crontab for running certbot
ADD crontab /etc/crontabs

# Copy scripts
COPY *.sh /opt/
RUN chmod +x /opt/entrypoint.sh
RUN chmod +x /opt/certbot_renew.sh

ENTRYPOINT [ "/opt/entrypoint.sh" ]

So now we have the Dockerfile for our custom nginx docker image finished. But we need to create few scripts as well.

We begin with the shortest one, crontab

nginx-img/crontab

# Run at 03:00 every day
0 3 * * * /opt/certbot_renew.sh

for this cron job to work we need the certbot_rewnew.sh script

nginx-img/certbot_renew.sh

#!/bin/sh

echo "----------------------------------------"
echo "Running certbot_renew"
echo $(date)
echo
if [ "$1" == "--dry-run" ]; then
  echo "Dry run"
  certbot renew --dry-run --webroot -w /var/www/certbot
else
  certbot renew --webroot -w /var/www/certbot 2&>1 > /var/log/certbot.log
fi

This script is just a simple certbot renew line, I like to do it like this so the crontab is easier to read. This way I could add the --dry-run option as well, very useful when testing the container and certbot.

To make the container run, we need to create the entrypoint.sh script

nginx-img/entrypoint.sh

#!/bin/sh

echo $(date)
echo "nginx and certbot container started"
echo "----------------------------------------"

# Make sure the cron.log exists
touch /var/log/cron.log

# Lets start crond for perodic updates
exec rsyslogd &
exec crond &

# Now start nginx
nginx -g 'daemon off;'

Just a simple script to make sure the /var/log/cron.log exists before we start syslog and cron, and finally starts nginx.

Finally I have added a nginx-certbot.conf that can be included in my nginx server {} blocks for the ACME challenge.

nginx-img/nginx-certbot.conf

location ^~ /.well-known/acme-challenge/ {
  allow all;

  default_type "text/plain";

  root /var/www/certbot;
}

# Hide /acme-challenge sub directory
location = /.well-known/acme-challenge/ {
  return 404;
}

Now the folder structure would have 1 sub folder and 5 files

nginx/
├─ nginx-img/
│  ├─ Dockerfile
│  ├─ crontab
│  ├─ certbot_renew.sh
│  ├─ entrypoint.sh
│  ├─ nginx-certbot.conf

Docker compose

Now we can finally create the docker-compose file.

This is a pretty simple docker compose file needed to build the container

docker-compose.yml

services:
  nginx:
    build: nginx-img/.  # Build custom certbot with nginx image
    container_name: nginx
    restart: always
    environment:
      - TZ=Europe/Stockholm
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./user-data/letsencrypt:/etc/letsencrypt  # Certificates
      # nginx config
      - ./user-data/conf.d:/etc/nginx/conf.d      # nginx conf.d
      - ./user-data/html:/var/www/html            # html root

I have some defined volumes

  • ./user-data/letsencrypt:/etc/letsencrypt - Folder to save the certificates generated by certbot
  • ./user-data/conf.d:/etc/nginx/conf.d - nginx server config files
  • ./user-data/html:/var/www/html - html root

You can add an extra line if you need a custom nginx.conf

  • ./user-data/nginx.conf:/etc/nginx/nginx.conf

Now you can build the image by running

docker compose build

Folder structure with the new files and folders

nginx/
├─ nginx-img/
│  ├─ Dockerfile
│  ├─ crontab
│  ├─ certbot_renew.sh
│  ├─ entrypoint.sh
│  ├─ nginx-certbot.conf
├─ user-data/
│  ├─ letsencrypt/
│  ├─ conf.d/
│  ├─ html/
├─ docker-compose.yml

Setup the site

To create new certificates with certbot for your site you can run this command

docker compose run --rm -p 80:80 -p 443:443 nginx certbot certonly --standalone -d www.example.com -d example.com --non-interactive --agree-tos --email your-email@example.com --expand

A basic server config for nginx and the host example.com

user-data/conf.d/default.conf

server {
  listen 443 ssl;
  http2 on;
  server_name www.example.com example.com;

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

  # Include for certbot ACME challenge
  include /etc/conf.d/nginx-certbot.conf;

  location / {
    root /var/www/html;
    index index.html;
  }
}

server {
  listen 80;
  server_name www.example.com example.com;

  # Just redirect to https
  location / {
    return 301 https://$host$request_uri;
  }
}

A simple page to serve for testing

user-data/html/index.html

<!DOCTYPE html>
<html>
<head>
</head>

<body>
Hello world!
Using https and certificates with certbot
</body>
</html>

Now the folder structure will look like this, with a bunch of certificate files under user-data/letsencrypt

nginx/
├─ nginx-img/
│  ├─ Dockerfile
│  ├─ crontab
│  ├─ certbot_renew.sh
│  ├─ entrypoint.sh
│  ├─ nginx-certbot.conf
├─ user-data/
│  ├─ letsencrypt/
│  ├─ conf.d/
│  │  ├─ default.conf
│  ├─ html/
│  │  ├─ index.html
├─ docker-compose.yml

Finally we can run the new combine nginx and certbot container

docker compose up -d