尧图精选

树莓派Docker化部署acme.sh实现HTTPS证书自动续签

🕒 发布时间:2026/9/10 3:24:01 📁 来源:尧图网络
树莓派 Docker 化网页服务器这个系列写到第五篇了。前面几篇咱们把 Nginx、PHP、Node 这类容器都跑起来了网站确实能用 HTTP 正常访问了。可只要一部署到真实环境浏览器那个“不安全”的红色小锁就会一直翘在那访客心里犯嘀咕你自己看着也别扭。更别提现在微信小程序、部分 App 接口、很多第三方回调没有 HTTPS 根本不给调。所以这一篇专门把 HTTPS 这个事补上而且是彻底补上——在树莓派上用 Docker 容器跑 acme.sh让证书自动申请、自动续签以后完全不用手动去碰。1. 为什么偏偏是 acme.sh还要把它容器化先说证书从哪来。常见的免费方案有三类云厂商阿里云、腾讯云、华为云的免费证书一般 3 个月或 1 年有效期需要手动申请、手动部署Let’s Encrypt90 天有效期全自动续签自签名证书浏览器会直接报警告自己测试玩玩可以对外提供服务基本行不通。我直接排除了云厂商的方案原因很简单你买了几台云服务器可以每天登录控制台点几下树莓派这种东西是放在家里或者机房角落的没有人天天盯着它一旦证书过期没人发现网站就突然崩了。所以必须选一个能自动续签的方案。Let’s Encrypt 为什么设计成 90 天有效期而不是一年这个其实是有意为之的。短周期的证书能让私钥泄露的风险窗口变小同时促使整个生态把自动化做起来。对用户来说这个设计也变相倒逼你用脚本去管理证书而不是到期手动换一遍。我见过太多人申请一年期的免费证书结果第 11 个月发现忘了续临时抓瞎。自动续签这套流程无论如何都得配起来。接下来就是选客户端工具。目前最主流的是 certbot 和 acme.sh。certbot 功能全面官方推荐但相对重而且不同 Linux 发行版、不同版本的适配有时候会闹脾气。acme.sh 是一个纯 shell 实现的 ACME 客户端最大的优势是轻、依赖少逻辑非常直观。它对 DNS API 的支持尤其全腾讯云、阿里云、Cloudflare 这些主流 DNS 服务商的 API 全部内置配置好密钥就能一键签发。对于树莓派这种性能不强的设备acme.sh 这种体积小、占用低、一个脚本就能跑的工具显然比整套 certbot 环境更合适。那为什么还要把它塞进 Docker有人可能会说直接在树莓派的系统里装个 acme.sh 不是更省事吗确实省事但有一个核心问题你在这台树莓派上手动装了环境之后如果将来换机器、重装系统、或者推给另一台机器所有配置都要重来一遍。而 Docker Compose 的用法是一份 YAML 配置管全部拉到哪台机器上都能以一模一样的方式跑起来。另外acme.sh 容器本身很干净cron 服务、证书存储、重启策略全都封装好了不需要在宿主机里加乱七八糟的计划任务。这个系列的既定路线就是全部容器化所有服务都走 Docker那证书管理也一样保持架构上的一致性后续维护成本才会低。我当时的方案选型其实还有一层考量。acme.sh 容器使用的是 neilpang/acme.sh 这个镜像它是 acme.sh 作者自己维护的官方容器版镜像很小定时任务已经内置你只需要把证书目录挂载出来、设置邮箱和域名它自己就会每天跑一次检查。这个方案在 x86 和 ARM 架构上都能跑树莓派自然没问题省了我自己去折腾 cron 的时间。2. 先把自动续签的机制搞明白后面才不会踩坑在动手部署之前我得先讲清楚 acme.sh 到底是怎么做到“自动续签”的。这个机制如果不理解后面配置出问题或者调试异常主机时就会一头雾水。ACME 协议的全称是 Automatic Certificate Management Environment自动证书管理环境。流程大致是客户端生成一对密钥私钥和公钥然后向 Let’s Encrypt 的服务器发起申请同时通过某种方式证明“我确实拥有这个域名”验证通过后 CA 颁发证书客户端把证书保存到本地。整个交互基于 HTTPS 的 API 调用acme.sh 就是用 curl 去完成这些请求。续签的逻辑其实并不是“每隔 90 天自动换一次”而是 acme.sh 在容器里装了一个 cron 任务每天定时跑一次。每次运行时它检查所有已签发的证书还剩多少天有效期。如果少于 60 天默认阈值才会真正执行续签操作如果时间还充足就什么都不做直接跳过。为什么要每天跑一遍而不是算好时间在第 70 天跑一次因为“计划赶不上变化”这件事在运维里太常见了。假如你在第 70 天设了一个定时任务那天网络波动、服务器宕机或者 DNS 解析异常这次任务就漏掉了而下次执行要再等 70 天证书肯定就过期了。每天跑一次的话错过一天第二天还会再试容错率高很多。acme.sh 容器里默认的 cron 是每天 0 点跑一次当然也可以自定义但默认值已经足够可靠。如何验证域名所有权是这个流程里最关键的环节直接影响你要选哪种部署方式。ACME 协议里有两种主流验证方式HTTP-01 验证CA 服务器会尝试访问http://你的域名/.well-known/acme-challenge/xxx这个特定路径下的文件。如果你的服务器能在 80 端口响应这个请求CA 就确认你对域名有控制权。这个方式配置简单适合绝大多数情况但前提是你的 80 端口能被外网访问到。DNS-01 验证CA 服务器要求你在域名的 DNS 解析记录里添加一条 TXT 记录记录里包含一串随机值。你需要在 DNS 服务商的 API 里自动添加这条记录。这个方式的优势在于不需要 80 端口开放适合没有公网 IP、服务器藏在 NAT 后面、或者域名只用于 CDN 的情况。代价是必须支持 DNS 服务商的 APIacme.sh 集成了大量主流服务商接口。我在树莓派上的实际情况是这样的80 端口已经被容器的 Nginx占用了但这个并不是什么大问题因为 acme.sh 的 HTTP-01 验证并不需要独占 80 端口它可以用 webroot 模式。就是说你只需要让 Nginx 把/.well-known/acme-challenge/这个路径映射到某个真实目录acme.sh 把验证文件写到这个目录CA 就可以通过 HTTP 访问到并完成验证。但是这里有个非常经典的坑很多人的 Nginx 容器和 acme.sh 容器是分开跑的目录挂载如果不一致验证文件就写不进去或者写进去了但 Nginx 访问不到。解决办法是让这两个容器共享同一个数据卷或者直接用一个宿主机的公共目录同时挂载进两个容器。这个细节后面部署的时候会详细说。理解这个机制之后你就能明白为什么 acme.sh 需要在容器里常驻运行了。它不是一次性执行完就退出的工具它需要在后台保持 cron 服务运行每天检查证书有效期。这就是为什么要用容器的方式跑它而不是构建完镜像就结束——你需要一个守护进程而不是单次脚本。3. 树莓派上的完整部署流程一步一步来下面就是实操部分了。我家的树莓派 4B 现在跑着 Nginx 容器和几个应用容器所有服务的配置文件都规划在/opt/docker这个目录下每类服务有自己的子目录。这个习惯我从一开始就坚持因为树莓派换过两次存储卡恢复环境全靠这些文件。先看一下整体目录规划/opt/docker ├── nginx/ │ ├── html/ # 存放静态页面或者验证文件的公共目录 │ └── conf.d/ # Nginx 站点配置 │ └── certs/ # 证书文件存放目录acme.sh 会自动写到这里 └── acme.sh/ └── data/ # acme.sh 的数据目录存储证书和账号信息这个结构的关键设计是/opt/docker/nginx/certs这个证书目录会同时挂载进 acme.sh 容器和 Nginx 容器。acme.sh 把新证书写进去Nginx 容器立刻就能读到。不需要额外执行拷贝操作也避免了容器间通过宿主机中转文件的麻烦。接下来创建 acme.sh 的docker-compose.yml。我放在/opt/docker/acme.sh/下面version: 3 services: acme.sh: image: neilpang/acme.sh:latest container_name: acme.sh restart: always environment: - TZAsia/Shanghai - LE_CONFIG_HOME/acme.sh volumes: - /opt/docker/acme.sh/data:/acme.sh - /opt/docker/nginx/html:/webroot - /opt/docker/nginx/certs:/certs dns: - 223.5.5.5 - 119.29.29.29逐个解释这些配置的含义这不是为了凑字数是因为每个参数都可能决定你排错方向restart: always确保容器意外退出后会自动拉起acme.sh 的 cron 服务不会因为容器挂掉而罢工。TZAsia/Shanghai容器默认时区是 UTC如果你不设置时区定时任务每天 0 点跑实际上会变成 UTC 0 点也就是北京时间早上 8 点问题不大但日志时间会很别扭还是统一到北京时间比较舒服。LE_CONFIG_HOME/acme.sh指定 acme.sh 配置目录与宿主机/opt/docker/acme.sh/data绑定确保容器重建后证书和账号信息不丢。/opt/docker/nginx/html:/webrootwebroot 目录。acme.sh 做 HTTP 验证时会把验证文件写到容器的这个路径由于这个目录同时也被 Nginx 挂载了所以 Nginx 能读取到。这就解决了前面说的目录不一致问题。/opt/docker/nginx/certs:/certs证书输出目录。dns这里我设置了国内公共 DNS 服务器因为 Let’s Encrypt 的 API 解析在国内网络环境下偶尔会抽风换一个稳定点的 DNS 能减少一些随机性的超时问题。镜像拉下来之后第一次启动cd /opt/docker/acme.sh docker compose up -d启动之后先验证 acme.sh 的 cron 任务容器是不是正常起跑docker logs acme.sh如果看到类似cron: started的日志说明容器里的计划任务已经在跑了。但是这里必须强调一个容易忽略的点容器正常起来不代表你万事大吉了。你还需要手动做一次首次签发因为 acme.sh 默认不会自动给一个从没签发过的域名去申请证书。你必须先给 acme.sh 一个明确的指令告诉它你要签哪个域名、用什么验证方式。后续的自动续签工作才是它在后台自己完成的。首次签发命令如下docker exec acme.sh --issue -d example.com -d www.example.com -w /webroot --server letsencrypt参数拆解--issue执行签发动作。-d example.com -d www.example.com你要申请的域名。这里可以填多个-d参数acme.sh 会把这些域名合并进一张证书里。一般我会把主域名和 www 域名都加进去这样用户访问 www 前缀时也不会有证书报警。-w /webroot指定验证目录对应容器里的/webroot。--server letsencrypt暴露一下 CA 服务器虽然当前默认就是 Let’s Encrypt但显式声明能让后面切换 staging 或 ZeroSSL 的时候更清楚。第一次执行会完成整个 ACME 流程注册账号、创建私钥、生成验证文件、确认所有权、领取证书。如果看到输出结尾附近出现了Cert success或者类似的关键词就说明成功了。证书文件会存放在配置目录下你可以看一眼确认ls -la /opt/docker/acme.sh/data/example.com/这个目录下会出现一个以域名命名的子目录里面有几个文件example.com.cer证书链、example.com.key私钥、ca.cerCA 中间证书、fullchain.cer完整证书链。这些文件就是你后面要挂进 Nginx 的东西。如果是多域名单证书还会多一个fullchain.cer这个才是 Nginx 里ssl_certificate要用的。这里有个小细节要提前说如果第一次签发报了invalid response from http://...这种错大概率不是 acme.sh 的问题而是验证链路没走通。先不要急着调参数按第五章的排查思路检查路径映射和外网可达性。4. 验证文件与证书如何被 Nginx 正确使用证书签下来了接下来就是把它接入 Nginx。这一部分每一步都有讲究少一个环节证书就能生效但你访问网站时就会遇到奇怪的互相不匹配的问题。先配置 Nginx 容器的挂载。我的nginx/docker-compose.yml长这样version: 3 services: nginx: image: nginx:1.26-alpine container_name: nginx restart: always ports: - 80:80 - 443:443 volumes: - /opt/docker/nginx/html:/usr/share/nginx/html - /opt/docker/nginx/conf.d:/etc/nginx/conf.d - /opt/docker/nginx/certs:/etc/nginx/certs注意/opt/docker/nginx/certs这个目录同时挂载进 acme.sh 容器作为/certs和 Nginx 容器作为/etc/nginx/certs。这样做的效果是acme.sh 续签后写入的新证书文件Nginx 容器立刻就能看到不需要任何拷贝动作。接下来是 Nginx 的站点配置。我建议你新建一个独立的配置文件比如/opt/docker/nginx/conf.d/example.com.conf不要直接改 nginx.conf这样每个站点独立配置互不干扰server { listen 80; server_name example.com www.example.com; location /.well-known/acme-challenge/ { root /usr/share/nginx/html; } location / { return 301 https://$host$request_uri; } } server { listen 443 ssl; server_name example.com www.example.com; ssl_certificate /etc/nginx/certs/example.com/fullchain.cer; ssl_certificate_key /etc/nginx/certs/example.com/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; root /usr/share/nginx/html; }两个 server 块分别处理 80 和 443 端口这样只会让普通 HTTP 请求访问验证路径其余请求统一 301 跳到 HTTPS。验证路径映射到 webroot 目录acme.sh 才能不断续签而ssl_certificate和ssl_certificate_key则直接指向共享证书目录里的文件。修改完配置后验证一下 Nginx 配置语法是否正确然后重载docker exec nginx nginx -t docker exec nginx nginx -s reload这时再访问你的域名浏览器地址栏应该会出现小锁不再有任何报错。证书自动续签到这里为止看似已经闭环每天 acme.sh 检测、快到期就续签、文件被新证书覆盖。但是这里有一个非常隐蔽的坑——续签成功了Nginx 服务可能还在用旧证书。这正是证书自动化的一个经典问题。acme.sh 续签成功后只是更新了文件Nginx 不会自己热加载新的证书。你需要让 Nginx 重新加载一次配置文件才能把新证书加载进内存。如果这一步没有自动化那就等于 90 天后证书文件是新的但 Nginx 还在用旧的直到旧证书过期你的网站照样报错。解决方式有两种。第一种是使用 acme.sh 的--install-cert参数在续签成功后执行 reload 命令docker exec acme.sh --install-cert -d example.com -d www.example.com \ --key-file /certs/example.com/example.com.key \ --fullchain-file /certs/example.com/fullchain.cer \ --reloadcmd docker exec nginx nginx -s reload第二种是更省心的方式给 Nginx 容器配一个周期性的 reload 计划任务或者用 Watchtower 这类工具统一监听文件变化。我个人的习惯是第一周用--install-cert手动执行一遍确认整个链路可靠然后让 acme.sh 的 cron 每次续签后都自动触发 reloadcmd。这个--reloadcmd会被 acme.sh 记录到配置里下次自动续签时它会自动带上执行不需要每次都手动传。只要目录挂载设计合理这一套下来从证书申请到续签后 Nginx 加载新证书全链路不需要人工干预。5. 排错指南我在实际部署中踩过的坑签发证书和自动续签方案看着不复杂但真正操作起来会遇到一些很磨人的细节问题。我把实际部署中踩过的几个坑列出来按出现频率从高到低排。5.1 验证文件 404HTTP-01 验证始终失败这是最常见的错误现象是申请证书时 CA 提示无法访问http://你的域名/.well-known/acme-challenge/xxx。排查顺序这样走先确认你的 80 端口是否真的能从外网访问。树莓派在家用网络环境运营商可能屏蔽了 80 端口或者路由器没有做端口映射外部 CA 访问不到你的树莓派。这时候就算 acme.sh 把验证文件写进去了Let’s Encrypt 服务器也拉取不到。最简单的方式就是你手机切到 4G/5G 网络直接用浏览器访问http://你的域名/.well-known/acme-challenge/test.txt能打开就说明链路通。再检查 Nginx 的路径映射。很多人的问题出在这个细节acme.sh 容器里的 webroot 路径和 Nginx 容器里的 root 路径必须指向同一个真实目录。我见过有人 acme.sh 挂载的是/opt/docker/nginx/html但 Nginx 配置里写的 root 是/usr/share/nginx/html结果验证文件一边能写、一边读不到。解决办法就是确认两个容器挂载的宿主目录是同一个且 Nginx 配置里的 root 与容器内挂载点一致。5.2 端口 80 冲突standalone 模式报 bind 失败acme.sh 参数说明文档里会提到 standalone 模式这种模式下它会自己监听 80 端口来完成验证。但如果你已经用 Nginx 或者其他 Web 服务器占了 80 端口再运行 standalone 模式就会直接报 bind 失败。解决办法很简单不要用 standalone改用 webroot 模式配置好-w /webroot参数。如果你用的树莓派后面还有别的服务占着 80 端口那更好办用 DNS-01 验证完全不需要动 80 端口只需要配好 DNS API 密钥acme.sh 会自动往 DNS 解析记录里加一条 TXT 记录来验证域名所有权。我当时就是因为在树莓派上装了别的调试工具占用了 80 端口干脆切换到了 DNS-01 模式虽然多配了一步 DNS API 密钥但后续再也没有端口冲突的烦恼。5.3 容器时区错误续签时间点不符合预期默认情况下Docker 容器使用 UTC 时区而 acme.sh 的 cron 任务默认在每天 0 点执行。这样导致实际触发时间是北京时间的早上 8 点整。很多人对这个问题没有太多感知但如果你需要观察日志定位问题看到时间总差 8 个小时会很别扭。更实际的一个问题是如果某些脚本对时间有严格要求比如你设置了--days参数来定义提前几天续签时区混乱可能导致判断偏差。建议在 docker-compose 中显式设置环境变量TZAsia/Shanghai。这一点在树莓派这类嵌入式设备上尤其值得注意因为它们不像云服务器那样有现成的时区同步工具经常忘了配。5.4 证书目录权限不对Nginx 读不到私钥这是另一个高发问题。acme.sh 容器运行的用户是 root它写出的私钥文件权限是 600属主是 root。如果你把证书目录挂载给了 Nginx 容器而 Nginx 容器内部的工作进程不是以 root 身份运行就可能出现权限不足、读不了私钥文件的情况。典型报错是nginx: [emerg] cannot load certificate key /etc/nginx/certs/example.com/example.com.key: Permission denied排查方法很简单docker exec nginx ls -l /etc/nginx/certs/example.com/看到私钥文件的属主和权限如果是 root 600而 Nginx 进程用户是 nginx就会出问题。临时解决办法是chmod 644 /opt/docker/nginx/certs/example.com/example.com.key但这不是长久之计因为 acme.sh 每次续签后可能重新写入文件权限可能被改回去。比较稳妥的做法是在 docker-compose 中给 acme.sh 容器设置用户 ID 与 Nginx 容器一致或者在挂载的目录层级上统一权限。我在实际项目中是把 Nginx 容器的 user 改成 root有些基础镜像限制了用户改不了或者干脆把证书目录的属主直接 chown 成 nginx 用户。5.5 Hash 算法导致部分客户端兼容性问题Let’s Encrypt 早期默认签发的是 SHA-1 签名证书。现在主流 CA 都转向 SHA-256所以如果你的 acme.sh 版本很老签出来的证书可能还是旧的 Hash 算法会有较老系统或浏览器的兼容性问题。acme.sh 支持指定密钥强度建议在签发时加上--keylength ec-256这个参数会生成 ECC 证书使用 SHA-256 签名算法体积更小、握手更快、兼容性也更好。需要注意用 ECC 证书后后面 Nginx 配置里的证书路径可能带_ecc后缀比如/certs/example.com_ecc/fullchain.cer别搞错了。6. 让整个方案更省心的几个细节优化主流程跑通之后还有几个可以提高幸福感的小优化值得顺手做掉。一个是在 acme.sh 的配置里加一个--days参数默认阈值是 60 天。如果你想早点替换证书、更激进一些可以设置成--days 30意味着只要证书剩余时间少于 30 天就会续签。不过默认情况已经够用了不建议改得太激进因为 Let’s Encrypt 对证书签发频率是有速率限制的同一域名每周只能签发五次设置得太激进容易被限流。另一个优化是合理利用 staging 环境。Let’s Encrypt 提供了 staging 服务器可以用来测试整个流程而不受签发频率限制最重要的是不会真的生成一个可以被公网信任的证书避免测试失误导致的警告。调试 HTTPS 配置时先切到 staging确认没问题再切回正式环境这个习惯我在实操中受益匪浅特别是大规模迁移证书时。还有一个小技巧是在 acme.sh 的数据目录里加一个.curlrc文件配置好代理或者超时参数。国内网络访问 Let’s Encrypt 服务偶尔会出现超时设置一个合理的超时时间可以让 acme.sh 快速失败并重试而不是无限等待。最后给各位一个非常重要的建议把所有配置文件纳入 git 管理。树莓派上有多个服务和容器配置文件版本化之后你不但能回溯每次改动还能快速恢复到上一个可用状态。我用 gitea 自建了一个私有仓库所有/opt/docker下的配置文件都推上去。实践下来的感受是虽然前期会多花几分钟敲 git 命令但出了问题之后省下的排查时间远远超过投入。到现在为止树莓派上的 HTTPS 证书自动化已经完整跑通了。从手动申请几个月续一次变成每天自动检测、到期前自动续签、续签后自动重载 Nginx这套体系现在运行了将近半年中间经历了一次完整的自动续签周期确实没有人工干预过。我在实际部署中的体会是容器化的好处在证书管理这个场景体现得格外明显。因为 acme.sh 不像普通的 Web 服务那样经常有人去操作它它更像是后台的一个守护进程一旦配错就无人察觉。把它容器化之后至少能够通过 Docker 的日志、容器状态这些标准手段去观察和诊断而不需要在一台裸机上寻找那些散落各处的配置文件和计划任务。如果你也是用树莓派搭服务建议按照这个方案把 HTTPS 一步到位不然等到 90 天的有效期满时再想起来那手忙脚乱的滋味可不好受。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →