ACME clients and automation
How to use Certbot renewal hooks to reload services
Certbot runs a deploy hook after each successful renewal, so the deploy hook is where you reload Nginx, restart a mail server or copy files. Save it in the certificate's renewal configuration with certbot reconfigure, or put a script in /etc/letsencrypt/renewal-hooks/deploy/ to cover every certificate.
The steps apply to Certbot 4.1 and later on Linux; the hook mechanism has been stable for many releases.
Prerequisites
You need an existing Certbot certificate and root access.
- Certbot installed from snap or pip, with at least one certificate (
sudo certbot certificates). - The services that use the certificate and the command that makes each one load new files (
systemctl reload nginx,systemctl restart postfix, and so on). - For standalone certificates, knowledge of which service holds port 80 during renewal.
Step 1: Pick the right hook type
Use a deploy hook to reload services, and pre and post hooks to stop and start a service around a standalone renewal. The three hooks run at different times:
| Hook | When it runs | Typical use |
|---|---|---|
--pre-hook |
Once before any renewal is attempted in a renew run |
Stop a service that holds port 80 for --standalone |
--deploy-hook |
Once per certificate that was successfully renewed | Reload Nginx, copy files, restart a mail server |
--post-hook |
Once after all renewal attempts in a renew run |
Start the service stopped by the pre-hook |
If no certificate is due, none of the hooks run. Deploy hooks see two environment variables: RENEWED_LINEAGE (for example /etc/letsencrypt/live/example.com) and RENEWED_DOMAINS (space-separated names).
Step 2: Save a hook on one certificate
certbot reconfigure attaches a deploy hook to an existing certificate without reissuing it:
sudo certbot reconfigure --cert-name example.com --deploy-hook "systemctl reload nginx"
grep -E 'hook' /etc/letsencrypt/renewal/example.com.conf
The renewal file now contains renew_hook = systemctl reload nginx. You can also pass the hook on the original command, for example sudo certbot certonly --webroot -w /var/www/html -d example.com --deploy-hook "systemctl reload nginx".
For standalone certificates whose port 80 is normally used by Nginx:
sudo certbot reconfigure --cert-name example.com \
--pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
Webroot mode avoids the downtime and is preferable when the web server is already running.
Step 3: Use the hook directories for shared logic
Scripts in these directories run for every certificate in alphabetical order. Before Certbot 3.2.0 they ran only during certbot renew; since 3.2.0 they also run when you obtain a certificate with certonly:
/etc/letsencrypt/renewal-hooks/pre/
/etc/letsencrypt/renewal-hooks/deploy/
/etc/letsencrypt/renewal-hooks/post/
A deploy script that reloads services depending on which certificate was renewed:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/10-reload.sh > /dev/null <<'EOF'
#!/bin/sh
set -e
case "$RENEWED_DOMAINS" in
*mail.example.com*) systemctl restart postfix dovecot ;;
esac
systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/10-reload.sh
Scripts must be executable and should exit non-zero on failure so errors appear in /var/log/letsencrypt/letsencrypt.log.
Step 4: Copy files for services that cannot read /etc/letsencrypt
Services running as unprivileged users cannot read /etc/letsencrypt/live. Copy the files with the right owner and mode in a deploy hook, writing atomically:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/20-myapp.sh > /dev/null <<'EOF'
#!/bin/sh
set -e
[ "$(basename "$RENEWED_LINEAGE")" = "example.com" ] || exit 0
dest=/etc/myapp/tls
install -d -o myapp -g myapp -m 700 "$dest"
install -o myapp -g myapp -m 644 "$RENEWED_LINEAGE/fullchain.pem" "$dest/fullchain.pem.new"
install -o myapp -g myapp -m 600 "$RENEWED_LINEAGE/privkey.pem" "$dest/privkey.pem.new"
mv "$dest/fullchain.pem.new" "$dest/fullchain.pem"
mv "$dest/privkey.pem.new" "$dest/privkey.pem"
systemctl reload myapp
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/20-myapp.sh
Verify
A dry run with --run-deploy-hooks tests renewal and every hook against the staging server without replacing your certificates:
sudo certbot renew --dry-run --run-deploy-hooks
sudo tail -n 50 /var/log/letsencrypt/letsencrypt.log
Then confirm what clients see with the SSL checker, or from any terminal:
curl ssl.sh/example.com
After the next real renewal, the expiry date reported there should move forward.
Automatic renewal
Hooks only matter if renewal runs. The snap installs snap.certbot.renew.timer, which runs certbot renew twice a day; pip installs need the cron line from the Certbot instructions. Certbot 4.1 and later follow Let's Encrypt's ARI window, so renewals can come earlier than the usual one-third-of-lifetime point, and your hooks must be safe to run at any time.
systemctl list-timers | grep certbot
As Let's Encrypt lifetimes shrink to 64 days (February 10, 2027) and 45 days (February 16, 2028), hooks run more often; keep them idempotent. Add the domains to ssl.sh monitoring (in development) to catch a hook that silently stopped working.
Troubleshooting
- Hook command returned error code 1: run the command by hand as root with the same environment variables, for example
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/example.com /etc/letsencrypt/renewal-hooks/deploy/10-reload.sh. - Directory hook did not run: the file is not executable, or its name contains a character Certbot skips; use
chmod 755and plain names. - Hook ran but the old certificate is still served: the service was reloaded before files were copied, or it reads a different path; check the live endpoint with
curl ssl.sh/example.com. - Standalone renewal fails with port 80 in use: add pre/post hooks or switch to webroot; see certbot timeout during connect for other port-80 causes.
- nginx -t fails after renewal with key values mismatch: a hook copied mismatched files; see nginx key values mismatch.
FAQ
What is the difference between renew_hook and deploy_hook?
They are the same thing. Older Certbot versions called the option --renew-hook; it was renamed --deploy-hook, but the renewal configuration file still stores it under the key renew_hook.
Do I need a hook if I used certbot --nginx or --apache?
No. The installer plugins reload the web server after renewal automatically. You need a deploy hook when you used certonly, or when other services (mail server, HAProxy, Java apps) also use the certificate.
Can a hook copy the certificate to another server?
Yes. A deploy hook can run rsync or scp with a dedicated SSH key and then reload the remote service. Make sure the copy is atomic (write to a temp file and move it) and that failures exit non-zero so they show up in the Certbot log.