docker-compose-certbot

A Docker-first solution to running NGINX reverse proxy with automatically updated Let’s Encrypt SSL certificates using nginx-proxy and acme-companion

Introduction

In a previous blog post, I presented a solution to use docker-compose to obtain and renew a Let’s Encrypt SSL certificate and configure NGINX to use it. The solution depended on using two docker-compose files, one for the initialisation and the second for operation, as well as a cron job, and a couple of very simple shell scripts.

In this blog post, I will present a solution that was developed by Jason Wilder that is entirely docker-based and that eliminates the need for the shell scripts and the cron job. The docker images nginx-proxy and acme-companion are both open-source projects initiated and maintained by Jason Wilder and are hosted on GitHub under https://github.com/nginx-proxy/nginx-proxy and https://github.com/nginx-proxy/acme-companion respectively. It is worth it to mention that at the time of writing this blog post, Nicolas Duchon is the maintainer of the nginx-proxy repository of Github.

Before we start, I have to mentioned that this solution requires sharing the Docker socket file of the host machine with one of the docker containers which goes against the recommendations of OWASP regarding Docker security. I wrote a blog post related to this issue, that also presents a slightly more secure solution.

The Solution

For the sake of simplicity, and to make comparison easier, I will use the same example I used for the in the previous related blog post, which involved deploying an instance of Matomo. . Let’s start with the docker-compose.yaml we developed in that post, and introduce nginx-proxy and acme-companions.

version: "3"

services:
  matomo:
    image: matomo
    container_name: matomo
    restart: always
    depends_on:
      - db
    volumes:
      - ./matomo:/var/www/html
  db:
    image: mariadb 
    container_name: db 
    command: --max-allowed-packet=64MB
    restart: always
    environment:
      - MARIADB_DATABASE=matomo
      - MARIADB_USER
      - MARIADB_PASSWORD
      - MARIADB_ROOT_PASSWORD
    volumes:
      - ./db:/var/lib/mysql
  nginx:
    container_name: nginx
    image: nginx:latest
    restart: unless-stopped
    environment:
      - DOMAIN
    depends_on:
      - matomo
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./etc/nginx/templates:/etc/nginx/templates:ro
      - ./etc/letsencrypt:/etc/letsencrypt:ro
      - ./certbot/data:/var/www/certbot
  certbot:
    container_name: certbot
    image: certbot/certbot:latest
    depends_on:
      - nginx
    command: >-
             certonly --reinstall --webroot --webroot-path=/var/www/certbot
             --email ${EMAIL} --agree-tos --no-eff-email
             -d ${DOMAIN}
    volumes:
      - ./etc/letsencrypt:/etc/letsencrypt
      - ./certbot/data:/var/www/certbot

Now we need to replace Nginx, and Certbot with nginx-proxy and acme-companion. Changes must be made also to the Matomo service where some necessary environment variables must be added. Changes are highlighted in the following code block.

version: "3"
services:
  matomo:
    image: matomo
    container_name: matomo
    restart: always
    environment:
      VIRTUAL_HOST: ${THE_DOMAIN_NAME}
      VIRTUAL_PORT: ${THE_PORT_MATOMO_IS_LISTENING_FOR}
      LETSENCRYPT_HOST: ${THE_DOMAIN_NAME}
      LETSENCRYPT_EMAIL: ${THE_EMAIL_USED_FOR_LETS_ENCRYPT}
    depends_on:
      - db
    volumes:
      - matomo:/var/www/html
  db:
    image: mariadb 
    container_name: db 
    command: --max-allowed-packet=64MB
    restart: always
    environment:
      - MARIADB_DATABASE=matomo
      - MARIADB_USER
      - MARIADB_PASSWORD
      - MARIADB_ROOT_PASSWORD
    volumes:
      - db:/var/lib/mysql

  proxy:
    image: nginxproxy/nginx-proxy:alpine
    restart: always
    ports:
      - 80:80
      - 443:443
    labels:
      com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
    volumes:
      - certs:/etc/nginx/certs:ro
      - vhostd:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
     # WARNING: OWASP discorages sharing /var/run/docker.sock even in read-only mode
     # Read this: https://blog.jarrousse.org/2024/09/01/a-slightly-more-secure-docker-first-solution/
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      - proxy-tier
      - default

  letsencrypt-companion:
    image: nginxproxy/acme-companion
    restart: always
    volumes:
      - certs:/etc/nginx/certs
      - acme:/etc/acme.sh
      - vhostd:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy-tier
    depends_on:
      - proxy

networks:
  proxy-tier

volumes:
  matomo
  db:
  certs:
  acme:
  vhostd:
  html:

The environment variables in the WebApp backend, in this case matomo, are necessary for nginx-proxy, and acme-companion to obtain the ssl certificate from Let’s Encrypt and to setup the reverse proxy the the WebApp:

environment:
  VIRTUAL_HOST: your_matomo_server.com
  VIRTUAL_PORT: 80
  LETSENCRYPT_HOST: your_matomo_server.com
  LETSENCRYPT_EMAIL: email@your_matomo_server.com

That’s it. Now it is only the matter of running docker compose.

$ docker-compose up -d

Conclusion

This solution is much simpler than the one suggested in my previous post, but comes at the expense of clarity and control… and the and the additional risk of going against an OWASP recommendation, as well as running code maintained by a person rather than an organisation.