在容器里使用 acme.sh

之前尝试了以最小系统权限运行证书签发脚本,上面用到的服务器在香港特区,所以下载和更新脚本软件都很快,但换用内地的服务器就无法更新软件。好在有 Docker Hub 镜像网站的存在,直接拉取容器镜像部署就完事了,顺便就探索了下用容器化的办法来安装 acme.sh,并且更新证书到容器的宿主机上的方法。

主要的问题

问题的起因是 GitHub 在内地连接不通畅导致 acme.sh 的联网式安装无法进行。我想到一个简单的解决方法是做容器化部署,既能使用腾讯云的 Docker Hub 镜像加速下载,又能隔离软件和宿主系统的运行环境,一石二鸟。😎

正因为容器和宿主环境的隔离,而 Nginx 大多数情况是要在容器的宿主系统上运行,此时 acme.sh 自带的钩子程序就无法重启 Nginx 来载入新证书。如果这个问题处理不得当,就会导致证书没有真正被更新到 Nginx。我的解决办法是写一个可已检查证书是否更新的计划任务,每天自动运行一次,如果证书更新了就重启 Nginx。

我的实践

首先是启动守护程序。如果使用了 DNS 验证模式,还要在容器环境中导入 API Token(以下 --env 以 CloudFlare API 举例)

1
2
3
4
5
6
7
8
docker run --restart always -itd \
-v "$(pwd)/acme.sh":/acme.sh \
--net=host \
--name=acme.sh \
--env CF_Token="<Your CloudFlare API Token>" \
--env CF_Account_ID="<Your CloudFlare Account ID>" \
--env CF_Zone_ID="<Your CloudFlare Zone ID>" \
neilpang/acme.sh daemon

之后创建一个 acme.sh 容器指令的精简别名,使用命令申请和安装证书。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建别名
echo "alias acme.sh='docker exec acme.sh'" >> ~/.bashrc
source ~/.bashrc

# 申请证书
acme.sh --issue --dns dns_cf -d uuznn.cn -d '*.uuznn.cn' --keylength ec-256

# 创建证书安装路径,而这里相对容器中的位置是 /acme.sh
mkdir -p $(pwd)/acme.sh/out/uuznn.cn_ecc

# 安装证书到上述路径,安装成功时则创建一个 reload 文件暗示证书已经更新
acme.sh --install-cert -d uuznn.cn --ecc \
--key-file /acme.sh/out/uuznn.cn_ecc/key.pem \
--fullchain-file /acme.sh/out/uuznn.cn_ecc/fullchain.pem \
--reloadcmd "touch /acme.sh/out/reload"

最后,写一个脚本放到宿主系统,用于检查证书是否更新、证书完整性和重启 Nginx。

/path/to/acme.sh/out/reload.sh
1
2
3
4
file="/path/to/acme.sh/out/reload"
if [ -f "$file" ]; then
sudo nginx -t && sudo systemctl restart nginx.service && rm "$file"
fi

在容器的宿主机上用计划任务执行上面的脚本。使用 crontab -e 打开任务编辑器,写入:

1
33 0 * * * /path/to/acme.sh/out/reload.sh

总结

哪怕是脚本程序也用容器运行,这样不仅隔离了宿主系统和各种应用服务,降低了系统环境的复杂性,减少了耦合度,也提高了安全性。🤪