# Certbot 续期钩子：pre、post、deploy hook 用法

> Certbot 续期后自动重载服务：--deploy-hook、--pre-hook、--post-hook、renewal-hooks 目录、RENEWED_LINEAGE 变量和 certbot reconfigure 用法。

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

Certbot 续期后要靠钩子重载服务：deploy hook 只在证书续期成功后运行，适合放重载命令；钩子可以保存在单张证书的续期配置里，也可以放进 `/etc/letsencrypt/renewal-hooks/` 公共目录。

适用于 Linux 上的 Certbot 4.1 及以上，钩子机制在很多版本里都没有变化。

## 前提条件

配置钩子需要一张已有的 Certbot 证书和 root 权限。

- 用 snap 或 pip 安装的 Certbot，至少有一张证书（`sudo certbot certificates`）。
- 清楚哪些服务在用这张证书，以及让它加载新文件的命令（`systemctl reload nginx`、`systemctl restart postfix` 等）。
- 用 standalone 方式的证书，要知道续期时哪个服务占着 80 端口。

## 第一步：选对钩子类型

重载服务用 deploy hook；pre 和 post hook 用来在 standalone 模式下停止和启动占用 80 端口的服务。三种钩子的运行时机：

| 钩子 | 运行时机 | 常见用途 |
|---|---|---|
| `--pre-hook` | 一次 `renew` 中，尝试续期前运行一次 | standalone 模式下先停掉占用 80 端口的服务 |
| `--deploy-hook` | 每张续期成功的证书各运行一次 | 重载 Nginx、复制文件、重启邮件服务 |
| `--post-hook` | 一次 `renew` 中，所有续期尝试结束后运行一次 | 启动 pre-hook 停掉的服务 |

没有证书到期时，三种钩子都不会运行。deploy hook 能读取两个环境变量：`RENEWED_LINEAGE`（如 `/etc/letsencrypt/live/example.com`）和 `RENEWED_DOMAINS`（空格分隔的域名）。

## 第二步：给单张证书保存钩子

给已有证书加 deploy hook，不用重新签发：

```bash
sudo certbot reconfigure --cert-name example.com --deploy-hook "systemctl reload nginx"
grep -E 'hook' /etc/letsencrypt/renewal/example.com.conf
```

续期配置文件里会出现 `renew_hook = systemctl reload nginx`。也可以在第一次申请时直接带上，例如 `sudo certbot certonly --webroot -w /var/www/html -d example.com --deploy-hook "systemctl reload nginx"`。

standalone 方式、80 端口平时被 Nginx 占用的证书：

```bash
sudo certbot reconfigure --cert-name example.com \
  --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
```

这样续期时网站会短暂中断；Web 服务器已经在运行时，改用 webroot 方式更好。

## 第三步：用钩子目录放公共逻辑

下面三个目录里的脚本对所有证书生效，按文件名字母顺序执行。Certbot 3.2.0 之前只在 `certbot renew` 时运行，3.2.0 起用 `certonly` 申请新证书时也会运行：

```text
/etc/letsencrypt/renewal-hooks/pre/
/etc/letsencrypt/renewal-hooks/deploy/
/etc/letsencrypt/renewal-hooks/post/
```

按续期的域名决定重载哪些服务：

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

脚本必须有执行权限，失败时返回非零退出码，错误才会记录到 `/var/log/letsencrypt/letsencrypt.log`。

## 第四步：给读不到 /etc/letsencrypt 的服务复制证书

以普通用户运行的服务读不了 `/etc/letsencrypt/live`，在 deploy hook 里按正确的属主和权限复制，先写临时文件再替换：

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

## 验证

`certbot renew --dry-run --run-deploy-hooks` 会在测试环境把续期流程完整跑一遍，包括 deploy hook：

```bash
sudo certbot renew --dry-run --run-deploy-hooks
sudo tail -n 50 /var/log/letsencrypt/letsencrypt.log
```

再用 [SSL 证书检查](https://ssl.sh/zh/tools/ssl-checker/) 确认客户端实际看到的证书，或在终端运行：

```bash
curl ssl.sh/example.com
```

下一次续期后，这里显示的到期时间应该往后推。

## 自动续期

钩子只有在续期运行时才起作用。snap 版自带 `snap.certbot.renew.timer`，每天运行两次 `certbot renew`；pip 安装的要按官方说明加 cron。Certbot 4.1 及以上按 Let's Encrypt 的 ARI 窗口续期，续期时间可能早于常规的剩余三分之一有效期，所以钩子要能在任何时候安全运行。

```bash
systemctl list-timers | grep certbot
```

Let's Encrypt 有效期 2027 年 2 月 10 日缩短到 64 天、2028 年 2 月 16 日缩短到 45 天，钩子运行会越来越频繁，必须保证可以重复执行。把域名加到 [ssl.sh 监控](https://ssl.sh/zh/monitor/)（开发中），钩子失效时也能及时发现。

## 常见问题排查

- **Hook command returned error code 1**：用 root 带上相同的环境变量手动运行，例如 `sudo RENEWED_LINEAGE=/etc/letsencrypt/live/example.com /etc/letsencrypt/renewal-hooks/deploy/10-reload.sh`。
- **目录里的钩子没运行**：文件没有执行权限，或文件名含有会被跳过的字符，用 `chmod 755` 和普通文件名。
- **钩子运行了，线上还是旧证书**：服务在复制文件之前就重载了，或者读的是另一个路径，用 `curl ssl.sh/example.com` 检查线上证书。
- **standalone 续期报 80 端口被占用**：加 pre/post 钩子或改用 webroot，其他 80 端口原因见 [certbot 连接超时](https://ssl.sh/zh/errors/certbot-timeout-during-connect/)。
- **续期后 nginx -t 报 key values mismatch**：钩子复制了不配对的文件，见 [nginx key values mismatch](https://ssl.sh/zh/errors/nginx-key-values-mismatch/)。
