很多人把证书装上就以为万事大吉,结果半年后访问网站突然报 NET::ERR_CERT_DATE_INVALID——一看,证书早过期了,自动续签压根没生效。本文基于一套已经稳定运行的 Docker + Nginx 部署模式,讲清 acme.sh 自动续签的完整链路,以及 如何确认”自动续签真的在工作”。
以下配置均已脱敏:域名用
example.com代指,证书目录用/path/to/nginx/cert,容器名用nginx-container。
一、部署模式概述:证书是怎么”自动”起来的
1.1 整体架构
宿主机 Docker 容器
┌──────────────────────┐ ┌──────────────────────┐
│ acme.sh(cron) │ │ nginx-container │
│ │ │ │ │
│ ├─ DNS API 签发 │ │ /etc/nginx/cert:ro │←── 挂载
│ ├─ 写证书到 cert 目录 │ │ /etc/nginx/conf.d:ro │←── 挂载
│ └─ reload 容器 │ │ │
└──────────────────────┘ └──────────────────────┘
几个关键设计点:
- 证书文件直接用域名命名(如
example.com.pem/example.com.key),不做订单号前缀,文件名稳定,配置里永远引用同一个路径。 - acme.sh 跑在宿主机,写入的是挂载给 nginx 容器的宿主目录(
/path/to/nginx/cert)。挂载是:ro,但 ro 只限制容器内写入,宿主机写不受影响。 - 签发后同步回”部署源”:项目的
./nginx/cert目录(部署脚本会rsync --delete用它推送线上)。这一步很关键——如果不回源,部署脚本的--delete会把刚签的新证当成”多余文件”删掉。 - reload 命令:
mkdir -p 部署源 && cp 新证 部署源/ && docker exec nginx-container nginx -s reload。续期成功后自动执行。
1.2 一张通配证书覆盖所有子域
用 2 张通配证书覆盖全部子域名(不细算具体数量,通常十几个):
acme.sh --issue --dns dns_ali -d example.com -d "*.example.com"
这是一条命令、一张证书(SAN 多域名证书),不是签两张:
| 证书里的 SAN | 覆盖范围 |
|---|---|
example.com |
域名本体(apex) |
*.example.com |
所有一级子域(www、blog、static、api…) |
为什么必须两个域名一起签?通配符不匹配 apex 本身——*.example.com 能命中 www.example.com,但命中不了 example.com。所以签发时要把本体域名也放进 SAN,和通配共用同一张证书。
- DNS API 方式签发,无需 80 端口验证、无需在每台服务器开 webroot
1.3 续期机制
# acme.sh 安装时自动登记的 cron(典型输出)
crontab -l | grep acme
39 1,7,13,19 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
- 每天固定时刻跑 4 次
--cron,每次只是检查,到期前 60 天内才真正续签(Let’s Encrypt 证书有效期 90 天) - 续签成功后自动执行
--reloadcmd(同步回源 + reload 容器) - 跑得频繁没有副作用:未到续期窗口时
acme.sh --cron空跑退出,不会骚扰 CA
二、如何确认自动续签真的生效(五层验证)
第 1 层:cron 任务在不在
crontab -l | grep acme
看到 acme.sh --cron 行 = 定时任务已登记。这是”自动”的前提,但只代表任务挂了,不代表续签成功过。
第 2 层:续签日志
tail -50 ~/.acme.sh/acme.sh.log
- 看到
Renew: example.com、Success、Install cert等 → 续签执行过 - 看到
Cert not due for renewal/Skip, Next renewal time is:→ 未到续期窗口,正常 - 没有任何 Renew 记录 + 证书又快到期 → 有问题,需排查
第 3 层:证书有效期(核心判断)
openssl x509 -in /path/to/nginx/cert/example.com.pem -noout -dates
输出示例:
notBefore=Jul 20 12:00:00 2026 GMT
notAfter=Oct 18 12:00:00 2026 GMT
判断标准:
| 现象 | 结论 |
|---|---|
notBefore 在最近 60 天内 + notAfter ≈ 90 天后 |
✅ 自动续签生效过 |
notAfter 距今 < 60 天,cron 正常 |
⏳ 下次 cron 跑时会续,等待即可 |
notAfter 距今 < 30 天 |
⚠️ 逼近到期,检查 cron 和日志 |
notAfter 已过期 |
❌ 续签失效,立即手动 --renew --force |
第 4 层:证书文件写入时间
ls -l /path/to/nginx/cert/*.pem
续签会 覆盖 证书文件,文件 mtime 接近续签时间。如果 notBefore 是新的但文件时间很旧,说明写盘链路有问题。
第 5 层:线上实际加载的证书
宿主机看文件 ≠ 容器里加载的就是它。外部验证最真实:
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -dates -issuer
线上返回的 notAfter 与第 3 层一致 = nginx 已加载新证书。
三、常见问题排查
Q1:cron 在,但从来没有 Renew 记录,正常吗?
正常。acme.sh 是”到期前 60 天内才续”,证书刚签完 3 个月里每次 cron 都是空跑。只要 notAfter 没过期,就是健康的。
Q2:证书文件在 /etc/nginx/cert/ 里看不到?
检查容器挂载:
docker inspect nginx-container --format '{{json .Mounts}}' | python3 -m json.tool
确认宿主目录和容器路径与 acme.sh 写入的是同一个宿主机路径。
Q3:续签成功了,但网站还是旧证书?
- 确认
--reloadcmd里有没有docker exec nginx-container nginx -s reload(续期后必须 reload 才生效) - 浏览器缓存:用
openssl s_client外部验证,别用浏览器判断
Q4:换了域名/证书,旧的还自动续吗?
acme.sh 按域名管理证书,旧域名证书到 60 天窗口还会续。不用了应显式停止:
acme.sh --remove -d old.example.com
四、巡检清单(建议每季度跑一次)
# 0. 所有证书一览(确认域名都还在 acme.sh 管理列表中)
acme.sh --list
# 1. 任务在
crontab -l | grep acme
# 2. 证书没过期
openssl x509 -in /path/to/nginx/cert/example.com.pem -noout -dates
# 3. 线上加载的是新证
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -dates
三条命令、30 秒,确认自动续签链路健康。只要 cron 在、证书没到期、线上和文件一致,就放心让它自己跑——这套模式跑起来后,基本可以做到”证书管理零维护”。
补充提醒:acme.sh 自身也会迭代,建议偶尔执行
acme.sh --upgrade升级到最新版,避免旧版本逻辑导致签发/续期异常。升级不影响已签证书,重启 cron 后自动续期照常生效。
参考
- acme.sh 官方文档:https://github.com/acmesh-official/acme.sh
- Let’s Encrypt 证书有效期说明:https://letsencrypt.org/docs/faq/