Appearance
Docker 服务去除 --net=host:从 host 网络回到 bridge 的一次改造记录
一、为什么要去掉 --net=host
现有生产环境中的 Java 服务以 Docker 容器运行。
历史上为了减少端口映射和网络配置问题,服务统一使用:
bash
--net=host启动。
这种方式确实简单。
容器直接使用宿主机网络栈,不需要额外考虑 Docker bridge、端口映射和容器地址。
但随着服务数量增加,这种“简单”开始带来两个更明显的问题:
维护上,需要让“服务到底开放了什么端口”变得显式。
安全上,需要让“服务能访问什么、谁能访问服务”保持独立边界。
这两个问题最终成为去除 --net=host 的核心动机。
二、维护层面:配置了 -p,但实际上没有作用
原来的 Docker 启动命令中,经常还能看到类似:
bash
-p 8003:8003这样的配置。
直觉上很容易理解为:
text
宿主机 8003
↓
映射到
容器 8003但只要同时使用:
bash
--net=host这个端口映射就没有实际意义。
因为容器已经直接共享宿主机网络命名空间。
此时服务如果监听:
text
0.0.0.0:8003本质上就是直接监听宿主机的:
text
8003Docker 不再负责做:
text
宿主机端口
→
容器端口这层映射。
于是就出现了一个维护上的问题:
docker run里虽然写着-p 8003:8003,但真正决定服务对外暴露什么端口的,是容器内 Java 进程实际监听了什么。
这会让部署配置出现“看起来有配置,实际上不生效”的情况。
长期维护时容易产生几个问题:
- 很难从
docker run一眼判断服务真正开放了哪些端口 - 端口暴露依赖 Spring Boot / JVM 实际监听配置
- 多个容器更容易出现宿主机端口冲突
- 防火墙、安全组与应用端口之间的关系不够直观
- 排查时难以区分“容器端口”和“宿主机端口”
这和之前排查 JAVA_OPTS 没有真正进入 JVM 的问题有一点类似:
配置写在那里,并不代表它真的参与了运行时行为。
三、安全层面:业务服务应该有独立的网络边界
另一个更重要的动机是网络隔离。
--net=host 的本质是:
容器直接共享宿主机的网络命名空间。
这意味着容器不再拥有独立的网络栈。
对于普通业务服务,这会让网络边界变得比较模糊。
例如:
text
容器中的服务
↓
直接监听宿主机端口而不是:
text
容器中的服务
↓
容器独立网络
↓
只通过明确的 -p 暴露必要端口从安全和隔离角度,更希望做到的是:
- 服务独立部署
- 服务独立监听
- 只开放真正需要的端口
- 服务之间通过明确的网络路径访问
- 容器和宿主机之间保留清晰边界
这并不意味着:
--net=host一定不安全,或者任何场景都不能使用。
某些网络工具、高性能网络服务、监控 Agent 或特殊基础设施确实可能需要 host network。
但对于普通 Java 业务服务,如果没有明确的 host network 需求,更合理的默认方式应该是:
使用独立容器网络,并通过显式端口映射控制服务暴露面。
四、真正准备删除 --net=host 时,两个隐含问题暴露出来了
一开始以为:
text
删除 --net=host
↓
增加 -p
↓
完成但实际切换到 bridge 网络后,很快暴露出两个原来被 host 网络掩盖的问题。
4.1 服务到底监听哪些端口
在 host 网络模式下,容器直接使用宿主机网络。
久而久之,很容易只记住:
text
这个服务大概是 8003却没有真正核实 Java 进程到底监听了哪些端口。
而切换到 bridge 后,每个需要跨容器或跨主机访问的端口都必须显式处理。
例如:
- 业务端口
- Actuator / 管理端口
- 其他额外监听端口
如果漏掉一个:
text
-p对应功能就会直接不可达。
4.2 Nacos 注册的地址到底是不是其他主机能访问的地址
第二个问题更关键。
Docker 默认 bridge 网络会给容器分配类似:
text
172.17.x.x的地址。
如果 Spring Cloud Nacos Discovery 自动把这个容器地址注册到 Nacos,那么调用方拿到的可能就是:
text
172.17.x.x:8003在单机内部,这个地址可能可以访问。
但在多宿主机场景里:
不能假定另一台宿主机能够直接访问当前宿主机默认 bridge 网络中的容器 IP。
所以从 host 网络切回 bridge,不只是“换一个 Docker 参数”。
还必须重新明确:
text
服务监听地址
服务暴露端口
注册中心实例地址
跨宿主机访问路径五、第一步:先确认服务真实监听的端口
在改 Docker 启动命令之前,先确认服务实际监听了哪些端口。
5.1 host 网络模式下查看
先获取容器在宿主机上的 PID:
bash
docker inspect -f '{{.State.Pid}}' 容器名称在 --net=host 模式下,容器和宿主机共享网络命名空间,可以直接通过:
bash
ss -tulnp | grep PID辅助定位服务监听端口。
5.2 bridge 网络模式下查看
切换到 bridge 后,可以进入容器自己的 network namespace:
bash
nsenter -t PID -n ss -tulnp最终确认服务需要暴露:
text
业务端口:8003
管理 / 监控端口:9997这里最重要的不是记住这两个数字,而是:
从实际监听状态确认端口,而不是继续依赖历史配置和经验。
六、第二步:让端口映射真正成为部署契约
确认端口后,在 Docker 启动命令中显式声明:
bash
-p 8003:8003
-p 9997:9997切回 bridge 后,这些配置终于真正具有明确含义:
text
宿主机 8003
↓
Docker 端口映射
↓
容器 8003以及:
text
宿主机 9997
↓
Docker 端口映射
↓
容器 9997这时候:
bash
docker run本身就开始能够表达:
这个服务到底对宿主机开放了哪些端口。
维护边界比 host network 清晰很多。
同时,还需要根据实际访问范围配置:
- 主机防火墙
- 云厂商安全组
- 必要的网络 ACL
但原则是:
只放行真正需要跨主机访问的端口。
七、切到 bridge 后,Nacos 注册出了容器地址
端口问题解决之后,第二个问题出现了。
服务注册到 Nacos 后,实例地址变成类似:
text
172.17.x.x这正是 Docker bridge 给容器分配的地址。
如果调用方也在同一宿主机,这个地址可能没有问题。
但当前环境是多台 ECS。
不同宿主机上的默认 Docker bridge 是各自主机内部的网络。
例如:
text
宿主机 A
└── docker0
└── 172.17.x.x
宿主机 B
└── docker0
└── 172.17.x.x即使地址看起来都属于:
text
172.17.0.0/16也不意味着:
这两个网段天然属于同一个可路由网络。
所以:
同网段样式,不等于网络真的互通。
在当前默认 bridge 模型下,跨宿主机不能直接把容器 172.17.x.x 当成服务注册地址使用。
八、为什么注册容器 IP 会导致跨主机调用失败
假设 Nacos 中注册的是:
text
172.17.0.3:8003另一台宿主机上的服务从 Nacos 获取实例后,会尝试:
text
调用方
↓
172.17.0.3:8003但这个:
text
172.17.0.3是目标宿主机本地 Docker bridge 里的容器地址。
对于另一台宿主机而言,它并没有一条天然成立的访问路径:
text
宿主机 B
→
宿主机 A 的 docker0
→
172.17.0.3于是服务调用就会失败。
可能看到:
text
No route to host或者其他连接失败现象。
这里需要注意:
No route to host并不一定只代表 Linux 路由表缺少路由,也可能由防火墙拒绝等原因产生。
本次场景的核心则是:
注册中心返回的实例地址本身不适合作为当前跨宿主机调用路径。
如果遇到主机防火墙导致的 No route to host,可以参考另一篇记录:
微服务访问报 No route to host:一次 firewalld 端口未放行问题排查
九、重新设计调用路径:注册宿主机 VPC IP
当前部署环境中,两台 ECS 的宿主机 VPC 内网地址之间可以互通。
因此最终选择:
Nacos 不注册容器 bridge IP,而是注册宿主机 VPC IP。
例如目标宿主机内网地址:
text
172.16.0.41Docker 显式映射:
bash
-p 8003:8003那么真正的服务调用路径就变成:
text
服务 A
↓
从 Nacos 获取实例
172.16.0.41:8003
↓
访问目标宿主机 VPC IP
172.16.0.41:8003
↓
Docker -p
↓
目标容器
8003这个路径和实际部署模型是一致的。
十、让 Nacos 注册宿主机内网 IP
服务启动时显式指定:
bash
-e SPRING_CLOUD_NACOS_DISCOVERY_IP=172.16.0.41也可以通过 Spring 配置:
yaml
spring:
cloud:
nacos:
discovery:
ip: 172.16.0.41这样 Nacos 中的服务实例就会显示:
text
172.16.0.41:8003而不是:
text
172.17.x.x:8003从另一台宿主机访问:
bash
curl http://172.16.0.41:8003/actuator/health可以正常到达目标容器。
十一、用 metadata 标记实例所属主机
原有启动参数中还额外加入了:
bash
--spring.cloud.nacos.discovery.metadata.host_public_ip=公网IP
--spring.cloud.nacos.discovery.metadata.host_private_ip=内网IP这两个字段是主动添加的自定义 metadata。
它们的目的不是参与服务路由,而是方便在 Nacos 中识别:
当前这个服务实例实际运行在哪台 ECS 上。
例如在多台宿主机部署相同服务时,仅看:
text
实例 IP
端口有时并不方便快速对应到具体机器。
因此额外记录:
text
host_public_ip
host_private_ip可以帮助排查和运维时快速确认实例所属主机。
在当前项目中,这些 metadata 仅作为附加描述信息使用,并不参与 Feign / Ribbon / LoadBalancer 的实例地址选择。
真正用于服务调用的仍然是 Nacos 实例本身注册的:
text
实例 IP
+
实例端口因此:
text
host_private_ip=172.16.0.41和:
text
spring.cloud.nacos.discovery.ip=172.16.0.41作用完全不同。
前者解决的是:
这个实例实际运行在哪台机器上。
后者解决的是:
其他服务应该通过哪个 IP 访问这个实例。
也就是说,这两个 metadata 应该继续保留。
它们解决的是“实例识别”问题,而不是“服务路由”问题。
十二、最终 Docker 启动方式
调整后的启动方式类似:
bash
docker run -d \
--name st_remit_weibo_prod \
-p 8003:8003 \
-p 9997:9997 \
-e JAVA_TOOL_OPTIONS='-Dserver.port=8003 -Xms2048m -Xmx4096m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/logs/xxx/heapdump-%p-%t.hprof' \
-e SPRING_CLOUD_NACOS_DISCOVERY_IP=172.16.0.41 \
<image> \
--spring.profiles.active=prod \
--spring.cloud.nacos.discovery.metadata.host_public_ip=公网IP \
--spring.cloud.nacos.discovery.metadata.host_private_ip=内网IP和原来相比,最重要的变化不是单纯删除:
text
--net=host而是把几个原本隐含的运行事实显式化了:
text
服务监听什么端口
↓
-p 明确声明
跨主机通过什么地址访问
↓
宿主机 VPC IP
Nacos 应该注册什么
↓
VPC IP + 服务端口十三、怎么验证这次改造真的成立
13.1 验证容器监听
进入容器 network namespace:
bash
nsenter -t PID -n ss -tulnp确认:
text
8003
9997确实处于监听状态。
13.2 验证 Docker 端口映射
检查:
bash
docker port st_remit_weibo_prod确认能够看到类似:
text
8003/tcp -> 0.0.0.0:8003
9997/tcp -> 0.0.0.0:999713.3 验证 Nacos
在 Nacos 控制台确认实例:
text
172.16.0.41:8003而不是:
text
172.17.x.x:800313.4 从另一台宿主机验证
执行:
bash
curl http://172.16.0.41:8003/actuator/health能够正常返回。
13.5 验证服务调用
确认 Feign 等服务调用恢复正常,没有因为 bridge 网络切换出现跨节点访问失败。
十四、这次改造真正解决了什么
如果只从表面看,这次只是:
text
删除 --net=host但真正解决的是两个长期隐藏的问题。
14.1 维护:端口开始变成显式配置
原来:
text
-p 8003:8003虽然写着,但因为 --net=host 并没有真正承担端口映射职责。
服务真正开放什么端口,只能去看 Java 进程。
切回 bridge 后:
text
-p 8003:8003
-p 9997:9997开始真正成为部署契约。
部署配置、应用监听、防火墙、安全组之间的关系也更清晰。
14.2 安全:服务重新拥有独立网络边界
原来容器直接共享宿主机网络:
text
容器
=
宿主机网络切回 bridge 后:
text
容器独立网络
↓
只通过明确端口映射对外提供服务这让:
text
服务内部监听和:
text
宿主机实际暴露重新变成两个可以分别控制的层次。
对于普通业务服务,这种边界更符合最小暴露原则,也更容易做后续网络策略收敛。
十五、这不是 Kubernetes 网络模型
去除 --net=host 有助于减少业务服务对宿主机网络的直接依赖。
但当前方案:
text
Docker bridge
+
宿主机 VPC IP
+
-p 端口映射
+
Nacos仍然是当前 ECS + Docker 架构下的解决方式。
它不是 Kubernetes 中:
text
Pod IP
Service
CNI那套网络模型。
因此,这次改造更准确的意义是:
让现有 Docker 部署方式更加显式、独立和可控,并减少对 host network 的依赖。
而不是:
已经完成了向 Kubernetes 网络模型的迁移。
十六、最终回看
如果只保留最终方案,可以把这次改造压缩成:
text
删除 --net=host
↓
使用 bridge
↓
-p 显式映射端口
↓
Nacos 注册宿主机 VPC IP但真正值得留下的是为什么必须这么做。
整个过程实际上是:
text
历史上使用 --net=host
↓
部署简单,但网络边界逐渐模糊
↓
发现 -p 写在命令里实际上没有承担映射职责
↓
决定让服务端口显式化
↓
切换到 bridge
↓
发现 Nacos 自动注册了 172.17.x.x
↓
本机可达不代表跨宿主机可达
↓
重新设计调用路径
↓
Nacos 注册宿主机 VPC IP
↓
VPC IP:端口
↓
Docker -p
↓
目标容器最终去掉的不只是一个 Docker 参数。
真正改变的是两件事:
维护上,让服务开放什么端口重新变得显式。
安全上,让服务重新拥有独立、可控制的网络访问边界。