Shore Up
A timer on a wall automatically swapping an old padlock on a gate for a fresh one, while a doorman flips a switch to briefly reopen and reclose the gate.
LinuxSecurity

Automate Let's Encrypt Renewal with a Deploy Hook

Ketan Aagja7 min read
No ratings yet

Certbot renews your certificates on a timer, but a renewed certificate sitting on disk does nothing until the service using it reloads and reads the new file. This guide sets up a deploy hook — a small script Certbot runs only when a certificate actually changes — so the reload happens automatically and only when it needs to.

Before you run this

What this does and why: you place one short shell script in Certbot's deploy-hook directory. After Certbot renews any certificate, it runs your script, which validates the web server config and reloads the service so it serves the new certificate. The goal is to stop the classic outage where a cert renewed fine but the running service kept the old, now-expired one in memory.

Privileges: everything here runs as root. Certbot's renewal timer already runs as root, and reloading a system service requires it. Run the setup commands with sudo or as root.

Test first: read the script before you install it. A systemctl reload is a graceful operation — an nginx reload does not drop existing connections — but if your service config is broken, a reload can fail and, depending on the service, leave it in a degraded state. So do this on a test VM or a non-production host first. The script below runs nginx -t and only reloads if the config is valid, precisely so a bad config doesn't take the site down.

What it changes, and reversibility: the only thing you add to the system is one executable file in a directory Certbot already reads. It does not modify your per-certificate renewal config, and it does not touch the certificates themselves. To undo it, delete the file. That's it. The reload it triggers is not destructive to data.

Assumptions

I'm assuming:

  • Debian 12 or Ubuntu 22.04, with certbot installed from the distribution's apt package.
  • systemd, with Certbot's renewal driven by the certbot.timer unit that the package ships.
  • nginx as the service consuming the certificate.

If you're on RHEL/Alma, the package and service names are the same (certbot, nginx) but Certbot may come from EPEL; the hook directory path is identical. If you terminate TLS in Postfix, Dovecot, or Apache instead, only the test-and-reload commands at the bottom of the script change — I note those below.

Where the hook goes

When Certbot is installed, it reads any executable script in these directories on every renewal run:

/etc/letsencrypt/renewal-hooks/pre/
/etc/letsencrypt/renewal-hooks/deploy/
/etc/letsencrypt/renewal-hooks/post/
  • pre runs before renewal is attempted.
  • deploy runs once for each certificate that was actually renewed.
  • post runs after all renewals, whether or not anything changed.

We want deploy, because it only fires when a certificate genuinely changed — no pointless reloads on the many days Certbot checks and finds nothing to do.

For each certificate a deploy hook processes, Certbot sets two environment variables:

  • RENEWED_LINEAGE — the full path to that certificate's live directory, e.g. /etc/letsencrypt/live/example.com.
  • RENEWED_DOMAINS — the space-separated list of domains on that certificate.

These are the standard, documented variables. Our nginx script doesn't strictly need them because nginx points at the live path itself, but I'll log them so you can see in the journal what triggered the reload.

The hook script

Create the file:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Contents:

#!/bin/bash
# Deploy hook: reload nginx after a Let's Encrypt certificate is renewed.
# Certbot runs this once per renewed certificate and sets
# RENEWED_LINEAGE and RENEWED_DOMAINS in the environment.

set -euo pipefail

# Log what triggered this, so it shows up in the systemd journal / syslog.
logger -t certbot-deploy "Cert renewed for: ${RENEWED_DOMAINS:-unknown} (${RENEWED_LINEAGE:-unknown})"

# Only reload if the running config is valid. If nginx -t fails,
# we exit non-zero WITHOUT reloading, leaving the working config in place.
if ! nginx -t 2>/dev/null; then
    logger -t certbot-deploy "nginx -t failed; NOT reloading"
    exit 1
fi

systemctl reload nginx
logger -t certbot-deploy "nginx reloaded successfully"

Make it executable — Certbot silently ignores files that aren't:

sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

A few notes on the choices here. set -euo pipefail makes the script stop on the first error rather than blundering on. The nginx -t gate is the important safety feature: a renewal should never be the thing that reloads a broken config into production. And logger writes to syslog so you get a breadcrumb without inventing a custom log file.

If your service isn't nginx

Swap the last two lines. The pattern is always test config, then reload:

  • Apache (Debian/Ubuntu): test with apache2ctl configtest, reload with systemctl reload apache2.
  • Postfix: reload with systemctl reload postfix (Postfix rereads its cert files on reload).
  • Dovecot: reload with systemctl reload dovecot.

Use the real service name for your distribution. If you're unsure whether a given daemon rereads its certificate on reload versus needing a full restart, check that project's own documentation before assuming — some daemons need a restart to pick up a new key.

Testing it without waiting for a real renewal

The surest test is to run the hook script by hand, exactly as Certbot would:

sudo RENEWED_DOMAINS="example.com" \
     RENEWED_LINEAGE="/etc/letsencrypt/live/example.com" \
     /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Replace example.com with a domain you actually have a certificate for. This proves the script's logic — config test, reload, logging — works, and it's safe because a reload is graceful.

To test the renewal path itself without touching your rate limits, use the staging dry run:

sudo certbot renew --dry-run

This checks that renewal would succeed against Let's Encrypt's staging environment. Whether --dry-run also executes deploy hooks has varied between Certbot versions, so don't rely on it alone to prove your hook — that's what the manual run above is for. Check certbot renew --help on your installed version for the current behaviour.

Verifying it works for real

First, confirm Certbot's timer is active and scheduled:

systemctl list-timers certbot.timer

You should see a next-run time. Certbot decides internally to renew only when a cert is within ~30 days of expiry, so most runs correctly do nothing.

After the next real renewal (or after a manual test run), check the journal for your log lines:

journalctl -t certbot-deploy --since "1 hour ago"

Confirm the certificate itself is current:

sudo certbot certificates

And confirm the running nginx is actually serving the new certificate — this is the whole point, catching the case where the file changed but the process didn't reload:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates

The notAfter date should match the freshly renewed certificate.

Rolling back

There's nothing subtle to undo. Remove the hook and Certbot goes back to renewing certificates without reloading anything:

sudo rm /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Your certificates and renewal config are untouched by this whole procedure — you only ever added and removed one script.

For the full list of hook types and the environment variables Certbot exposes, see the "Renewing certificates" section of the official Certbot user guide on the EFF Certbot documentation site (eff-certbot.readthedocs.io). Confirm any flag against the certbot version you actually have installed with certbot --version before relying on it.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.