续期日志显示成功,宿主机上的证书也换了,容器里的文件却还是旧的。这时先别连续reload:服务可能根本没读到你刚替换的那个文件。这里拆开三个对象:宿主机路径、容器内路径、服务加载的证书。
本文聚焦Linux Docker Engine绑定挂载。实验用普通文本占位,不生成或挂载真实私钥,不申请证书;它只验证文件可见性,不代表Nginx、TLS或生产续期已通过。
一、先确认"旧"在哪一层
记录叶证书序列号、有效期和SHA-256指纹;文件哈希还受PEM编码与链排列影响,不能混用。

图为三个独立检查对象,不是自动更新流程。目录挂载也不保证服务重新加载,更不保证证书链和名称验证通过。图片由AI辅助生成。
| 观察 | 下一步 |
|---|---|
| 宿主机新、容器旧 | 查挂载源、替换方式及软链接 |
| 容器新、连接旧 | 查服务加载、入口和连接复用 |
| 文件打不开 | 查路径、链接目标、权限和安全策略 |
二、查看运行中的挂载,不只看配置文件
把CONTAINER替换成目标容器名或ID。先读实际Mounts的Type、Source、Destination、RW,再确认服务配置真正引用哪个容器路径。下面是只读排查模板,不要把私钥内容贴进工单。
bash
docker inspect --format '{{json .Mounts}}' CONTAINER
# 只读取公开证书,不打印私钥
docker exec CONTAINER cat /certs/fullchain.pem
绑定挂载源属于daemon主机,不是远程CLI所在电脑。修改Compose文件不证明运行中挂载已变更;挂载还会遮住镜像该路径下的原有内容。
三、原地写入与替换文件不是同一种更新
原地改写已有文件内容,和创建新文件再rename覆盖旧路径,行为不同。Linux单文件绑定挂载关联已有文件对象,不是每次读取都回宿主机按原路径重新找一遍。
因此,旧文件被新文件替换后,容器内的单文件挂载可能仍指向旧对象;稳定父目录挂载下,重新打开目录里的文件名则可以找到新对象。这里限定父目录本身未被替换,不能推成"目录挂载永远自动同步"。
别为了绕开挂载问题改成原地覆盖真实私钥。截断再写入有读到半份内容的窗口,两份材料也可能不配套;可见性修好不等于更新事务已经安全。
四、用隔离容器复现,不碰业务证书
在本机Linux测试daemon上运行,需Docker权限、Bash及含sh/cat/sleep的已审核本地镜像。IMAGE指定镜像名或ID,TMPDIR指定已有临时目录。脚本不拉镜像、不开网络,只挂自建目录并清理;不适用于远程daemon路径。
相等判断不成立即非零退出;fullchain.pem只装OLD/MID/NEW文本,不是PEM证书。
bash
set -eu
: "${IMAGE:?请指定已在本机审查过的含sh和cat的镜像}"
: "${TMPDIR:?请指定本机临时目录}"
d=$(mktemp -d "$TMPDIR/bind-cert.XXXXXX")
cid=
cleanup() {
[ -z "$cid" ] || docker rm -f "$cid" >/dev/null
rm -rf -- "$d"
}
trap cleanup EXIT
printf 'OLD\n' > "$d/fullchain.pem"
cid=$(docker run -d --pull=never --network none --read-only \
--cap-drop ALL --security-opt no-new-privileges \
--mount "type=bind,src=$d/fullchain.pem,dst=/single.pem,readonly" \
--mount "type=bind,src=$d,dst=/certs,readonly" \
--entrypoint sh "$IMAGE" -c 'sleep 300')
readfile() { docker exec "$cid" cat "$1"; }
[ "$(readfile /single.pem)" = OLD ]
printf 'MID\n' > "$d/fullchain.pem"
[ "$(readfile /single.pem)" = MID ]
printf 'NEW\n' > "$d/new.pem"
mv "$d/new.pem" "$d/fullchain.pem"
[ "$(cat "$d/fullchain.pem")" = NEW ]
[ "$(readfile /single.pem)" = MID ]
[ "$(readfile /certs/fullchain.pem)" = NEW ]
printf 'host=NEW single=MID directory=NEW\n'
本次在Docker Engine 26.1.3、Linux内核4.18环境实际执行:原地写MID后单文件读取为MID;rename替换后宿主机为NEW、单文件挂载仍为MID、目录内读取为NEW。没有测试Docker Desktop、远程daemon或其他存储驱动的等价行为。
五、Certbot的live路径还要检查archive
Certbot官方说明续期会更新live下的最新材料。常见布局中live内文件通过相对软链接指向archive的版本文件:只把live子目录挂进容器,链接目标可能根本不在容器可见范围内。此时是目标不可达,不应直接归因为权限或证书坏了。
应在宿主机和容器分别核对readlink结果与最终目标是否可读。仅挂载软链接文件也不等于保留可追踪的目录关系;运行时可能在建挂载时解析到当时目标。别猜更新会自动追踪。
可按服务需要保留live/archive相对关系并只读挂载必要范围,或由受控部署步骤输出独立的服务证书目录。不要为省事把全部账户材料和所有域名私钥暴露给每个容器;只读防写,不防读取外泄。
六、目录读到新文件后,服务还要单独加载
服务可能已把证书装入内存,或一直持有旧文件描述符。目录挂载解决的是重新按路径打开时的可见性,不会替应用完成热加载。Nginx官方说明HUP会尝试新配置,失败时保留旧配置;因此发送信号或命令返回不能代替后续检查。
确认容器内材料正确且配对,再按目标服务的受控流程检查配置、加载和日志,最后从正确入口建立新TLS连接,核对SNI、叶证书指纹、名称、链与业务响应。本文未执行这些生产操作。
重启或重建容器有可用性影响,不能代替根因分析;应用reload与容器重建也不是一回事。
七、排查时别扩大权限和变更范围
优先显式--mount并核对源路径存在;默认情况下源不存在会报错,而-v可能创建目录,使"文件路径"问题更难看懂。默认可写的绑定挂载会影响宿主机,证书读取端通常应明确readonly。
遇到Permission denied还要查实际UID/GID、目录遍历权限及SELinux策略,不能靠chmod 777或关闭安全机制消除表面报错。挂载传播控制的是子挂载传播,不是修复普通文件rename可见性的万能开关。
八、交付前保留这张检查清单
- 运行态Source/Destination与预期一致,知道是文件还是目录挂载。
- 已确认更新方式,并从容器内重新打开实际配置路径。
- 软链接的最终目标可达,私钥只对必要服务开放。
- 新材料、服务加载、新连接与业务结果分别留证;失败能回滚。
参考资料与实现入口: