Skip to content

树莓派 Docker 化网页服务器(5):acme.sh 容器化 —— HTTPS 证书的自动续签

前言

本专栏第4篇中,我们完成了 ddns-go 的容器化部署,pi-ddns.jiejinx.cn 已能正确解析到树莓派的公网 IPv6 地址。本篇我们继续推进,加入 HTTPS 证书服务 —— acme.sh

旧专栏第6篇 中,我们安装 acme.sh、配置阿里云 DNS API、签发泛域名证书、挂载到 Nginx,一步步完成了 HTTPS 证书的自动申请与续签。那是树莓派拥有可信证书的开端。

这一篇,我们做同样的事 —— 让 pi-ddns.jiejinx.cn 拥有受浏览器信任的 HTTPS 证书 —— 但换一种方式:直接以容器化部署 acme.sh,跳过下载脚本、配置环境变量、手动挂载证书这些步骤。拉取镜像 → 创建共享卷 → 签发证书 → 挂载到 Nginx,四步到位。

acme.sh 容器化之后,后续的 frpc 也会遵循同样的模式。每个组件都以容器形态加入,最终汇聚到一个 Compose 文件中。

一、原理

1. acme.sh 的作用

acme.sh 是一个纯 Shell 编写的 ACME 协议客户端,用于从 Let's Encrypt(或其他 ACME CA)免费申请和自动续签 SSL/TLS 证书。它的工作流程是:

  1. 通过 DNS API 验证域名所有权
  2. 向 Let's Encrypt 提交证书签名请求
  3. 下载证书文件(完整证书链 + 私钥)
  4. 在证书到期前自动续签

简单说:你不需要手动申请证书、上传文件、记住续签日期,acme.sh 保证证书永不过期。

2. 容器化前后对比

维度裸机运行Docker 容器化
安装方式下载 shell 脚本执行docker pull
配置位置~/.acme.sh//root/.acme.sh/~/docker/acme.sh/ 统一管理
API 凭证写入 account.conf环境变量传入
自启管理crontab 定时任务restart: unless-stopped + daemon 模式
升级方式acme.sh --upgradedocker compose pull && up
证书共享手动复制或软链到 Nginx 目录Docker volume 自动挂载
卸载残留需手动清理脚本和配置删容器 + 删 volume,干干净净

3. 为什么必须用 DNS API 模式

Let's Encrypt 提供两种域名验证方式:

  • HTTP-01:在 http://<domain>/.well-known/acme-challenge/ 放置 token 文件,CA 通过 HTTP 访问验证。要求服务器开放 80 端口且公网可达。
  • DNS-01:在域名的 _acme-challenge TXT 记录中写入 token,CA 通过 DNS 查询验证。只需 DNS 服务商的 API 凭证,不依赖任何端口。

家用宽带封禁 80/443 端口,HTTP-01 模式无法工作。因此只能使用 DNS-01 模式,通过阿里云 DNS API 验证域名所有权。

4. 关于端口选择的说明

家用宽带通常封禁 80 和 443 端口,外部 HTTPS 请求无法通过标准端口到达树莓派。因此本篇沿用旧专栏的方案,将 HTTPS 服务部署在 9133 端口

访问方式变为 https://pi-ddns.jiejinx.cn:9133,而非默认的 https://pi-ddns.jiejinx.cn。这对 acme.sh 的工作没有影响 —— 它只关心域名所有权验证(通过 DNS API),端口是 Nginx 的职责。

后续 frpc 容器化后,可通过 frp 隧道将公网服务器的 443 端口流量转发到树莓派的 9133 端口,届时在第七篇的整合 Compose 中统一处理。

5. Docker volume 机制说明

Docker volumeDocker 管理持久化数据的一种方式。与 bind mount(如 ~/docker/nginx/conf:/etc/nginx/conf.d)不同,volume 的生命周期由 Docker 自身管理,不依赖宿主机上的特定目录路径。

docker volume create certs 这条命令做了三件事:

  1. 在 Docker 的数据目录(默认 /var/lib/docker/volumes/)下创建一块独立的存储空间
  2. 给它命名为 certs,后续容器通过这个名字引用它
  3. 初始化空的 _data 目录,等待容器写入数据

多个容器可以挂载同一个 named volume,实现数据共享。在本篇中:

  • acme.sh 容器将 certs 挂载到 /acme.sh,证书写入该目录
  • Nginx 容器将 certs 挂载到 /etc/nginx/certs,从该目录读取证书

两个容器各自只关心自己的挂载点路径,不需要知道对方的存在,也不需要知道证书在宿主机上的具体位置。

6. 两步走的设计思路

本篇分两步完成 acme.sh 容器化,不是重复,而是分工不同:

第一步:一次性容器签发证书

bash
docker run --rm ... --issue --dns dns_ali -d jiejinx.cn -d '*.jiejinx.cn'
  • --rm 容器执行完自动删除
  • 任务是首次向 Let's Encrypt 申请证书,涉及 DNS 验证、域名所有权确认、CA 交互
  • 证书签发成功后容器退出,使命完成

第二步:常驻容器自动续签

bash
docker run -d ... daemon
  • -d 后台长期运行
  • 任务是每天检查证书有效期,到期前自动续签
  • 不重新走 DNS 验证流程,只在需要续签时才调用 API

为什么不能合二为一?

acme.shdaemon 模式只会检查已有证书的有效期,不会主动发起首次签发。如果直接用 daemon 模式启动一个没有证书的环境,它会一直空转,永远不会去申请第一张证书。

因此标准流程就是:先一次性签发拿到第一张证书,再用 daemon 容器长期守护续签。 两个步骤缺一不可。

二、操作步骤

1. 建目录

SSH 登录树莓派,创建 acme.sh 的专用目录:

bash
mkdir -p ~/docker/acme.sh
cd ~/docker/acme.sh

这个目录将存放 Compose 文件和环境变量文件。后续所有操作都在此目录下进行。

2. 创建共享 volume

创建一个名为 certsDocker volumeacme.shNginx 都将挂载它:

bash
docker volume create certs

3. 拉取镜像

acme.sh 官方镜像支持多架构,树莓派 4B(arm64) 会自动拉取对应版本:

bash
docker pull neilpang/acme.sh:latest

利用第3篇配置的阿里云镜像加速器,拉取过程通常在几秒到半分钟内完成。

若拉取过程中断,Docker 会自动处理不完整的层,再次执行 docker pull 即可继续下载,无需额外清理。

4. 首次申请证书

运行一个一次性容器来签发证书。使用阿里云 DNS API 模式,签发主域名 jiejinx.cn 和泛域名 *.jiejinx.cn

bash
docker run --rm \
  -v certs:/acme.sh \
  -e Ali_Key='你的阿里云AccessKey ID' \
  -e Ali_Secret='你的阿里云AccessKey Secret' \
  neilpang/acme.sh:latest \
  --issue --server letsencrypt --dns dns_ali -d jiejinx.cn -d '*.jiejinx.cn'

参数说明:

参数作用
--rm容器执行完毕后自动删除
-v certs:/acme.sh将证书输出到共享 volume certs,挂载到容器内的 /acme.sh 目录
-e Ali_Key / Ali_Secret传入阿里云 API 凭证
--server letsencrypt显式指定 Let's Encrypt CA,避免 ZeroSSL 的 EAB 问题
--dns dns_ali使用阿里云 DNS API 验证
-d jiejinx.cn -d '*.jiejinx.cn'签发主域名和泛域名

执行成功后,日志末尾会显示证书文件的存放路径:

Your cert is in: /acme.sh/jiejinx.cn_ecc/jiejinx.cn.cer
Your cert key is in: /acme.sh/jiejinx.cn_ecc/jiejinx.cn.key
The intermediate CA cert is in: /acme.sh/jiejinx.cn_ecc/ca.cer
And the full-chain cert is in: /acme.sh/jiejinx.cn_ecc/fullchain.cer

关于自检过程中的 libcurl 错误acme.sh 在验证 DNS 记录时会通过公共 DNS 服务器回查 TXT 记录是否正确添加。家庭宽带环境下,可能出现 libcurl error 35/28 提示,这是 DNS 查询偶发的 SSL 握手超时,acme.sh 会自动重试,不影响签发。旧专栏对此有详细说明,此处不再赘述。

5. 验证证书文件

借助一个极小的 alpine 容器,查看 certs volume 中的证书文件:

bash
docker run --rm -v certs:/acme.sh alpine ls -la /acme.sh/jiejinx.cn_ecc/

输出示例:

total 40
drwxr-xr-x    2 root     root          4096 Sep  1 12:17 .
drwx---rwx    4 1000     1000          4096 Sep  1 12:15 ..
-rw-r--r--    1 root     root          3523 Sep  1 12:17 ca.cer
-rw-r--r--    1 root     root          4871 Sep  1 12:17 fullchain.cer
-rw-r--r--    1 root     root          1324 Sep  1 12:17 jiejinx.cn.cer
-rw-------    1 root     root           675 Sep  1 12:17 jiejinx.cn.conf
-rw-r--r--    1 root     root           528 Sep  1 12:15 jiejinx.cn.csr
-rw-r--r--    1 root     root           258 Sep  1 12:15 jiejinx.cn.csr.conf
-rw-------    1 root     root           260 Sep  1 12:15 jiejinx.cn.key

四个关键文件均已就位:

  • fullchain.cer —— 完整证书链(Nginx 使用)
  • jiejinx.cn.key —— 私钥(Nginx 使用)
  • ca.cer —— CA 中间证书
  • jiejinx.cn.cer —— 域名证书

alpine 只是一个临时查看工具,确认文件齐全后将其删除:

bash
docker rmi alpine

6. 启动常驻续签容器

首次签发是一次性操作,证书到期后还需要自动续签。为此,启动一个常驻容器,它以 daemon 模式运行,每天检查证书有效期:

bash
docker run -d \
  --name rpi-acme-sh \
  --restart unless-stopped \
  -v certs:/acme.sh \
  -e Ali_Key='你的阿里云AccessKey ID' \
  -e Ali_Secret='你的阿里云AccessKey Secret' \
  neilpang/acme.sh:latest \
  daemon

查看启动日志,确认 supercronic 定时任务已就绪:

bash
docker logs --tail 5 rpi-acme-sh

输出示例:

Running Supercronic using crontab at /acme.sh/crontab
time="2026-09-01T12:29:58Z" level=info msg="reaping dead processes"
time="2026-09-01T12:29:58Z" level=info msg="read crontab: /acme.sh/crontab"

7. 修改 Nginx 配置

acme.sh 容器已将证书写入 certs volume,现在需要让 Nginx 容器也挂载这个 volume,并配置 HTTPS 站点。

① 停掉旧容器

第3篇中 Nginx 使用 docker run 启动,也提供了Compose 启动方式,若尚未 Compose 化,则先停掉旧容器:

bash
docker stop rpi-nginx && docker rm rpi-nginx

如果你的 Nginx 已做过 Compose 化,则直接在 docker-compose.yml 中修改即可,无需此步骤。

② 创建 docker-compose.yml

bash
cd ~/docker/nginx
nano docker-compose.yml

粘贴以下内容:

yaml
services:
  nginx:
    image: arm64v8/nginx:stable
    container_name: rpi-nginx
    restart: always
    ports:
      - "9133:9133"
    volumes:
      - ./conf:/etc/nginx/conf.d:ro
      - ./html:/usr/share/nginx/html:ro
      - ./logs:/var/log/nginx
      - certs:/etc/nginx/certs:ro

volumes:
  certs:
    external: true

保存退出(Ctrl+O→ Enter→ Ctrl+X)。

相比第三篇的改动:

  • ports"80:80" "443:443" 改为 "9133:9133",因家宽封禁标准端口
  • 新增 certs:/etc/nginx/certs:ro,挂载共享 volume,只读模式
  • 顶层新增 volumes: certs: external: true,引用已创建的 certs volume
  • 删除了旧的 ./ssl:/etc/nginx/ssl:ro 挂载(裸机时代的证书路径)

③ 新建站点配置文件

bash
nano ~/docker/nginx/conf/pi-ddns.jiejinx.cn.conf

粘贴以下内容:

nginx
server {
    listen 9133 ssl http2;
    listen [::]:9133 ssl http2;
    server_name pi-ddns.jiejinx.cn;

    ssl_certificate     /etc/nginx/certs/jiejinx.cn_ecc/fullchain.cer;
    ssl_certificate_key /etc/nginx/certs/jiejinx.cn_ecc/jiejinx.cn.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

保存退出(Ctrl+O→ Enter→ Ctrl+X)。

配置要点:

  • 监听 9133 端口,启用 SSL 和 HTTP/2
  • server_name 设为 pi-ddns.jiejinx.cn,精确匹配
  • 证书路径指向 certs volume 中的 fullchain.cerjiejinx.cn.key
  • 注意文件名是 .cer.key,不是旧专栏的 .pem

④ 启动新容器

bash
docker compose up -d

8. 验证 HTTPS 访问

在树莓派本机或同一局域网的电脑上执行:

bash
curl -I https://pi-ddns.jiejinx.cn:9133

应返回 HTTP/2 200

HTTP/2 200
server: nginx/1.30.4
date: Tue, 01 Sep 2026 12:47:44 GMT
content-type: text/html
content-length: 741
last-modified: Fri, 21 Aug 2026 09:26:16 GMT
etag: "6a8819b8-2e5"
accept-ranges: bytes

也可以在浏览器中直接访问 https://pi-ddns.jiejinx.cn:9133,应显示绿色锁标志,证书信息为 Let's Encrypt,域名匹配 pi-ddns.jiejinx.cn

9. acme.sh 改用 Compose 管理

acme.sh 也纳入 Compose 管理体系,与 Nginx 统一管理。

① 创建 docker-compose.yml

bash
cd ~/docker/acme.sh
nano docker-compose.yml

粘贴以下内容:

yaml
services:
  acme-sh:
    image: neilpang/acme.sh:latest
    container_name: rpi-acme-sh
    restart: unless-stopped
    command: daemon
    volumes:
      - certs:/acme.sh
    environment:
      - Ali_Key=${Ali_Key}
      - Ali_Secret=${Ali_Secret}

volumes:
  certs:
    external: true

保存退出(Ctrl+O→ Enter→ Ctrl+X)。

② 创建 .env 文件

~/docker/acme.sh/ 目录下创建 .env 文件,存放阿里云 API 凭证(敏感信息,不要提交到 Git):

bash
nano .env

内容:

Ali_Key=你的阿里云AccessKey ID
Ali_Secret=你的阿里云AccessKey Secret

保存退出(Ctrl+O→ Enter→ Ctrl+X)。

③ 切换至 Compose 管理

停掉当前容器,改用 Compose 启动:

bash
docker stop rpi-acme-sh && docker rm rpi-acme-sh
docker compose up -d

确认运行状态:

bash
docker compose ps

输出示例:

NAME          IMAGE                     COMMAND              SERVICE   CREATED         STATUS         PORTS
rpi-acme-sh   neilpang/acme.sh:latest   "/entry.sh daemon"   acme-sh   6 seconds ago   Up 6 seconds

查看启动日志,确认 supercronic 定时任务已就绪:

bash
docker logs --tail 5 rpi-acme-sh

输出示例:

Running Supercronic using crontab at /acme.sh/crontab
time="2026-09-01T12:29:58Z" level=info msg="reaping dead processes"
time="2026-09-01T12:29:58Z" level=info msg="read crontab: /acme.sh/crontab"

至此,acme.sh 容器化部署完成。证书已签发、Nginx 已加载、续签守护已运行,全部纳入 Compose 管理。

三、总结

1. 新旧做法对比

维度旧专栏(裸机)本专栏(容器化)
安装下载 shell 脚本执行docker pull
配置编辑 account.conf 写入 AK/SK环境变量传入
证书签发acme.sh --issue ...一次性容器执行相同命令
自启续签crontab 定时任务daemon 模式 + restart: unless-stopped
证书共享手动复制到 Nginx 目录Docker volume 自动挂载
升级acme.sh --upgradedocker compose pull && up
卸载手动清理脚本、配置、crontab删容器 + 删 volume

下篇预告

HTTPS 证书已经容器化,但还有一个组件正在路上 —— frpc。目前树莓派的家宽环境无法直接通过 80/443 端口对外提供服务,需要借助 frp 隧道穿透。

下一篇,我们将把 frpc 迁进容器,让内网穿透也纳入 Docker 管理体系。

届时,Nginx + ddns-go + acme.sh + frpc 四个核心服务全部容器化,离第七篇的终极目标 —— 一个 Compose 文件启动全家桶 —— 又近了一步。