When using Let's Encrypt certbot, how do I restart/reload a network service only once and only if the cerificate was actually renewed?

The certbot command provides two hooks that run after automated renewals, from the docs:

 --post-hook POST_HOOK
                       Command to be run in a shell after attempting to
                       obtain/renew certificates. Can be used to deploy
                       renewed certificates, or to restart any servers that
                       were stopped by --pre-hook. This is only run if an
                       attempt was made to obtain/renew a certificate. If
                       multiple renewed certificates have identical post-
                       hooks, only one will be run. (default: None)
 --deploy-hook DEPLOY_HOOK
                       Command to be run in a shell once for each
                       successfully issued certificate. For this command, the
                       shell variable $RENEWED_LINEAGE will point to the
                       config live subdirectory (for example,
                       "/etc/letsencrypt/live/example.com") containing the
                       new certificates and keys; the shell variable
                       $RENEWED_DOMAINS will contain a space-delimited list
                       of renewed certificate domains (for example,
                       "example.com www.example.com" (default: None)

This issue is outlined in this (now closed) LE thread and is basically about minimising interruption to services. POST_HOOK executes every time an attempt to renew is made even if no certificates were issued, though only once. This makes it possible to unnecessarily restart services. DEPLOY_HOOK runs for each and every successful certificate renewal. If one uses DEPLOY_HOOK, and has multiple certificates, each service may restart multiple times when once is enough. More info on renewal hooks here.

I use an issuance method that does not interrupt my services at all, e.g.:

certbot certonly --webroot ...

or

certbot certonly --dns-PROVIDER ...

I want to restart/reload each dependent service only once, and only if its certificate actually changed.


I was able to overcome this limitation by using both hooks with a simple script to save state. For example, to reload nginx, and restart vsftpd when a certificate they both use is renewed:

certbot certonly ... \
    --deploy-hook '/usr/local/sbin/read-new-certs-services nginx --restart vsftpd' \
    --post-hook /usr/local/sbin/read-new-certs-services

The script /usr/local/sbin/read-new-certs-services is this:

#!/bin/sh

run_base=/run
dir_reloads="$run_base/new-cert-reloads"
dir_restarts="$run_base/new-cert-restarts"

if [ $# -gt 0 ]; then
    some=0
    dir="$dir_reloads"
    while [ $# -gt 0 ]; do
        case $1 in
            -h|--help)
                >&2 cat <<-'EOHELP'
                    Usage:

                      read-new-certs-services [-l|-s] service1 [[-l|-s] service2]
                      or
                      read-new-certs-services

                      When called without arguments, marked services will be reloaded or restarted.

                    Arguments:

                      -h, --help
                        This help.

                      -l, --reload
                        Mark all subsequently listed services for reloading.

                      -s, --restart
                        Mark all subsequently listed services for restarting.
                    EOHELP
                exit
                ;;
            -l|--reload)
                dir="$dir_reloads"
                ;;
            -s|--restart)
                dir="$dir_restarts"
                ;;
            *)
                if [ -n "$1" ]; then
                    some=1
                    run_file="$dir/$1"
                    if [ ! -f "$run_file" ] && ! install -D /dev/null "$run_file"; then
                        >&2 echo "Service could not be marked: $run_file"
                        exit 1
                    fi
                fi
                ;;
        esac
        shift
    done
    if [ $some -eq 0 ]; then
        >&2 echo 'No service(s) specified.'
        exit 1
    fi
    exit
fi

if [ -d "$dir_restarts" ]; then
    find "$dir_restarts" -mindepth 1 -printf '%P\t%p\n' | while IFS="$(printf '\t')" read -r svc run_file; do
        systemctl restart "$svc" && rm "$run_file"
        # no need to reload if restarting
        run_file="$dir_reloads/$svc"
        if [ -f "$run_file" ]; then
            rm "$run_file"
        fi
    done
fi

if [ -d "$dir_reloads" ]; then
    find "$dir_reloads" -mindepth 1 -printf '%P\t%p\n' | while IFS="$(printf '\t')" read -r svc run_file; do
        systemctl reload "$svc" && rm "$run_file"
    done
fi

This must be used for every certificate issue so that they don't use conflicting methods of restarting services.

You can change existing certificate renewals to use this method by editing their /etc/letsencrypt/renewal/*.conf files to contain hooks like this in the [renewalparams] section:

renew_hook = /usr/local/sbin/read-new-certs-services nginx -s vsftpd
post_hook = /usr/local/sbin/read-new-certs-services