Nginx 上的证书过期,我遇到过不止一次,基本都是客户反馈网站打不开,查下去才发现证书忘了换。另一种问题更难查:换证时只放了站点证书,没带中间证书,电脑上的浏览器访问正常,手机上却报证书错误。
这篇讲怎么让 Nginx 的证书自动续期,续期后自动部署并 reload。前提是 Nginx 直接跑在 Linux 服务器上,站点已经配好 HTTPS,证书现在靠手动下载、上传、替换。还没配 HTTPS 的站点、跑在 Docker 里的 Nginx 不在这篇范围内;Nginx 前面还有 CDN 的,证书应该部署到 CDN 上。
手动换证常出的两个问题
最常见的是忘了换。阿里云免费证书有效期 3 个月,一个站点一年要换四次左右,站点一多就不能靠记性。我设过日历提醒,后来换手机,提醒跟着丢了。重要的站点改买付费证书,有效期长一些,换的次数少了。但付费证书的有效期也在缩短:公开证书的最长有效期从 2026 年 3 月 15 日起是 200 天,2027 年 3 月 15 日起是 100 天,2029 年 3 月 15 日起是 47 天(CA/B Forum SC-081v3)。到 2027 年 3 月以后,付费证书要换的次数和 3 个月的免费证书已经差不多。
另一个是证书链不全。Nginx 的 ssl_certificate 要指向包含中间证书的完整链(fullchain)。有的证书下载下来是站点证书和中间证书分开的两个文件,只配了前一个,部分浏览器会自己补全中间证书,看起来一切正常;手机上的浏览器很多不会补,就直接报错。这个问题我每次都是拿报错上网查,才定位到是证书链,再回去补上。
站点多的时候,手动换证的成本会逼着人改架构。我管过一台测试服务器,一个 Nginx 挂了十几个站点。阿里云免费证书只支持单个域名,每年 20 张,十几个站点就要申请十几张,每 3 个月轮一遍。为了少折腾,我们先把这些站点放到阿里云全站加速后面,在控制台换证书比往服务器传文件省事;后来干脆只给一个专门的 SSL 子域名配证书,Nginx 按 URL 前缀转发到各个业务。证书是少了,代价是多了一层本来不需要的转发。
用 Certbot 自动续期
Certbot 我自己没怎么用过,这一节按它的官方文档整理,命令都以 root 执行。
先要知道一点:certbot renew 只续由 Certbot 签发的证书,从阿里云下载的证书它不会续。所以每个站点要先用 Certbot 重新申请一张:
certbot --nginx -d www.example.com
--nginx 会申请证书,并把 Nginx 配置里这个站点的证书路径改到 /etc/letsencrypt/live/www.example.com/ 下,需要公网能访问这台服务器的 80 端口。用 apt、snap 等方式安装的 Certbot 一般已经带了续期定时任务,这样申请的证书续期后会自动 reload Nginx。
不想让 Certbot 改 Nginx 配置,可以用 certbot certonly --nginx -d www.example.com 只申请证书,自己把 ssl_certificate 和 ssl_certificate_key 改成上面目录里的 fullchain.pem 和 privkey.pem。这时续期后的 reload 要自己配,把它写成 deploy hook 放到 /etc/letsencrypt/renewal-hooks/deploy/ 目录。这个目录里的可执行文件只在证书真正续期成功后运行,没有证书需要续期时不会 reload:
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
配好后用 --dry-run 验证。--dry-run 默认不运行 deploy hook,加上 --run-deploy-hooks 才会把 reload 一起测到:
certbot renew --dry-run --run-deploy-hooks
已经在用 Certbot、续期一直正常的单机,继续用就行。我那台测试服务器的十几个站点,要逐个改由 Certbot 申请,Nginx 配置里的证书路径也都要换掉。下面这种做法沿用现有的证书路径,续期和部署结果在控制台里统一看。
用 LapseZero 自动续期和部署
LapseZero 是一个证书托管服务,负责签发证书、到期前自动续期,并部署到服务器和云产品上。部署到 Nginx 时,在服务器上装一个客户端,它会找出 Nginx 上已有的 HTTPS 站点,之后把新证书写到这些站点原来的证书路径,检查配置后 reload,再确认线上已经换上新证书。一台 Nginx 上有多个站点、还在手动换证,又不想自己维护续期和部署,我推荐用 LapseZero。
域名验证用 CNAME 委派:每个域名在 DNS 里加一条 _acme-challenge CNAME,只加一次,以后续期不用再改 DNS,也不用把 DNS 服务商的密钥交出去。签发的是公开受信任的 DV 证书,主流浏览器和手机都认。
客户端的权限、数据和卸载
在服务器上装一个以 root 运行的程序,我第一反应会担心三件事:权限、会不会改坏 Nginx 配置、会不会把服务器暴露出去。
权限:安装和运行都需要 root,客户端作为 systemd 服务运行。它要读 Nginx 配置、写证书文件、执行 reload。客户端代码开源(GitHub、Gitee),安装脚本也可以先下载下来看一遍再运行。
配置:客户端用 nginx -T 读取配置,不修改配置文件,只替换你选定站点的证书和私钥文件,保留原来的属主和权限。几个站点共用一组证书文件的,「发现站点」会把它们合成一行显示,选中这一行,这几个站点会一起换。nginx -t 不通过就不会 reload;写入证书或 reload 失败时,自动换回原来的证书。
网络:客户端只主动连接 LapseZero 的 443 端口领取部署任务,自己不监听端口,服务器不用开放入站端口,也不用提供 SSH 密码。
发给平台的内容:扫描时只上报每个站点的域名、证书路径和当前证书的公开信息(签发者、到期日),不读取私钥,不上传配置全文。证书私钥由平台生成并加密保存,部署时通过 HTTPS 下发给客户端写入服务器。
不想用了:以 root 运行 lapsezero-agent uninstall 卸载客户端,网站正在用的证书文件会保留。再删掉那条 _acme-challenge CNAME,平台就不能再为这个域名签发证书。
1. 安装客户端
在控制台「服务器」里点「添加客户端」,填一个名称,系统选 Linux,点「生成安装命令」。命令类似:
curl -fsSL https://cn.lapsezero.com/install.sh | sh -s -- --token <一次性 token>
在服务器上以 root 运行。命令 1 小时内有效,只能用一次;需要带 systemd 的 Linux,x86_64 和 ARM64 都支持。安装只要几秒,装完控制台里这台服务器显示「在线」。

2. 发现站点,添加域名
在这台服务器的菜单里点「发现站点」。几秒后列出所有已配置 HTTPS 的站点,包括域名、证书文件路径、当前证书的签发者和到期日。只配了 HTTP 的站点不会出现在这里。
还没加到账号里的域名,旁边有「添加 www.example.com」这样的按钮。点开后域名已经填好,验证方式默认是 CNAME 委派,点「下一步」。

3. 添加 CNAME 记录
页面给出一条 CNAME 记录:主机记录是 _acme-challenge.<子域名>,记录值是一串以 .acme.lapsezero.com 结尾的地址。到 DNS 服务商那里加上这条记录,回来点「验证 CNAME 并完成」。
主机记录按所在 DNS 区域填相对名称。比如域名是 www.example.com,在阿里云的 example.com 下添加,主机记录填 _acme-challenge.www,后面不要再带 example.com。这条记录只用于证书验证,不影响网站访问。


验证通过后会自动重新扫描,刚才的站点变成可接管,并且已经勾选。
4. 接管站点并签发
每个站点下面有部署后命令,默认是 nginx -t && systemctl reload nginx,需要时可以改。确认后点「接管所选站点」,页面提示「已接管。下面的域名还没有证书,签发后会自动部署到对应站点。」这里说的是账号里还没有证书,服务器上原来的证书照常在用,签发失败也不影响网站。再点「立即签发」。

5. 等待部署完成
大约 1 分钟后,页面显示「证书已签发并部署完成」,站点状态是「部署成功 · 已生效」。这期间客户端把证书和私钥写到站点原来的路径,执行部署后命令,再在本机按域名连接 443 端口,确认 Nginx 返回的已经是新证书。所以「已生效」说明的不只是文件写进去了,还有 Nginx 已经在用新证书。写入的是带中间证书的完整链,手机上报证书链错误的问题也一起解决了。

之后不用再操作。证书剩余有效期不到总有效期的 1/3 时(90 天的证书大约还剩 30 天)自动续期,续期成功后部署到同样的位置。签发失败、部署失败、部署后线上没生效,默认都会发邮件到账户邮箱;自动续期卡住时,到期前 14、7、3、1 天还会再提醒。续期我还没等到真实发生过一次。
一台 Nginx 上有十几个站点
这些站点在同一个父域名下时,比如 a.test.example.com、b.test.example.com,只需要加一个通配符域名 *.test.example.com:一条 CNAME,签一张证书,发现站点时这些站点都会匹配到它,接管后一次签发全部部署。几个站点共用同一组证书文件的,「发现站点」的按钮会直接推荐添加通配符域名。通配符只覆盖一层子域名,x.a.test.example.com 不在 *.test.example.com 范围内。
套餐按域名数和部署目标数计算,一组证书文件路径算一个部署目标:
| 套餐 | 域名 | 部署目标 | 价格 |
|---|---|---|---|
| 免费版 | 3 个 | 5 个 | 免费 |
| 入门版 | 10 个 | 20 个 | ¥6/月或 ¥60/年 |
| 专业版 | 30 个 | 80 个 | ¥15/月 |
十几个站点在一个通配符下、共用一组证书文件,免费版就够;各用一组证书文件,部署目标超过 5 个,要用入门版。如果这些站点是十几个互不相关的域名,用不了通配符,域名数超过入门版的 10 个,要用专业版。
CNAME 验证不通过
页面会给出具体原因,常见的有三种:
- 「公开 DNS 暂未查询到这条 CNAME」:记录还没加,或者刚加还没生效。主机记录填错也是这条提示,所以等了几分钟还不通过,先回去核对主机记录和记录类型。
- 「公开 DNS 已检测到 CNAME,但当前指向……,需要改为……」:记录值填错了,按提示改成页面给出的值。
- 「你的 DNS 里有泛解析记录指向……,查询返回的是它」:域名配了
*泛解析,查询_acme-challenge时被泛解析接住了。按页面的值加上这条 CNAME 就行,它优先于泛解析生效,泛解析不用动。
装好客户端后点一次「发现站点」,就能看到这台 Nginx 上每张证书的签发者和到期日,可以从快到期又不太重要的站点开始接管。