
1. 网络模式全景先把 Doker 的几种网络模式捋清楚我最早接触 Docker 的时候最不重视的就是网络这块。想着容器跑起来、端口能访问就行直到一次线上事故——服务之间互相访问超时、数据库连不上、日志里全是 Connection refused我才老老实实把 Docker 网络从头到尾啃了一遍。回头看这其实是每个用 Docker 的人早晚要补的一课因为容器网络的选型和配置直接决定了你的服务能不能正常通信、能不能被人访问、能不能灵活扩容。Docker 网络到底解决什么问题一句话它决定了容器怎么和其他容器通信、怎么和宿主机通信、怎么和外部世界通信。Docker 默认提供几种网络模式bridge、host、none、container加上用于 Swarm 集群的 overlay以及更进阶的 macvlan。每种模式的隔离级别、性能开销、端口映射方式完全不同选错了轻则网络不通重则拖着整个架构一起踩坑。先看我整理的一张对比表后面每个模式我都会展开讲网络模式通信范围端口映射适用场景隔离性bridge默认容器 ↔ 容器容器 ↔ 外部需要 -p 做映射单机多容器、开发测试、小型生产好host容器直接使用宿主机网络栈无需映射直接端口对网络性能要求极高的场景差none容器只有 loopback 接口无纯计算任务、安全隔离容器完全隔离container与其他容器共享网络命名空间跟随被共享容器需要共享网络栈的成对容器中overlay跨宿主机容器互联依靠 ingress 或 LBSwarm 集群、跨主机微服务好macvlan容器绑定物理网卡、获得局域网 IP无需映射容器需要直接暴露在局域网中一句话概括单机开发首选 bridge集群跨主机用 overlay性能极端敏感才考虑 host。至于 none 和 container属于特殊场景新手阶段基本用不上但理解原理能帮你更好看懂整个网络模型。2. bridge 模式最常用的默认选择从原理到配置2.1 默认 bridge 为什么叫“默认坑”当你在没有任何特殊参数的情况下执行docker run容器加入的就是 Docker 默认创建的 bridge 网络名字叫docker0。在宿主机上执行ip addr show docker0能看到一个 172.17.0.1/16 的接口具体网段可能因机器而异这就是所有默认 bridge 容器的网关。Docker 在这个网桥上做了一件关键的事每个容器创建时会分到一个 172.17.0.0/24 范围内的 IP通常是 172.17.0.2、172.17.0.3 这样递增同时通过 veth pair一对虚拟网线把容器网卡和 docker0 桥接起来。veth pair 一端在容器里叫 eth0另一端挂在 docker0 上数据包从容器发出走到 docker0再根据目标地址决定要不要转发到宿主机物理网卡。这里有个最容易踩的坑默认 bridge 不支持容器之间通过容器名hostname互相访问。也就是说你在默认 bridge 网络里跑一个 MySQL 容器另一个应用容器里写jdbc:mysql://mysql:3306是解析不到mysql这个主机名的。Docker 默认 bridge 从理论上是支持通过 IP 互通的但容器重建后 IP 会变靠 IP 写配置等于给未来埋定时炸弹。原因在于Docker 的嵌入式 DNS 服务器只在用户自定义 bridge 网络里启用。默认 bridge 网络下的容器只能靠 IP 或者--link参数来访问而--link本质上是往/etc/hosts写一条静态记录容器多了以后维护成本很高。所以我建议凡是多个容器需要互相通信的场景一律使用自定义 bridge 网络别贪方便用默认的。这种做法带来的好处是内置 DNS 解析生效容器之间可以直接用容器名作为主机名来访问这也符合微服务架构中服务注册与发现的思路。2.2 自定义 bridge网段、网关、容器互联创建自定义 bridge 网络是 Docker 网络配置里最常用也最重要的操作。命令很简单docker network create -d bridge --subnet 172.20.0.0/24 --gateway 172.20.0.1 mynet解释一下每个参数-d bridge指定网络驱动为 bridge。下次你用docker network ls能看到自定义网络名字mynet类型显示为 bridge。--subnet 172.20.0.0/24指定容器 IP 分配的网段。默认 bridge 网段是 172.17.0.0/16为了避免和宿主机所在局域网网段冲突自定义网络最好手动指定一段不太常用的私网网段。--gateway 172.20.0.1指定网关一般取子网段的第一个可用 IP 即可。创建好网络后运行容器时指定加入该网络即可docker run -d --name mysql8 --network mynet --ip 172.20.0.10 -e MYSQL_ROOT_PASSWORD123456 mysql:8.0 docker run -d --name app --network mynet -p 8080:80 nginx此时app容器里访问mysql8:3306就能通原因就是自定义网络启用了 Docker 内置 DNS。这个行为背后涉及 Docker 的 DNS 解析链路容器内/etc/resolv.conf指向 127.0.0.11这个地址是 Docker 内嵌的 DNS 服务它能解析容器名到对应容器 IP实现“自动服务发现”。还有一个细节同一个自定义网络里加进来的容器--ip可以手动指定但要注意不能和已存在容器冲突否则启动直接报错而且指定静态 IP 必须在你创建网络时给定的子网范围内。自定义 bridge 还有个很实用的特性可以随时把运行中的容器连接到新的网络也可以断开。docker network connect mynet mysql8 docker network disconnect mynet mysql8这意味着你不需要重启容器就能调整它的网络拓扑。比如排查问题时临时把一个应用容器连接到数据库所在的网络去看看能不能连通不需要改 compose 文件重启服务这个操作在故障定位时非常好用。另一个值得注意的点如果你的容器需要同时访问外网和另一个自定义网络默认情况下只要你创建容器时指定了--network mynet而且还想加默认的 Internet 访问不需要额外做什么因为 Docker 的 NAT 规则默认允许出网流量。但如果你的容器还要和默认 bridge 网络里的容器通信那就得让容器同时连着两个网络docker network connect bridge myapp一条命令搞定容器里会出现两个网卡eth0 和 eth1一个对应自定义网络一个对应默认 bridge 网络。跨网络通信时注意网卡优先级和路由问题不过大部分场景不需要这样玩用到时再深入也不迟。2.3 端口映射-p 和 -P 背后的 iptables 逻辑本地向外暴露服务时docker run -p 8080:80 nginx是最常规的写法。这个映射到底是怎么生效的这背后是一条 iptables DNAT 规则所有到达宿主机 8080 端口的数据包被重定向到容器的 80 端口源 IP 地址也会被改写返回数据能正确送回外部客户端。你可以自己验证一下在宿主机执行iptables -t nat -L DOCKER -n会看到形如DNAT tcp dpt:8080 to:172.20.0.2:80的规则。这里有几个细节需要在配置时特别注意-p 8080:80表示宿主机 8080 映射到容器的 80左侧是宿主机端口。如果省掉左侧写成-p 80Docker 会随机分配一个宿主机端口用docker ps查看实际分配结果。映射 UDP 服务时要加/udp例如-p 53:53/udp。这是因为同一个端口号可能同时是 TCP 和 UDP 服务Docker 默认只处理 TCP。端口映射只对 bridge 网络生效。使用 host 模式的容器不需要-p你等于直接占了宿主机的端口配置-p反而无效。此外Docker 从某个版本起引入了 userland-proxy它是一个用户态进程用于把发到宿主机端口的流量转发给容器同时解决一些 iptables 无法覆盖的边缘 case例如从宿主机自己访问映射端口。如果你想减少这一层代理、获得更好的 NAT 性能可以在/etc/docker/daemon.json里设置{ userland-proxy: false }但注意关闭后宿主机访问自己映射的端口在某些条件下可能出现不通的情况稳妥起见生产环境先做完整验证再决定开关。3. 特殊模式与拓展网络host、none、container、overlay 一次讲透3.1 host 模式为什么快又为什么不建议随便用host 模式下容器不会获得自己的网络命名空间而是直接使用宿主机的网络栈。也就是说容器内的 eth0 就是宿主机的 eth0容器监听的端口就是宿主机监听的端口。优势非常明显网络性能没有任何 NAT 转换损耗数据包直来直去几乎没有额外延迟端口也不用映射容器里的服务启动后直接通过宿主机的 IP 访问即可。但缺点也不能忽视容器和宿主机共享端口空间容易出现端口冲突。容器 A 用了 3306容器 B 就想用 3306 就会失败没有隔离性可言。容器网络层面几乎无隔离疑似网络安全的容器如果跑在 host 模式等于把自己完全暴露在宿主机网络上。无法使用 Docker 自带的一些网络管理功能比如自定义 IP、端口映射、连接自定义网络更别提跨主机网络了。什么场景必须用 host我实际遇到过的有两类一是性能压测类的容器基准测试工具对网络延迟非常敏感二是需要监听非常规网络流量比如抓包工具、网络监控类容器。除此之外的日常业务服务我强烈建议还是用 bridge网络隔离是 Docker 的立身之本主动放弃隔离性要有充分理由。3.2 none 模式和 container 模式的典型玩法--network none是极端隔离方式容器内只有 loopback 接口没有外部网络。这种容器只能自己和自己通信。我目前见过的实际用途有两类一是运行不需要网络的纯计算任务比如数据批处理、离线渲染二是用来做安全审计沙箱容器内的进程无法外连最大限度限制攻击面。--network container:被共享容器名则是把新容器塞进一个已有容器的网络命名空间里。两个容器共享同一个 IP 和端口空间它们之间用 localhost 就能互相访问。典型场景是 sidecar 模式一个主业务容器一个边车容器用于日志收集、流量代理或者监控抓取两者共享同一网络栈边车容器直接通过 127.0.0.1 访问主容器的端口不需要额外的服务发现机制。需要注意一点共享网络的容器中如果主容器重启或删除边车容器的网络也会跟着失效。所以这种模式只适合生命周期高度耦合的容器组不要在无状态业务容器上这么搞否则编排调度时很容易出问题。3.3 overlay 网络跨主机容器的互联关键如果你和我一样用 Docker Swarm 或者想通过 Docker 原生的方式在多个宿主机上编排服务overlay 网络是绕不开的话题。overlay 网络依靠 VXLAN 技术在三层网络之上叠加一层虚拟二层网络数据包会先被封装成 UDP 包通过宿主机之间的物理网络传输到达目标宿主机后再解封装交给对应的容器。创建 overlay 网络的基本命令是docker network create -d overlay --attachable myswarm-net使用--attachable参数后非 Swarm Service 的普通容器也可以接入这个 overlay 网络。这个参数在实际调试中特别有用比如你想临时跑一个调试容器去访问 Swarm 服务内部的网络。这套机制的精髓在于服务发现所有接入同一 overlay 网络的容器自动注册到 Docker 的内置 DNS。你用服务名就能跨主机访问到某一个服务而无需关心它此刻调度在哪一台宿主机上。这种“位置透明”的能力正是微服务架构所依赖的服务实例漂移、扩缩容都不会影响调用端的配置。但注意overlay 网络配置涉及宿主机的端口和防火墙。VXLAN 通常使用 UDP 4789 端口另外 Swarm 管理通信还需要 TCP 2377、TCP/UDP 7946 这些端口。如果你在云上部署安全组里没放行这些端口跨主机的 overlay 网络大概率是起不来的。这个点我已经在不少生产环境里见过翻车案例。3.4 macvlan让容器直接拿到局域网 IP还有一种没那么常用、但某些场景下效果拔群的模式是 macvlan。它允许你给容器分配一个和宿主机同一子网的 IP 地址让容器从网络层面看起来就是一台独立的物理设备可以直接被局域网内的其他机器访问不需要端口映射。典型场景是内网有固定的服务发现机制比如基于 IP 的配置文件、网管系统容器需要以独立 IP 的身份对外提供服务同时又要避免端口映射带来的性能损耗。用法示例docker network create -d macvlan \ --subnet192.168.1.0/24 \ --gateway192.168.1.1 \ -o parenteth0 \ macnet然后运行容器时指定--network macnet容器就会拿到 192.168.1.x 网段的 IP。这里有三个坑我要特别强调宿主机和 macvlan 容器之间默认无法直接通信。因为 macvlan 接口收到从宿主机发出的数据包时会觉得这个包“不该出现在这里”直接丢弃。解决方法是额外创建 macvlan 的 bridge 子接口或者接受“宿主机与容器间不走 macvlan 网络”这个限制。绑定的物理网卡eth0必须处于混杂模式否则无法处理多个 MAC 地址的流量。大部分 Linux 网卡默认支持但某些虚拟化环境或云主机可能有限制。很多云厂商的 VPC 网络本身做了二层隔离即便容器和宿主机在同一子网能否真正对外通信还取决于云平台是否放行该网卡上的附加 MAC 地址。用之前先做小范围测试。4. Docker Compose 场景下的网络配置实战4.1 compose 文件里的网络写法实际做项目时我们很少只用裸docker run命令跑容器更多是用 Docker Compose 来编排一组相关服务。Compose 的网络配置也比命令行更直观我直接给一个可复用的例子version: 3.8 services: mysql: image: mysql:8.0 container_name: mysql8 environment: MYSQL_ROOT_PASSWORD: 123456 networks: backend: ipv4_address: 172.28.0.10 volumes: - mysql_data:/var/lib/mysql redis: image: redis:7 container_name: redis7 networks: - backend app: image: nginx:latest container_name: app ports: - 8080:80 networks: - backend - frontend netdata: image: netdata/netdata container_name: netdata networks: - backend networks: backend: driver: bridge ipam: config: - subnet: 172.28.0.0/24 gateway: 172.28.0.1 frontend: driver: bridge这个编排里有几个值得注意的设计思路backend网络定义为带指定子网的 bridgemysql容器手动分配了 172.28.0.10 的静态 IP为什么因为在生产实践中很多数据库迁移工具和监控系统需要基于固定 IP 做白名单静态 IP 能避免容器重建后 IP 漂移。app服务同时加入backend和frontend两个网络。它既能通过 MySQL 的容器名mysql访问数据库又通过frontend网络接受外部流量这样 MySQL 不暴露给外部只有 Nginx 通过端口映射对外提供服务。这是典型的网络分段安全设计。netdata容器加入backend但不需要对外暴露端口。因为它要监控 MySQL 和 Redis只需要能访问mysql8:3306和redis7:6379即可完全不向外部暴露。Compose 还有一个细节不显式声明 networks 时Compose 会默认创建一个以项目名命名的 bridge 网络所有服务都在同一个网络里可以通过服务名互相访问。这种方式对简单项目完全够用但如果你要精细控制网络分段或者让不同项目之间共享某个网络就必须显式声明。4.2 跨项目共享网络和网络别名多项目协作时经常遇到这样的场景项目 A 里跑着 MySQL项目 B 的后台服务想直接连 A 的数据库。两个项目如果用各自的 compose 文件管理默认网络是各自独立的互不相通。解决办法是把共享网络声明成 external# 项目 B 的 compose 文件片段 networks: shared-net: external: true name: project_a_backend然后在 services 里引用shared-net。前提是项目 A 的 compose 文件创建的网络名确实叫project_a_backendCompose 默认命名规则是项目名_网络名项目名默认取目录名创建目录为 project_a网络声明为 backend最终网络名就是project_a_backend。还有一种做法更优雅在项目 A 里显式给网络起个稳定的名字networks: backend: name: shared-mysql-net这样项目 B 引用的 external 网络名就是shared-mysql-net不再依赖目录名也不容易记错。关于网络别名再补充一个实用小知识。如果你在同一个自定义网络里有两个容器名分别为mysql8和mysql-old想让应用代码统一访问mysql这个名字可以在网络连接时加入别名docker network connect --alias mysql shared-mysql-net mysql8这样其他容器访问mysql时解析到mysql8的 IP。Compose 里对应的写法是services: mysql8: networks: backend: aliases: - mysql别名机制在做灰度发布和数据库切换时特别有用不用改代码就能把流量从老库切到新库。5. 常见网络问题排查与避坑技巧5.1 典型故障容器之间互相访问不通这大概是使用 Docker 后最常见的故障之一。我在实际排障过程中总结了一套固定的排查顺序按这个顺序走基本能定位 90% 的问题第一步确认容器是否在同一网络。docker inspect 容器名 | grep -A 20 Networks两个容器必须在同一个网络里才能直接用容器名互通。如果发现不在一个网络用docker network connect把其中一个容器挂到另一个网络去。第二步检查容器名能否解析。在应用容器里执行docker exec -it app bash ping mysql8如果 ping 不通大概率是 DNS 解析问题。确认一下容器是接在默认 bridge 还是自定义 bridge 网络。默认 bridge 不支持容器名解析只有自定义网络才有内置 DNS。第三步检查目标容器的端口监听。进入目标容器以 MySQL 为例docker exec -it mysql8 bash netstat -tln | grep 3306容器里没监听这个端口外面自然是连不上的。这个问题经常出现在镜像配置了错误的环境变量、服务启动失败或端口配置不一致时。第四步检查宿主机防火墙。很多 Linux 发行版的firewalld或 UFW 会拦截 Docker 网桥的流量。如果是 firewalld尝试systemctl stop firewalld然后再次测试容器间通信。如果你发现停止防火墙后网络恢复就不要简单粗暴地永久关闭防火墙而是把 Docker 的网段加入白名单firewall-cmd --permanent --zonetrusted --add-source172.28.0.0/24 firewall-cmd --reload这只放行 Docker 内部网段外部防火墙策略仍然生效安全性不会被破坏。5.2 宿主机访问不了容器端口这类问题常见于部署完业务后宿主机上访问localhost:8080发现没有响应但容器本身运行正常、日志也没有报错。排查顺序如下先确认端口映射是否生效。docker ps看 PORTS 列有没有0.0.0.0:8080-80/tcp。如果显示的是127.0.0.1:8080-80/tcp说明映射只绑定在本机回环地址外部机器当然无法访问。想在特定 IP 上暴露端口写清楚 IP-p 192.168.1.100:8080:80。其次确认服务是否监听了0.0.0.0。有时容器内的服务默认绑定了 localhost比如某些 Node.js 应用只监听 127.0.0.1Docker 的端口映射转发目标若是 localhost外部流量就进不去。这时去容器配置里把监听地址改成0.0.0.0即可。最后检查云安全组和宿主机防火墙。在云服务器上部署时即使宿主机本地测试没问题外部访问不了也要先看安全组的入站规则是否放行了对应端口。这一步经常被大家忽略甚至我已经帮助好几个朋友排查了半天最后发现是安全组没加规则。提示宿主机在 Windows 上用 Docker Desktop 时Docker 实际运行在 WSL2 虚拟机里端口映射链路是 Windows 主机 → WSL2 虚拟机 → 容器。确认 Windows 防火墙是否拦截了对应端口以及 WSL2 的端口转发是否正常是排查该类问题的重要分支。5.3 Docker Desktop 和 WSL 场景下的网络差异关于 Docker Desktop 在网络方面有一个功能值得特别留意host.docker.internal这个特殊域名。在容器内部访问它可以解析到宿主机的 IP 地址这是 Docker Desktop 默认写入容器 DNS 的一个特殊记录方便容器访问宿主机上的服务。容器内访问宿主机服务时# 容器里执行 curl http://host.docker.internal:3306/ # 探测宿主机上的 MySQL在 Linux 原生 Docker 环境里没有这个域名需要手动用--add-hosthost.docker.internal:宿主机IP加上。跨平台开发时这个差异很容易被忽略同一份代码在 Mac 或 Windows 下用 Docker Desktop 能跑通部署到 Linux 服务器上就报数据库连接失败第一反应应该是检查这个特殊域名配置。WSL2 网络模式本身也有自己的特性。WSL2 默认使用 NAT 网络Docker Desktop 在它之上再叠加一层网络。如果你在 Windows 上用 WSL2 跑 Docker并在 WSL 里直接执行docker run -p 8080:80那么 Windows 主机访问localhost:8080一般没问题但同一局域网内的其他设备想访问就需要 Windows 防火墙放行 WSL 的虚拟网卡或者使用 WSL2 的 mirrored 网络模式较新版本 WSL 支持可以让 WSL 共享宿主机的网络接口实现更透明的网络行为。5.4 网络配置速查大杂烩最后整理一份速查表覆盖了我上文提到的全部关键操作方便你直接照着用目标命令或配置查看当前所有网络docker network ls查看某个网络的详细信息docker network inspect 网络名创建自定义 bridge 网络docker network create -d bridge --subnet 172.20.0.0/24 --gateway 172.20.0.1 mynet容器加入指定网络docker run --network mynet ...运行中容器加入网络docker network connect mynet 容器名运行中容器退出网络docker network disconnect mynet 容器名端口映射 TCPdocker run -p 8080:80 ...端口映射 UDPdocker run -p 53:53/udp ...host 模式运行docker run --network host ...共享其他容器网络docker run --network container:mysql8 ...创建 overlay 网络docker network create -d overlay --attachable myswarm-net创建 macvlan 网络docker network create -d macvlan --subnet192.168.1.0/24 --gateway192.168.1.1 -o parenteth0 macnet查看容器连接网络情况docker inspect 容器名查看 Networks 字段进入容器测试连通性docker exec -it 容器名 ping 目标容器名查看 Docker 的 NAT 规则iptables -t nat -L DOCKER -n在长期的实践里我发现 Docker 网络配置最大的难点不是命令本身而是对“默认行为”的理解。很多坑都源于直觉判断和 Docker 实际实现不符比如默认 bridge 不能域名解析、host 模式下端口映射无效、macvlan 下宿主机和容器互相 ping 不通。把每种模式背后的原理捋清楚了遇到网络问题才能从“瞎试”变成“有思路的排查”。另外多说一句生产环境中对容器网络的任何调整——无论是修改网段、切换网络模式还是调整防火墙策略——都建议先在测试环境完整跑一遍流程再动手。我自己就不止一次因为直接在线上改网络配置导致服务间断连最后花更长时间去恢复现场。Docker 网络操作很灵活但这份灵活要在可控的前提下使用。