# Certbot renewal hooks: pre, post and deploy hooks explained

> Reload services after Certbot renews: --deploy-hook, --pre-hook, --post-hook, the renewal-hooks directories, RENEWED_LINEAGE and certbot reconfigure.

Source: https://ssl.sh/guides/certbot-renewal-hooks/
Last updated: 2026-11-10

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:

```bash
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:

```bash
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`:

```text
/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:

```bash
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:

```bash
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:

```bash
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](https://ssl.sh/tools/ssl-checker/), or from any terminal:

```bash
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.

```bash
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](https://ssl.sh/monitor/) (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 755` and 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](https://ssl.sh/errors/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](https://ssl.sh/errors/nginx-key-values-mismatch/).
