前端证书是不是又快到期了?手里域名多、子域名更多的时候,每三个月手动申请一次证书、再手动扔进 Nginx、还要记得 reload,这套流程重复几轮之后真的会烦到怀疑人生。我最近把整套签发链路彻底换成了 acme.sh + Docker + 腾讯云 DNSPod API 的自动化方案:DNS 解析自动验证、泛域名证书一次申请、到期自动续签。这篇文章就是我的完整部署记录,所有步骤都是实测跑通的,照着做基本能实现“只要域名还在你手里,证书就永远不会过期”。
这个方案尤其适合这几类人:家里或公司跑了一堆自建服务,比如 Nextcloud、GitLab、Grafana、API 网关,每个服务都想要 HTTPS;或者你在做小程序、App 的后端,需要给多个子域名统一配证书;再或者纯粹是厌倦了每三个月调一次闹钟去续证书。接下来我从方案选型开始讲,再到具体配置、排错,全程带命令和解释。
1. 方案选型拆解:为什么是 acme.sh + DNS API + Docker
1.1 泛域名证书到底解决了什么问题
泛域名证书,也叫通配符证书,一张证书里包含 *.example.com 这样的通配条目。只要 DNS 解析落在 example.com 这个主域下,不管是 blog.example.com、api.example.com 还是 dev.example.com,这张证书都能直接覆盖。
那为什么不给每个域名单独申请一张证书?第一是管理成本,申请、续签、部署、监控,每多一张证书就多一圈事情;第二是浏览器兼容性,一张证书挂满所有子域名,用户访问任何一个子服务都不会出现证书不匹配的告警;第三是新增子域名的时候,你不需要再申请新证书,只要把 DNS 解析指过去,HTTPS 马上就能用。
需要注意一个细节:通配符证书的 *.example.com 并不包含裸域 example.com 本身。所以我实际操作时,申请命令里会同时带 -d example.com -d "*.example.com" 两个域名,等于一张证书把裸域和所有一级子域全覆盖。Let's Encrypt 对泛域名证书的签发是完全免费的,有效期 90 天,所以自动续签就成了刚需。
1.2 为什么验证方式必须用 DNS-01 而不是 HTTP-01
ACME 协议里常用的域名验证方式有两种:HTTP-01 和 DNS-01。
HTTP-01 的流程是向 http://你的域名/.well-known/acme-challenge/xxx 放一个随机文件,证书机构来访问这个文件,能访问到就证明你控制了这个域名。这种方式配置简单,但不支持签发泛域名证书,因为它验证的只是某一个具体域名,没法覆盖通配符。
DNS-01 则是要求你在域名的 DNS 解析记录里添加一条 _acme-challenge 的 TXT 记录,记录值是证书机构下发的随机字符串。证书机构去查询这条 TXT 记录,能查到就证明你对该域名有控制权。因为 DNS 解析本身是域名级别的权限,所以 DNS-01 才能用来签发 *.example.com 这种泛域名证书。
DNS-01 的问题是手动操作太痛苦:每次申请或续签,你要去 DNS 控制台手动加一条 TXT 记录,等生效,验证完再删掉。三个月的周期说长不长,但手动折腾几次就容易漏。所以真正靠谱的方式是让证书签发工具直接调用 DNS 服务商的 API,自动添加和删除 TXT 记录,全程无人工介入。acme.sh 内置了大量 DNS 服务商的 API 插件,腾讯云 DNSPod 正好是支持最完善的那一批。
1.3 容器化部署的价值在哪里
acme.sh 本身是一个纯 Shell 脚本,直接装在宿主机上也能跑。那为什么我还坚持用 Docker 容器部署?
首先是环境隔离。acme.sh 运行时会写一堆配置、证书和日志文件,装到宿主机上会散落在各个目录,卸载也麻烦。用容器之后,所有数据都集中在一个挂载卷里,想迁移就直接把卷拷走;想升级 acme.sh 版本就重新拉镜像,不用管宿主机的依赖。
其次是定时任务不用单独配。acme.sh 官方镜像 neilpang/acme.sh 提供了 daemon 模式,容器启动后内部会自动按周期检查证书是否需要续签。如果直接装在宿主机上,你还得自己写 crontab,还要考虑 crontab 的环境变量、路径问题。
第三是统一运行时。acme.sh 依赖 curl、openssl、cron 这些工具,不同系统版本可能缺东西。容器镜像把这些都打包好了,换一台机器部署,一条命令拉起来就能用,行为完全一致。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前的准备:域名、API 密钥与 Docker 环境
2.1 域名托管确认:解析必须在 DNSPod
acme.sh 的 DNS API 插件本质上是调用 DNSPod 的 OpenAPI 去操作你的解析记录,所以前提条件是你的域名 NS 记录指向 DNSPod,或者在腾讯云 DNSPod 控制台里能看到并管理这个域名的解析记录。
很多人会混淆“域名在腾讯云注册”和“解析在 DNSPod”这两个概念。事实上,只要你的域名解析托管在 DNSPod 控制台,不管注册商是哪家都可以用这个方案。判断方法很简单:登录 DNSPod 控制台,找到“我的域名”,能看到你的域名并且能添加解析记录,就说明满足条件。
如果你的域名解析还在其他服务商,比如阿里云、Cloudflare,那就需要先做一次 DNS 迁移,把 NS 记录改成 DNSPod 的地址,等解析完全生效后再继续。这一步不做好的话,后面 acme.sh 会一直报“域名不存在”之类的错误。
2.2 生成 DNSPod API Token(两种方式任选)
DNSPod 现在有两条 API 认证路线,acme.sh 都支持,我分别说清楚,方便你按自己的账号情况选择。
第一种是 DNSPod 传统的 API Token 方式。登录 DNSPod 控制台,进入“账号中心 -> 密钥管理 -> API Token”,创建一个新 Token。创建完成后你会得到两个关键信息:ID 和 Token。这里有个常见坑:Token 值只在创建成功那一刻完整显示一次,关掉页面就再也看不到了,所以务必马上复制保存。后面在 acme.sh 里对应的环境变量是 DP_Id 和 DP_Key。
第二种是腾讯云 CAM 方式。如果你的账号已经习惯了腾讯云的 API 密钥体系,可以在腾讯云访问管理 CAM 里创建一组 SecretId 和 SecretKey,然后在 acme.sh 里对应设置 Tencent_SecretId 和 Tencent_SecretKey,插件选择 dns_tencent。这种方式的权限控制更细,但前提是你的腾讯云账号和 DNSPod 账号已经完成了关联。
我实际更常用的是第一种 DP_Id/DP_Key 方式,因为配置最简单,一次申请命令里直接传两个环境变量就够了。如果你的域名和腾讯云账号是通的,那用 CAM 方式也没问题,两条路都能走通。
2.3 Docker 环境准备及 Windows/Mac 常见坑
这个方案的正式运行环境我建议放在一台长期在线的 Linux 服务器上,不管是云服务器、NAS 还是树莓派都行。安装 Docker Engine 本身很简单,直接按官方文档用 apt 或 yum 装就行,装完执行 docker version 能正常输出 client 和 server 信息就说明装好了。
如果你只是想在本地 Windows 或 Mac 的 Docker Desktop 里先测试一遍,有一个非常常见的启动报错:virtualization support not detected,Docker Desktop 起不来。这种问题几乎都是本机虚拟化没开,或者系统虚拟化组件没装全。解决办法是进 BIOS/UEFI 开启 Intel VT-x 或 AMD-V,然后在 Windows 功能里打开“适用于 Linux 的 Windows 子系统”和“虚拟机平台”,装好 WSL2 之后再启动 Docker Desktop。Mac 的话主要是检查是否允许使用 Virtualization Framework。
不过说实话,如果你只是测试流程,Windows/Mac 上跑通没问题;但真正要放到生产环境自动续签,我强烈建议还是迁移到 Linux 服务器上,省电省心,重启策略也好控制。
3. 完整实操:一次申请泛域名证书
3.1 首次签发命令与关键参数说明
先创建证书和数据文件的存放目录,我用的是 /opt/acme.sh,这个路径后面会作为 Docker 挂载卷使用。目录创建好之后,执行以下命令进行一次性的证书签发:
bash复制docker run --rm -it \
-v /opt/acme.sh:/acme.sh \
-e DP_Id="你的DNSPod_ID" \
-e DP_Key="你的DNSPod_Token" \
neilpang/acme.sh --issue \
--dns dns_dp \
-d example.com \
-d "*.example.com" \
--server letsencrypt
这里每个参数都有讲究。--rm -it 表示容器是一次性的、前台交互运行,证书签发完容器就销毁,方便直接看日志输出;-v /opt/acme.sh:/acme.sh 是关键,容器内的 /acme.sh 目录是 acme.sh 存放配置和证书的地方,挂载到宿主机后,即使容器删了数据也还在;DP_Id 和 DP_Key 就是我们在 DNSPod 控制台创建 API Token 时拿到的两个值。
--dns dns_dp 是让 acme.sh 使用 DNSPod 的 DNS API 插件来完成验证。-d example.com -d "*.example.com" 是本次要签发的域名列表,裸域加泛域名一条龙搞定。最后 --server letsencrypt 显式指定使用 Let's Encrypt 作为证书颁发机构,避免某些情况下默认走 ZeroSSL 需要额外注册邮箱。
如果你选择的是腾讯云 CAM 密钥,命令改成这样:
bash复制docker run --rm -it \
-v /opt/acme.sh:/acme.sh \
-e Tencent_SecretId="你的SecretId" \
-e Tencent_SecretKey="你的SecretKey" \
neilpang/acme.sh --issue \
--dns dns_tencent \
-d example.com \
-d "*.example.com"
3.2 签发过程中的验证链路解析
执行签发命令后,你会看到 acme.sh 打印出一系列日志,我建议第一次操作时不要跳过看日志,因为整个验证链路的信息全在里面。
首先,acme.sh 会向 Let's Encrypt 发起证书申请,拿到一个用于 DNS 验证的随机字符串。然后 acme.sh 调用 DNSPod 的 API,在你域名的解析记录里自动添加一条主机记录为 _acme-challenge 的 TXT 记录,值就是那个随机字符串。这个操作是真实发生在你的 DNSPod 控制台里的,如果你这时候打开 DNSPod 的解析记录页面,运气好的话能看到这条 TXT 记录短暂出现一下,验证完成后它又会被 acme.sh 自动删除。
TXT 记录添加之后,acme.sh 会等待一段时间让记录在全球 DNS 节点生效,默认的等待时间依赖各家 DNS 的 TTL 设置,有时会稍微慢一点。等待结束后,Let's Encrypt 会去查询这条 TXT 记录,查到并且值匹配,就证明你确实拥有该域名的控制权,然后签发证书。整个过程看起来像是在终端里自动跑的魔法,实际上就是 ACME 协议加 DNS API 的标准配合。
这里要给新手一个提醒:如果日志里出现 TXT record not found 或者 Timeout 之类的字眼,别急着怀疑 API 密钥有问题,很可能是 DNS 生效慢。可以给命令加上 --dnssleep 30,让 acme.sh 在验证前多等 30 秒,大部分情况下能解决问题。
3.3 验证证书产物:证书文件与有效期
签发成功之后,去挂载目录看一下产物:
bash复制ls -la /opt/acme.sh/example.com_ecc/
你会看到 example.com.cer、example.com.key、fullchain.cer、ca.cer 等文件。目录名字带 _ecc 是因为 acme.sh 默认使用的是 ECC 椭圆曲线算法,生成的私钥是 EC 密钥,比传统 RSA 密钥更短、握手更快。如果你的业务系统对客户端兼容性要求特别高,需要 RSA 证书,可以在签发时加 --keylength 2048 强制生成 RSA 证书。
用下面这条命令可以查看证书的基本信息:
bash复制openssl x509 -in /opt/acme.sh/example.com_ecc/fullchain.cer -noout -dates -subject -issuer
输出里最重要的两行是 notBefore 和 notAfter,能看到证书有效期是 90 天。subject 里会包含 CN = example.com 和 DNS:*.example.com 的条目,说明通配符覆盖是生效的。这一步确认没问题,签发环节就算彻底跑通了。
4. 自动续签与落盘部署联动
4.1 容器的 daemon 模式与自动续签机制
一次性签发只是第一步,整个方案的核心价值在于自动续签。acme.sh 官方镜像提供了 daemon 模式,容器启动后会常驻运行,默认每 12 小时检查一次所有已签发的证书,如果发现距离过期不足 60 天,就会自动重新走一遍“DNS 添加 TXT 记录 -> 验证 -> 签发 -> 删除记录”的流程。
正式部署时,我把一次性容器改成常驻容器:
bash复制docker run -d \
--name acme.sh \
--restart unless-stopped \
-v /opt/acme.sh:/acme.sh \
-e DP_Id="你的DNSPod_ID" \
-e DP_Key="你的DNSPod_Token" \
neilpang/acme.sh daemon
-d 表示后台运行,--name acme.sh 给容器起个固定名字,--restart unless-stopped 让 Docker 在容器异常退出或服务器重启时自动把容器拉起来。
这里要特别强调一个容易踩的坑:DP_Id 和 DP_Key 环境变量必须在创建容器的时候就传进去。acme.sh 判断证书需要续签时,是要拿着这两个值重新去调用 DNSPod API 添加 TXT 记录的。如果容器创建时漏了环境变量,或者后面用 docker update 想补环境变量,你会发现 Docker 并不能直接更新已有容器的环境变量。正确做法是删掉旧容器,保留挂载卷,用带环境变量的命令重新创建。因为证书数据都挂在 /opt/acme.sh 卷里,重建容器不会丢任何证书。
4.2 证书部署到 Nginx 的实操示例
证书自动续签成功只是完成了“拿到新证书”这一步,还要让 Web 服务器真正用上它。如果你用的是 Nginx,最简单的部署方式是直接把 acme.sh 的挂载目录共享给 Nginx 容器,或者在宿主机 Nginx 的配置里指向这个目录。
这里给一个典型的 Nginx 配置片段:
nginx复制server {
listen 443 ssl;
server_name example.com *.example.com;
ssl_certificate /opt/acme.sh/example.com_ecc/fullchain.cer;
ssl_certificate_key /opt/acme.sh/example.com_ecc/example.com.key;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
关键问题是:acme.sh 自动续签后证书文件会更新,但 Nginx 不会自己感知文件变化,需要 reload 才会加载新证书。网上很多教程到这里就没说清楚,导致很多人以为续签完就万事大吉,结果证书更新了但服务还在用旧证书,最后一天突然暴雷。
我实测下来最省心的方案是在宿主机上配置一条 cron,每天凌晨执行一次平滑 reload:
bash复制0 3 * * * /usr/sbin/nginx -s reload
nginx -s reload 是平滑重载,不会中断现有连接,只是让 Nginx 重新读取一遍配置文件和证书文件。每天 reload 一次的成本极低,但能保证证书续签后最迟 24 小时内生效。如果你不想每天 reload,也可以写一个脚本去检测 fullchain.cer 的修改时间,有变化才执行 reload,但对绝大多数场景来说,每天凌晨 reload 一次是最简单可靠的方案。
4.3 续签日志、权限与重启后的注意事项
常驻容器运行后,查看续签是否正常有两个手段。第一是看容器日志:
bash复制docker logs acme.sh
日志里会周期性出现类似 Skip, Next renewal time is: ... 的输出,这表示 acme.sh 检查过证书,还没到续签时间,一切正常。如果日志里出现 Renew: ... 之类的字段,说明它正在执行续签流程。
第二是手动触发一次强制续签来测试整个链路是否通畅。注意,不要用 --force 直接对同一个域名反复强制续签,Let's Encrypt 有速率限制,同一个域名每周最多签发 5 张重复证书,频繁测试容易把自己封进黑名单。真想测试,可以等证书进入续签窗口期,或者用全新的测试域名。
权限问题也值得单独说。acme.sh 容器内运行时的用户 ID 可能和宿主机不同,挂载出来的证书文件权限可能会让 Nginx 或其他服务读不了。解决办法有两种:一是部署联动脚本时用 cp 把证书复制到目标目录,并显式设置权限;二是在宿主机上对关键证书文件执行 chmod 644(证书文件)和 chmod 600(私钥文件)。私钥文件权限千万别放太开,否则等于把 HTTPS 的底裤晾在门口。
5. 常见问题与排查经验速查
5.1 高频问题现象与解决方案
这个方案我前前后后跑了半年多,各种问题基本都遇到过,把高频的整理成一个速查表,方便你直接对号入座。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
报错 Domain not found |
DNSPod 账号下没有该域名的解析权 | 确认域名解析确实托管在 DNSPod 控制台 |
报错 TXT record not found |
DNS 生效慢,验证超时 | 给签发命令加 --dnssleep 30 或增大数值 |
使用 dns_dp 验证失败 |
账号是腾讯云体系,老 Token 接口不适用 | 改用 dns_tencent + CAM 密钥方式 |
| 容器重启后没有自动续签 | 创建容器时漏了环境变量 | 删掉容器,保留挂载卷,重新创建并传入环境变量 |
| Nginx 无法加载证书文件 | 挂载目录权限不对 | 检查容器用户 ID,对证书文件执行 chmod 644/600 |
| 浏览器提示证书过期 | 续签成功但 Nginx 没 reload | 配置 cron 定时 reload,或部署后手动 reload |
| Docker Desktop 启动失败 | 虚拟化未开启或 WSL2 未安装 | 进 BIOS 开启虚拟化,安装 WSL2 并启用“虚拟机平台” |
5.2 几条实在的避坑心得
第一,申请泛域名证书时一定要把裸域一起带上。我见过有人只申请了 *.example.com,结果访问 example.com 本身的时候证书不匹配,浏览器直接红屏。命令里同时写 -d example.com -d "*.example.com" 是最保险的习惯。
第二,生产环境不要频繁测试签发。Let's Encrypt 的速率限制不是摆设,同一个域名反复签发会触发 too many certificates already issued 错误。想测试流程是否正常,优先看日志和模拟续签检查,不要动不动就 --force。
第三,API Token 的保存要讲究。DNSPod 的 Token 只在创建时显示一次,而且这个 Token 拥有解析记录的读写权限,泄露给别人意味着对方可以随意篡改你的 DNS 解析。建议把 Token 放密码管理器里,不要写进代码仓库或者贴在博客里。容器运行时会通过 docker inspect 看到环境变量值,所以运行环境本身也要保证安全。
第四,关于证书算法的选择。acme.sh 默认生成 ECC 证书,体积小、握手快,绝大多数现代浏览器和设备都支持。但如果你的服务要兼容特别老的客户端,比如旧版 Android、老式嵌入式设备,建议额外生成 RSA 证书备用。
第五,如果你在同一台机器上既跑了 acme.sh 容器又跑了 Nginx 容器,注意别把 acme.sh 的挂载卷和 Nginx 的证书目录搞混。我的习惯是单独建一个 /opt/certs 目录,用部署脚本把 acme.sh 新签发的证书复制过去,再统一交给 Nginx 使用,这样即使以后重装 acme.sh 也不会影响正在运行的服务。
我个人在实际操作中还有一个心得:整个链路跑通之后,定期翻一下 docker logs acme.sh 的输出,花不了半分钟,但能提前发现环境变量失效、DNS API 权限变动之类的问题。这比等到证书过期当天被监控告警砸醒要从容得多。这套方案搭好之后,你基本可以忘了“续证书”这件事,域名解析不换、容器不删,证书就会一直自动续签下去,这也是我强烈推荐它的原因。
