今天踩了一个坑,起因是博客突然无法访问了,浏览器报错 526 Invalid SSL certificate。从这次排查中,我对容器挂载、Let’s Encrypt 的证书目录结构、Cloudflare 的 TLS 模式都有了更深入的理解。
背景
我用 Podman 启动了一个 nginx 容器,承载个人博客,通过 Let’s Encrypt 签发证书,并挂在 Cloudflare 后面,使用了 Full (Strict) 模式。这意味着:
不仅客户端到 Cloudflare 要走 TLS,Cloudflare 到源站也必须使用一个有效的 CA 签发的证书。 一旦证书失效,Cloudflare 直接拒绝中转,返回 526 错误。
问题出现:Cloudflare 返回 526
我的第一个排查点是 nginx 配置,确认并无问题。接着查看容器日志,发现 podman logs 输出太长,尝试 --tail 100 却无效(原因是容器已经崩溃退出)。
索性我直接删了容器,更新宿主机证书,再重建:
podman run -d \\
--name nginx-blog \\
-p 80:80 -p 443:443 \\
-v /home/action/web_service/nginx.conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \\
-v /etc/letsencrypt/live/bloginfo.blog:/etc/letsencrypt/live:ro,z \\
-v /etc/letsencrypt/archive/bloginfo.blog:/etc/letsencrypt/archive/bloginfo.blog:ro,z \\
-v /home/action/web_service/blog/:/usr/share/nginx/html:ro \\
docker.io/library/nginx:latest然而容器启动瞬间退出,再次查看日志:
[emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/fullchain.pem": BIO_new_file() failed这说明 nginx 无法读取证书文件 fullchain.pem,而不是配置错误。
初步排查:文件确实存在
我回到宿主机,查看 /etc/letsencrypt/live,证书文件一切正常:
lrwxrwxrwx 1 root root 42 fullchain.pem -> ../../archive/bloginfo.blog/fullchain3.pem这是 Let’s Encrypt 的常规做法 —— live/ 下是软链接,实际文件放在 archive/ 中。
于是我怀疑是容器对软链接解析失败,但由于 nginx 容器无法 exec 进入,我新建了一个 Alpine 容器来测试:
podman run --rm -it \\
-v /etc/letsencrypt/live/bloginfo.blog:/etc/letsencrypt/live:ro,z \\
docker.io/library/alpine:latest sh进入容器后
/ # ls -l /etc/letsencrypt/live
fullchain.pem -> ../../archive/bloginfo.blog/fullchain3.pem路径看起来也没问题。但是一旦用 openssl 打开它
/ # openssl x509 -in /etc/letsencrypt/live/fullchain.pem -noout -text
Could not open file... No such file or directory证据确凿:容器中软链接的目标文件不存在。
根因分析:软链接无法解析
仔细思考后发现问题关键在于:
容器中 fullchain.pem 虽然存在,但它是一个相对路径软链接,指向容器内并不存在的文件。
Let’s Encrypt 的证书结构如下:
/etc/letsencrypt/
├── archive/
│ └── bloginfo.blog/
│ ├── fullchain3.pem
├── live/
│ └── bloginfo.blog/
│ └── fullchain.pem -> ../../archive/bloginfo.blog/fullchain3.pem这意味着如果只挂载 live/ 而没有 archive/,容器中的软链接就是“断链”的,打不开。
所以正确方式要么是挂上 archive/,要么干脆复制实际证书文件:
mkdir -p /opt/certs/bloginfo.blog
cp /etc/letsencrypt/live/bloginfo.blog/*.pem /opt/certs/bloginfo.blog/然后挂载 /opt/certs/bloginfo.blog 到容器中。
补充问题:dhparams.pem 也缺失
重启 nginx 后,又遇到了这个报错:
[emerg] 1#1: BIO_new_file("/etc/letsencrypt/live/dhparams.pem") failed原来我在 nginx.conf 中配置了:
ssl_dhparam /etc/letsencrypt/live/dhparams.pem;但这个文件并不是 Let’s Encrypt 默认生成的,而是需要我手动通过如下命令生成:
openssl dhparam -out /etc/letsencrypt/live/dhparams.pem 2048生成后重启 nginx,终于一切恢复正常。
最终反思
这次事件暴露出以下几个方面的问题:
-
不了解 Let’s Encrypt 的证书结构
- 忽略了软链接指向 archive 的逻辑
-
容器挂载路径不完整
live/的软链接在容器内失效,除非同时挂载 archive,或复制实际文件
-
nginx.conf 硬编码依赖未准备的文件
- 如 dhparams.pem 需要显式生成,不然容器无法启动
-
缺乏对容器运行失败的诊断手段
- 忽略
podman logs的正确使用,未设置合理的日志查看与监控机制
- 忽略
Checklist:证书部署到容器的注意事项
- 宿主机证书更新后是否同步到容器?
- 软链接是否在容器中可达?
- nginx 是否依赖 dhparams.pem 等额外文件?
- Cloudflare TLS 模式是否匹配服务端证书?
- 容器重启失败是否能第一时间定位问题?
一次小错误,一次大教训。记录下来,防止未来再掉坑。