Skip to content

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
8003

Docker 不再负责做:

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.41

Docker 显式映射:

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:9997

13.3 验证 Nacos

在 Nacos 控制台确认实例:

text
172.16.0.41:8003

而不是:

text
172.17.x.x:8003

13.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 参数。

真正改变的是两件事:

维护上,让服务开放什么端口重新变得显式。

安全上,让服务重新拥有独立、可控制的网络访问边界。