树莓派Docker部署acme.sh:HTTPS证书自动续签实践指南
发布时间:2026/9/11 6:44:03 锦皓数字建站

前四篇我们把树莓派的 Docker 环境、网页服务、域名反代都一一跑通了文章发布之后收到不少留言问得最多的就是 HTTPS 怎么弄。尤其是现在浏览器一看到 HTTP 就满屏警告很多自媒体平台和微信小程序也强制要求 HTTPS 回源证书这件事确实躲不开。这篇继续沿着系列往下走把第五块拼图补齐用 acme.sh 容器化部署解决证书申请和自动续签。先说结论树莓派这种长期开机、资源又紧张的小主机acme.sh 是我试下来最对味的方案。纯 shell 实现镜像体积小支持几十家 DNS 服务商的 API 验证自动续签也足够聪明。文章里的操作我都在树莓派 4B 上实测过命令可以直接抄连踩坑记录都一并奉上。1. 为什么是 acme.sh容器化证书续签的选型逻辑1.1 证书续签这件事为什么不能手动用 Lets Encrypt 签发的免费证书有效期是 90 天。言下之意每三个月你得重新申请一次。手动申请本身不麻烦麻烦的是你得记得这件事。树莓派这种常年塞在弱电箱或者角落里吃灰的设备别说三个月三天不看它一次都算不错了。等到浏览器提示证书过期、用户访问出现红色警告的时候才想起来处理已经晚了。所以自动续签不是舒适性需求是刚需。这个逻辑和服务器上跑 cron 定时备份一样核心思路是把有固定周期的重复劳动交给机器的时钟去处理而不是依赖人脑的记忆。1.2 与 certbot 的对比轻量程度和 DNS 验证的差异市面上主流的免费证书客户端无非两个certbot 和 acme.sh。我两个都在树莓派上试过说点主观感受。certbot 优势是生态成熟、插件多安装的时候一键搞定 Nginx 配置。但放在树莓派上有个尴尬它依赖的 Python 环境和插件体系在容器里跑起来体积不小尤其是你要用 DNS 方式验证域名所有权的时候还得专门装对应 DNS 服务商的插件镜像一拉就是几百 MB。而且 certbot 原版容器对 ARM 架构的优化一般我的 2GB 内存树莓派跑起来能明显感到内存占用偏高。acme.sh 就一个 shell 脚本容器镜像不到 20MB随便跑。它最大的亮点是 DNS API 验证覆盖面极广阿里云、腾讯云、Cloudflare 这些主流 DNS 服务商全都原生支持不需要额外装插件只要设置几个环境变量就能开始签发。这对没有公网 IP、只能走内网穿透或者 DDNS 的用户来说太重要了。另外 acme.sh 的续签策略我比较欣赏默认证书剩余天数少于 60 天就自动续签而不是死板地掐着 90 天整。这样哪怕某次续签失败了你还有将近一个月的缓冲时间去处理不会等到最后一天才发现证书废了。1.3 系列环境的自然衔接前四篇搭好的是树莓派 Docker Compose Nginx 反代这一套基础框架。本文延续这个架构思路所有新组件都以容器方式加入不污染宿主机系统。这样整条链路都是可复现、可迁移的哪天你想把整套服务搬到另一台机器上只需要复制一份目录、改一下域名配置跑起来就行。2. 动手之前先理清三件事域名解析、验证方式和系统时间2.1 确认域名已经正确解析到树莓派证书签发的第一步是证明“你确实拥有这个域名”。无论用哪种验证方式前提都是域名能指向你的服务器或者域名是你的 DNS 服务商账号下可以操作的。如果你有公网 IP 并且做过端口映射那直接 A 记录解析到公网 IP 就完事。如果像大多数家庭宽带用户一样没有公网 IP走的 DDNS 动态解析那要确认解析记录是通的——在局域网内可以先用内网 IP 临时验证但最终证书签发流程必须让 Lets Encrypt 的服务器能从公网访问到你的服务或者通过 DNS API 完成验证。我的做法是路由器上把 80 和 443 端口映射到树莓派域名用 DDNS 自动更新解析记录。测试解析是否生效在树莓派上执行ping yourdomain.com看返回的 IP 是否是公网地址对应的内网映射。注意如果你觉得端口映射不安全至少要把 80 端口映射出去。因为 HTTP-01 验证方式必须通过 80 端口访问一个临时验证文件这个文件由 acme.sh 自动生成。443 端口可以等证书签下来之后再做映射或者干脆只映射 80。2.2 HTTP-01 和 DNS-01两种验证方式的取舍ACME 协议里最常见的验证方式有两种HTTP-01 验证Lets Encrypt 服务器会尝试访问http://你的域名/.well-known/acme-challenge/xxx这个临时路径。如果返回内容匹配说明你拥有这个域名的 HTTP 服务控制权。这种方式的优点是配置简单缺点是你必须有一个能从公网访问到的 80 端口或者至少让验证请求能穿透到你树莓派上的某个容器。DNS-01 验证Lets Encrypt 会要求你在域名的 DNS 解析记录里添加一条指定的 TXT 记录。acme.sh 通过调用 DNS 服务商的 API自动添加和删除这条记录。这种方式的优点是不需要任何公网端口哪怕树莓派在内网深处、完全没有公网暴露只要能联网访问 DNS 服务商的 API 就能完成验证。缺点是配置 API 密钥复杂一些而且有些 DNS 服务商添加记录后有生效延迟。排除标准很简单有公网端口、域名解析正常优先用 HTTP-01省事没有公网 IP、纯内网环境、或者用 Cloudflare 这类托管 DNS 的直接选 DNS-01。2.3 树莓派的系统时间一个隐藏的坑这个坑我在树莓派上反复踩过也是很多新手签证书失败之后百思不得其解的原因。树莓派没有硬件 RTC 时钟模块系统时间是靠开机后联网同步的。如果 NTP 同步失败系统时间停留在几个月或者几年前HTTPS 握手会直接失败——因为证书有效期校验依赖时间戳。在开始一切之前先确认时间date如果时间不对启用时间同步sudo timedatectl set-ntp true timedatectl status看到System clock synchronized: yes和NTP synchronized: yes再继续下一步。我见过不少人在 NAS 或者树莓派上折腾了半天证书最后发现是时区问题导致的奇奇怪怪的错误日志。3. 部署 acme.sh 容器的完整配置拆解3.1 目录规划让数据和配置都持久化容器化的精髓就是把一切可持久化数据都挂载到宿主机。acme.sh 所有的配置、证书、ACME 账号信息都保存在一个目录里。我习惯把它放在/opt/docker/acme.sh这个目录下。目录结构/opt/docker/acme.sh/ ├── docker-compose.yml └── data/ # 挂载到容器的 /acme.sh创建目录sudo mkdir -p /opt/docker/acme.sh/data cd /opt/docker/acme.sh3.2 docker-compose.yml 逐行解读在/opt/docker/acme.sh目录下创建docker-compose.ymlversion: 3.8 services: acme.sh: image: neilpang/acme.sh:latest container_name: acme.sh hostname: acme.sh command: daemon environment: - LE_CONFIG_HOME/acme.sh - TZAsia/Shanghai volumes: - ./data:/acme.sh - webroot:/var/www/html:rw restart: unless-stopped volumes: webroot: name: nginx_webroot逐项说明几个关键配置command: daemon是官方镜像的常驻模式。acme.sh 镜像默认有两个运行形态交互模式和守护模式。交互模式跑完一条命令进程就退出适合手动操作daemon 模式启动后容器内部会拉起 crond 计划任务默认每小时检查一次所有证书的剩余时间发现需要续签就自动执行完全不需要宿主机再配置额外 cron。LE_CONFIG_HOME/acme.sh指定 acme.sh 的工作目录。所有配置、账号、证书都会落在这个目录下并通过卷挂载到宿主机./data。这是个重要细节如果不设置这个环境变量镜像默认工作目录是/root/.acme.sh那样数据虽然也持久化了但目录名会让很多第一次用的人找不到证书在哪。webroot:/var/www/html:rw这个卷是为了 HTTP-01 验证准备的。如果你只用 DNS-01 验证这个挂载可以省略。我的场景是既有公网端口又想保留 DNS 验证的灵活性所以一开始就把两者都配好。它和 Nginx 容器共享同一个 webroot 卷acme.sh 写入验证文件Nginx 对外提供访问两者不需要额外通信非常干净。TZAsia/Shanghai设置时区。如果不设置容器默认 UTC 时间和树莓派宿主机时间相差 8 小时。虽然不影响证书签发的正确性但看日志的时候会让人困惑。顺手改了。3.3 为什么不挂载 docker.sock网上很多镜像方案会在 acme.sh 容器里挂载/var/run/docker.sock目的是让 acme.sh 容器在续签完成后直接命令 Nginx 容器重载配置。这个方案很流行但我不推荐在树莓派上这么做。原因有两层。第一是安全docker.sock 是 Docker 守护进程的管理接口谁访问到它谁就能在宿主机上执行任意容器操作这是典型的权限放大风险。第二是必要性存疑后面会讲证书重新加载其实有更轻量、更安全的替代方案挂 docker.sock 属于为了省事而牺牲安全性在一个长期开机的内网设备上尤其划不来。3.4 启动容器并验证状态docker compose up -d docker ps看到acme.sh容器处于Up状态就说明配置没问题。再确认一下容器日志没有报错docker logs acme.sh --tail 20首次启动不会做任何签发操作它只是拉起 crond 并待命。所以我习惯在正式签发前先用普通命令测试一下容器的执行环境比如进入容器查看版本号docker exec acme.sh --version能看到版本号说明镜像正常、挂载正常这才算部署完成。4. 签发证书实测两种验证模式的完整命令4.1 用 webroot 模式签发第一张证书webroot 模式属于 HTTP-01 验证的容器化实现。特点是 acme.sh 不需要开额外的监听端口只需要能往 Nginx 的站点目录里写入验证文件。先确认 Nginx 容器已经挂载了名为nginx_webroot的卷并且网站的根目录指向这个卷。如果之前系列文章里 Nginx 配置的 root 路径不是这个卷需要先调整 Nginx 容器配置否则验证文件写了也访问不到。验证 Nginx 挂载docker inspect nginx --format {{json .Mounts}}确认之后执行签发命令。把yourdomain.com换成你自己的域名docker exec acme.sh --issue \ -d yourdomain.com \ -w /var/www/html \ --server letsencrypt过程大概十几秒。如果看到Cert success字样说明签发成功。acme.sh 默认使用 ECC 证书密钥是 ECDSA 的比 RSA 密钥短对树莓派这种处理器性能不那么强的设备友好TLS 握手开销也更小。如果签发失败先不要慌。90% 的概率出在验证文件无法从公网访问上curl http://yourdomain.com/.well-known/acme-challenge/test手动创建一个测试文件用curl验证一下 Nginx 能不能正确路由到这个路径。公网访问不通先排查路由器端口映射和防火墙规则。4.2 用 DNS-01 模式签发以阿里云 DNS 为例如果你的树莓派没有公网端口或者你不想动路由器就用 DNS-01 模式。这里以阿里云为例其他服务商的流程大同小异。先去阿里云控制台创建一个 RAM 子账号只需要授予域名解析相关的权限不要直接用主账号的 AccessKey。在 acme.sh 容器里设置环境变量docker exec -e Ali_Key你的AccessKey ID \ -e Ali_Secret你的AccessKey Secret \ acme.sh --issue \ -d yourdomain.com \ --dns dns_ali \ --server letsencryptacme.sh 收到命令后会调用阿里云 DNS API 自动添加一条 TXT 记录等待生效后请求 Lets Encrypt 签发证书签发完成后删除 TXT 记录。整个流程完全自动化。第一次执行时acme.sh 会把Ali_Key和Ali_Secret保存到account.conf配置文件中。以后自动续签的时候不需要再传环境变量它自己会读取配置。所以密钥只需要在首次导入的时候出现一次。这里有个重要的安全提醒不要把这些密钥写进 docker-compose.yml 的 environment 字段然后提交到 git。我见过不止一个人把阿里云密钥推到公开仓库里结果账号被拿去刷流量。正确做法是用docker exec -e临时注入或者使用 Docker secrets 机制。4.3 用 --install-cert 固定证书输出位置签发成功后证书会放在/acme.sh/yourdomain.com_ecc/目录下。但这个目录结构对 Nginx 不友好文件名也不统一。我更推荐用--install-cert参数把证书复制到规范的路径docker exec acme.sh --install-cert \ -d yourdomain.com \ --ecc \ --key-file /acme.sh/certs/yourdomain.com/key.pem \ --fullchain-file /acme.sh/certs/yourdomain.com/fullchain.pem执行完成后证书就落在/acme.sh/certs/yourdomain.com/这个固定目录下。Nginx 配置的时候只需要指向这一条路径以后不管续签多少次路径永远不变。--ecc参数是因为我们签的是 ECC 证书不加这个参数acme.sh 可能去找 RSA 证书然后报错找不到文件。在宿主机上确认文件生成ls -l /opt/docker/acme.sh/data/certs/yourdomain.com/看到key.pem和fullchain.pem两个文件说明安装成功。4.4 让 Nginx 容器看到证书文件Nginx 容器要读证书就得把宿主机的证书目录挂载进去。修改 Nginx 的 docker-compose.yml在 volumes 里添加services: nginx: volumes: - /opt/docker/acme.sh/data/certs:/etc/nginx/certs:ro然后用一份最小化的 HTTPS 配置验证一下。假设你的站点目录是/etc/nginx/htmlNginx site 配置大概是server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/nginx/certs/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/yourdomain.com/key.pem; root /etc/nginx/html; index index.html; }重载 Nginxdocker exec nginx nginx -t docker exec nginx nginx -s reload浏览器访问https://yourdomain.com看到小锁图标就说明整条链路已经通了。5. 自动续签的验证与 Nginx 重载联动5.1 容器内置定时任务的工作原理acme.sh 容器以 daemon 模式运行时内部跑了一个 crond。它默认每隔 1 小时扫描一次所有已签发的证书用 OpenSSL 检查证书剩余天数。当剩余天数小于 60这个阈值可以通过配置文件修改时自动执行续签。这个机制的好处是容错性好。它不依赖宿主机 crontab不需要你记得往里加任务而且续签失败后下一个小时会继续尝试不会因为一次偶发网络错误就彻底放弃。5.2 强制续签用来验证全流程配置好之后我强烈建议你手动强制跑一次续签确认整条链路是通的不要坐等 60 天后让它自己跑。毕竟你无法预知那时候的 Nginx 配置会不会因为后续改动而出问题。强制续签命令docker exec acme.sh --cron --force这个命令会忽略证书剩余时间强制对所有托管证书重新签发。执行完成后检查证书文件是否更新——最简单的办法是看证书的到期日期openssl x509 -in /opt/docker/acme.sh/data/certs/yourdomain.com/fullchain.pem -noout -dates注意到期时间变成从现在开始算的 90 天之后说明续签成功。注意--force会实际调用 Lets Encrypt 的签发接口消耗一定的请求配额。免费证书的配额限制为每周 5 张正常测试一两张完全没问题。如果你要频繁测试签发流程建议使用临时测试环境docker exec acme.sh --issue \ -d yourdomain.com \ -w /var/www/html \ --server letsencrypt_test测试环境签发的证书不被浏览器信任但它能帮你验证整个签发流程是否正确且完全不影响生产配额。确认流程没问题后用--server letsencrypt重新签发正式证书。5.3 证书重载的三种做法与我的取舍证书续签完成文件已经变成新的了但如果 Nginx 没有重新加载它内存里还是旧的证书。这里就需要一个机制让 Nginx 在续签完成后重新读取证书。有三种常见做法做法一挂载 docker.sock 让 acme.sh 直接操作 Nginx续签命令后加--reloadcmd docker exec nginx nginx -s reload。好处是续签成功后立即触发重载一步到位。坏处是前面说的 docker.sock 安全风险。做法二宿主机 crontab 定期触发 Nginx 重载在宿主机上配置一个 crontab每 6 小时执行一次0 */6 * * * docker exec nginx nginx -s reloadNginx reload 本身是热重载对运行中的连接几乎无感知只是重新读取一次配置文件。哪怕你一天 reload 好几次代价也极小。这个做法胜在思路简单不关心证书是否更新过反正定时重载一次如果证书没变这次 reload 就是空操作如果证书变了恭喜恰好被捕捉到了。做法三用 acme.sh 的 hook 脚本在容器内置目录放一个脚本续签后由 acme.sh 自身调用。这个方式可以做到最精细的控制但配置复杂度略高需要额外维护脚本文件。我实际用的是做法二。原因很简单一个 crontab 规则就能解决所有问题不引入安全风险而且在树莓派这种低负载设备上每 6 小时 reload 一次 Nginx 对性能的影响可以忽略不计。不管用什么方案核心思路都是一致的让 Nginx 定期或按需重新加载证书文件并且保证这个动作足够鲁棒不会因为偶发异常导致服务中断。6. 我在树莓派上踩过的坑与排查清单6.1 standalone 模式端口冲突最经典的翻车现场我第一次在树莓派上签证书时用的是 standalone 模式。这种模式下 acme.sh 自己监听 80 端口来响应验证请求。问题是树莓派上 Nginx 早就占用了 80 端口两个进程抢同一个端口结果 Nginx 正常服务acme.sh 直接报bind: address already in use。解决方案有两条路要么像前面一样用 webroot 模式让 Nginx 继续占着 80 端口acme.sh 只往站点目录写临时文件要么给 acme.sh 容器单独映射一个端口比如把宿主机的 8080 映射到容器的 80然后加--httpport 8080参数。前者更符合 Nginx 容器的常规部署场景推荐优先用前者。6.2 证书文件权限不足导致 Nginx 无法读取这是另一个典型问题。默认情况下acme.sh 容器以 root 用户运行生成的证书文件权限是600只有 root 能读。如果 Nginx 容器以非 root 用户运行读取证书时会报Permission denied。排查命令ls -l /opt/docker/acme.sh/data/certs/yourdomain.com/如果文件权限是-rw-------需要改成 Nginx 可读sudo chmod 644 /opt/docker/acme.sh/data/certs/yourdomain.com/*.pem sudo chmod x /opt/docker/acme.sh/data/certs/yourdomain.com/但 chmod 只是临时方案下次续签重新生成文件后权限会重置。更稳妥的办法是在--install-cert命令中加--key-file后配合权限设置或者让安装命令带上chmodhook。不嫌麻烦的话直接在 Nginx 的 compose 配置里强制指定运行用户为 root但我不建议这么做安全账不划算。我当时的最终方案是写了个小脚本续签命令之后自动chmod 644一行 crontab 的事。6.3 系统时间错乱导致签发失败有段时间我的树莓派经常重启重启后时间停留在上次断电的时刻NTP 同步需要几秒甚至十几秒。acme.sh 在时间没同步好之前发起请求Lets Encrypt 服务器拒绝签发报错信息是Error creating new order :: Cannot parse request之类。解决方案就是前面第 2 节说的提前检查timedatectl status并且建议给树莓派装上更可靠的时间同步服务。我实际用了 chrony 替代默认的 systemd-timesyncd它对网络抖动更宽容在老旧的 SD 卡设备上表现也稳定。6.4 域名解析到内网 IP验证请求压根到不了树莓派如果你在家里局域网内测试很可能会顺手把域名解析到内网 IP比如192.168.1.100然后在局域网里访问一切正常。但 Lets Encrypt 服务器在公网上它访问你这个内网 IP 是进不来的。解决方案是让解析记录始终指向公网 IP局域网内想测试就临时改 hosts 文件或者使用 DNS 的 split-horizon 功能如果路由器支持。判断问题出在这里的方法很简单dig short yourdomain.com如果返回的是192.168.x.x而你又没有做正确的 NAT 回流配置那 HTTP 验证请求大概率就是卡在公网到内网这一段。6.5 证书文件看起来是新的浏览器的锁还是红色这种场景大多出现在续签之后没有重载 Nginx之前讲过的 reload 机制就是为了解决这个问题。另外还有一种情况你用了 Cloudflare 的 CDN 代理浏览器访问的是 Cloudflare 边缘节点的证书你源站更新了证书但 CDN 节点还是在用旧的。遇到这种情况需要在 CDN 控制台里把 SSL/TLS 模式从“灵活”改为“全端到端”并上传源站证书的 CA 根证书。排查证书链路是否完整的命令openssl s_client -connect yourdomain.com:443 -servername yourdomain.com重点关注输出里的Verify return code: 0 (ok)如果不是 0根据具体错误码去排查。6.6 多域名证书和泛域名证书的注意事项如果树莓派上跑了多个服务比如一个 WordPress、一个导航页、一个 API你当然可以给每个域名各签一张证书。但更省事的方式是申请多域名证书或者直接上泛域名证书docker exec acme.sh --issue \ -d yourdomain.com \ -d *.yourdomain.com \ --dns dns_ali \ --server letsencrypt泛域名证书必须走 DNS-01 验证因为*.yourdomain.com这种记录无法通过 HTTP 验证来证明所有权。签发成功后在 Nginx 里可以给所有子域名用同一张证书省心很多。注意泛域名证书的私钥安全更敏感因为它能覆盖你所有子域名的 HTTPS 服务妥善保管好/acme.sh目录的备份。7. 最后补一句整个证书链路的健全性自检等整条链路跑通并验证了自动续签之后我建议你把以下几条命令整理成一个脚本定期跑一遍# 1. 检查容器是否正常运行 docker ps | grep acme.sh # 2. 检查证书是否在有效期内 openssl x509 -in /opt/docker/acme.sh/data/certs/yourdomain.com/fullchain.pem -noout -dates # 3. 检查从外部访问 HTTPS 是否正常 openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -brief # 4. 检查 Nginx 是否能正常读取证书 docker exec nginx nginx -t把这四条存成一个.sh脚本配合 cron 每个月跑一次基本上可以确保证书链路不会在你毫不知情的情况下默默失效。树莓派这类设备最大的特点就是常年不闻不问正因如此把所有监控都做成自动化的、有输出的才能避免三个多月后突然发现全站无法访问的尴尬。按照这个流程走下来从签发到自动续签再到 Nginx 自动重载整条链路已经闭环。往后只需要每月花一分钟看一眼脚本输出其他时候完全不用管它。树莓派虽然性能不强但就它承担的这些轻量任务来说稳定跑上一整年不折腾绝对绰绰有余。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。