今天,我的博客突然毫无征兆地无法访问了,浏览器直白地吐出了一个 502 Bad Gateway。
作为一个正在摸索和学习服务器运维与 Docker 配置的开发者,遇到这种服务挂掉的情况确实有些手忙脚乱,但每一次“踩坑”其实都是极佳的学习机会。这次排错的过程,以及最终的解决方案,让我对 Docker 容器网络和反向代理的底层逻辑有了更清晰的认识。
为了总结经验,同时也为了梳理和巩固排错思路,我决定把今天的排查全过程以及架构优化记录下来。在这篇文章中,我将还原这起“事故”的诊断过程,并聊聊为什么我最终选择弃用旧的方案,转而全面拥抱 Docker 容器局域网架构。
1. 现场还原:发现博客无法访问
当时打开网页转了半天圈,最后返回 502 Bad Gateway。我的第一反应是:博客容器挂了?
立刻 SSH 登录云服务器,敲下最常用的查看指令:
sudo docker ps -a
然而,输出结果却让我有些意外:
- 博客容器
standalone-halo状态是Up X hours,处于正常运行状态。 - 网关容器
global_caddy也是Up状态。 - 宿主机内存、CPU 均有富余,没有发生 OOM(内存溢出)导致的闪退。
既然博客服务本身没死,网关也活着,那为什么流量进不去?
2. 深入日志:定位 502 的蛛丝马迹
既然容器都在运行,那必然是“中间的网路断了”。我决定看一看网关 Caddy 的日志,看看它是怎么转发流量的:
sudo docker logs -f --tail 50 global_caddy
在滚动的日志中,我抓到了下面这行报错:
{"level":"error","ts":...,"logger":"http.log.error","msg":"dial tcp: lookup host.docker.internal on 127.0.0.11:53: no such host","request":{"host":"blog.ownd.cc", ...}}
lookup host.docker.internal on 127.0.0.11:53: no such host —— 这行报错透露了关键信息。
127.0.0.11:53是 Docker 容器内部默认的 DNS 解析服务器。- Caddy 试图将博客的域名请求转发给
host.docker.internal:8090,但是 Docker 的 DNS 解析服务表示:找不到host.docker.internal这个主机。
DNS 解析失败,导致网关无法把流量送达博客服务,最终向浏览器返回了 502。
3. 问题根源:沙箱隔离与临时的“跳板”
为什么 Caddy 会去解析 host.docker.internal?这得从我之前的部署架构说起。
之前为了省事,我的博客是用独立的 docker-compose.yaml 启动的,它运行在 Docker 默认的 Bridge 网络中,并且把端口映射到了宿主机:
ports:
- '8090:8090' # 将容器的 8090 映射到宿主机的 8090
而网关 Caddy 运行在另一个网络中。因为两个容器处于不同的虚拟网络(网络隔离),Caddy 无法直接通过容器名访问到博客。
为了让 Caddy 能够“跨网络”找到博客,我当时配置了 host.docker.internal,试图让 Caddy “跳出沙箱”,先访问宿主机的网关 IP,再通过宿主机映射的 8090 端口绕回博客容器。
但这种方案存在两个致命的缺陷:
- 可靠性极差:在 Linux 环境下,
host.docker.internal的解析需要依赖 Caddy 容器配置extra_hosts强行映射。一旦 Docker 守护进程重启、网卡配置变动或 DNS 缓存失效,这个映射就会断开,导致 502。 - 严重的安全性隐患:为了让 Caddy 通过宿主机绕回来,我必须在宿主机上暴露
8090端口。这意味着任何人都可以通过http://服务器公网IP:8090绕过 Caddy 网关,直接裸奔访问我的博客后台。
4. 架构重构:全面拥抱容器局域网
找到了根源,解决思路就很清晰了:不应该让流量在宿主机绕圈子,而应该让 Caddy 和博客直接在 Docker 内部局域网进行安全互联。
我决定对博客的网络架构进行重构。
第一步:关闭外网端口,加入统一共享网络
我修改了博客的 docker-compose.yaml。
首先,删除了 ports: - '8090:8090',改为仅在容器网内暴露的 expose: - '8090'。
然后,在 Compose 文件末尾声明,将博客服务加入 Caddy 所在的外部共享虚拟网络 ownd-api_default:
services:
halo:
image: halohub/halo:2.24
container_name: standalone-halo
expose:
- '8090' # 仅在 Docker 内网暴露,外网无法直接探测
# ... 其他配置 ...
networks:
default:
name: ownd-api_default # 接入统一的共享网桥
external: true # 声明是外部已存在的网络
第二步:修改网关 Caddyfile 路由
既然大家都在同一个虚拟局域网了,Caddy 就可以直接通过博客的容器名进行内网通信,不再需要任何 host.docker.internal 的网关桥接:
# Caddyfile
blog.ownd.cc {
reverse_proxy standalone-halo:8090 # 直接使用容器名加内部端口
}
第三步:内网“借道测试”验证
在服务器上应用新配置并重启容器后,我没有直接去浏览器刷新,而是采用了一个更稳妥的调试手法——跳入网关容器内部直接 curl 博客服务:
sudo docker exec -it global_caddy curl -I http://standalone-halo:8090
输出结果令人振奋:
HTTP/1.1 200 OK
content-type: text/html;charset=UTF-8
...
这表明,在 Docker 内部局域网中,网关 Caddy 已经可以通过容器名完美地解析并连接到博客服务。外部浏览器再次刷新,博客也瞬间恢复了访问,且响应速度有了明显的提升。
5. 总结与技术收获
经历这次 502 事故和架构重构,我梳理出了自建微服务时关于 Docker 网络的核心收获:
- 物理隔离,逻辑互通:在管理服务时,我们在磁盘物理目录上应该保持高内聚低耦合(博客、主站、网关各自拥有独立的 Compose 项目和数据卷);但在网络层面上,应该通过
external虚拟网桥将它们逻辑上连接在一起,交给统一的网关进行反向代理。 - 安全第一:除了网关(如监听 80/443 的 Caddy/Nginx),其他业务容器(包括数据库、API 后端)绝对不要在 Compose 中使用
ports映射端口到宿主机。只用expose保证内网互通,能挡掉互联网上 99% 的恶意端口扫描。 - 善用内网工具诊断:当遇到网络问题时,尽量把诊断链路缩短。使用
docker exec进入网关容器,使用curl -I http://<容器名>:<内网端口>进行直接测试,能帮你瞬间排查出是 DNS 解析问题、容器网络未加通,还是业务程序自身崩溃。