首页 / 资讯中心 / 文章详情

Docker网络实战:五种模式、端口映射、跨主机组网与分层排错

Docker网络实战:五种模式、端口映射、跨主机组网与分层排错 ★ FEATURED ARTICLE
容器里的服务自己跑得好好的健康检查也过可同宿主机上另一个容器拿服务名去连死活连不上换成一个固定 IP 立刻通。这种现场我碰到过不止一次第一次遇到时足足查了两个小时最后发现是两个容器根本不在同一个网络里——名字解析走的是 Docker 内嵌的 DNS而默认的 bridge 网络压根不提供这个能力。Docker 网络这块知识文档上写的都是参数说明真正让人卡住的往往是参数背后那条包从容器出来、穿过网桥、被规则改写、最后落到宿主网卡上的完整链路。这篇东西就是把我这些年攒下来的 Docker 网络实操经验摊开讲默认网络到底怎么拼出来的五种网络模式各自的边界在哪-p 参数究竟动了几张表跨主机组网为什么总在 MTU 上翻车以及排错时该怎么一层一层往下剥。不管你是刚把第一个容器跑起来的新手还是已经在维护多服务拓扑的老手下面这些内容应该都能对上你踩过的某个坑。1. 一个连不上的容器把 Docker 网络的组成拆开了1.1 容器网络由哪几块拼成很多人对 Docker 网络的第一印象就是docker network ls里那几个名字觉得网络就是一个配置对象。实际上容器能通网靠的是四样东西叠在一起一是network namespace它给容器提供了一套独立的网卡、路由表、iptables 规则和端口空间二是veth pair一对虚拟网线一头插在容器的 namespace 里另一头插在宿主的 namespace 里三是网桥宿主上的 docker0 或者自定义网桥负责把这些虚拟网线连成一张二层网络四是iptables 规则负责地址转换和转发放行。四者缺一容器就是个孤岛。理解这个分层很关键因为它决定了排错的方向。容器 ping 不通外网问题可能在路由表第三层可能在网桥转发第二层也可能在 NAT 规则iptables还可能是宿主内核的ip_forward没开。你在容器里ping一个域名失败看见的是同一个现象但根因差得远。所以我自己养成一个习惯遇到网络问题先在宿主机上ip link看一眼 veth 是否存在再docker network inspect看容器挂在哪张网桥上最后iptables -t nat -L看规则三分钟能定位大半。顺带说一个容易忽略的点veth pair 是成对存在的容器一停宿主上对应那根 veth 也就消失了。所以你不要用ip link里 veth 的数量去长期统计容器数那个数字是实时变化的脚本里依赖它会出问题。1.2 docker0 网桥和 veth pair 长什么样装完 Docker 之后宿主机上一定会多出一块叫docker0的网卡它默认拿到172.17.0.1/16状态是 UP 但通常没有物理上联。这块网卡就是默认 bridge 网络的网关。容器启动时Docker 会创建一对 veth容器内那端通常叫eth0宿主那端叫veth加一串随机字符然后把宿主端 attach 到 docker0 上。你在容器里ip addr看到eth0的地址是172.17.0.2/16在宿主上ip link show能看到vethxxxxif3这种形式if3后面的数字就是容器内那端的 interface index。这个对应关系在抓包时特别有用你想看某个容器的流量就在宿主上tcpdump -i vethxxxx或者在 docker0 上抓全部。我排一个丢包问题时就是这么干的先在 docker0 上抓发现包根本没出来往回一查是容器内的路由表把目标网段指向了错误的网关。还有一个细节默认 bridge 的容器 IP 是从172.17.0.0/16里顺序分配的容器删了地址可能被别的容器复用。如果你在配置里硬编码了容器 IP重启之后大概率出事。这也是我后面强烈推荐自定义网络的原因之一——自定义网络虽然也会复用地址但至少你能通过名称解析绕开 IP 硬编码这个坑。1.3 出网这一步NAT 链上的顺序容器访问外网包先到 docker0宿主内核发现目标不是本地网段查路由表决定从物理网卡出去然后进POSTROUTING链做源地址转换把源 IP 从172.17.0.2改成宿主网卡的 IP回包再反向转换回来。这条 MASQUERADE 规则在iptables -t nat -L POSTROUTING里能看到条件是源地址属于172.17.0.0/16且出口不是 docker0。问题经常出在FORWARD链上。有些系统的防火墙默认策略是 DROPDocker 启动时会往里插自己的规则但如果规则顺序被别的工具打乱或者有人手动清过链容器就出不了网现象是容器内 ping 网关通、ping 外网不通。这时候你去看iptables -L FORWARD -n -v会看到 Docker 相关的链没有被引用。修复方式不是暴力iptables -F而是重启 Docker 服务让它重新插入规则或者在防火墙配置里显式放行 docker0 的转发。我个人的经验是永远不要在装了 Docker 的机器上随手清 iptables。那些规则不是垃圾是容器通网的命脉。要定制规则走DOCKER-USER链这条链是 Docker 专门留给用户的前置钩子位置在 Docker 自己的规则之前改它不会被容器重启冲掉。2. 五种默认网络模式它们不是选项列表是五个答案2.1 bridge、host、none、container 的边界docker network ls里默认能看到 bridge、host、none 三个第四个 container 模式不在列表里是通过--network container:xxx引用的。这四个加自定义网络构成了容器网络的全部基础形态。它们的区别不在于哪个更好而在于你要解决什么问题。bridge 是默认值容器有独立网络栈通过 NAT 出网需要端口映射才能被外部访问。host 模式下容器直接共享宿主的网络命名空间容器里监听 80 就是宿主监听 80不需要-p也不会有 NAT 开销。none 模式只给容器一个 lo完全没有对外网卡。container 模式让新容器复用另一个已存在容器的网络栈两者共享 IP 和端口空间。模式独立网络栈需要端口映射典型用途bridge默认是是绝大多数普通服务自定义 bridge是是多容器协作需要名称解析host否否高性能网络、需要拿到真实客户端 IPnone是仅 lo不适用纯计算任务、离线批处理container否共享否边车容器、共享网络栈的辅助进程这张表看着简单实际选型时的判断依据只有两条这个容器需不需要独立 IP以及它需不需要和别的容器按名字互相找。第一条决定用不用 host第二条决定用不用自定义网络。默认 bridge 在这两个问题上都是都不太行的答案所以我几乎不在正式环境里用它。2.2 host 模式被低估的地方与被高估的地方host 模式最实在的好处是省掉了一层 NAT。高并发短连接的场景下NAT 带来的 conntrack 表压力和端口耗尽风险是真实存在的我之前维护的一个采集服务QPS 上去之后经常在 conntrack 上报警换成 host 模式后这个现象明显缓解。另一个好处是容器里看到的源 IP 就是真实客户端 IP不用在应用层去解析X-Forwarded-For这类头部。但 host 模式的代价也很明确容器不再有独立 IP端口直接和宿主共享两个容器都想监听 8080 就必然冲突。它还会破坏容器可随意迁移的前提——如果宿主上 80 端口已经被别的服务占了这个容器就起不来。更要命的是可观测性变差你在宿主上ss -lntp看到的进程归属会变得混乱排查端口占用时得额外区分是宿主进程还是容器进程。还有一个常被误解的点host 模式下-p参数不是报错而是被静默忽略并给一条 warning。我就见过有人写了-p 8080:80 --network host然后发现服务监听的其实是 80-p完全没生效。这种参数写了但不起作用的情况最容易误导排查方向。我的判断标准很直接需要拿到真实源 IP、或者对网络性能极其敏感、且端口不会冲突的服务用 host其余一律用自定义 bridge。中间地带的服务宁可多一层 NAT也别为了省那点开销牺牲部署灵活性。2.3 none 与 container 模式的真实用途none 模式听起来很边缘其实在安全敏感的场景里挺有用。给容器一个完全隔离的网络环境只留 lo意味着它连 DNS 都查不了任何外联都出不去。做数据处理的批任务、跑纯计算的模型推理、执行一次性的脚本用 none 模式能从网络层面掐断意外外联的可能。我自己在做一些需要严格隔离的数据清洗任务时就会用--network none跑完把结果通过挂载卷写出去整个过程不碰网络。container 模式的价值在于共享网络栈。最典型的用法是边车主容器跑业务边车容器跑一个本地转发或者采集程序两者共享同一个网络命名空间边车可以直接监听127.0.0.1上的端口主容器访问127.0.0.1就等同于访问边车。这种模式下两个容器连 IP 都一样端口自然也不能冲突。不过 container 模式有个隐蔽的坑网络栈的生命周期跟着被引用的那个容器走。如果主容器重启网络命名空间会重建边车容器虽然还在跑但它引用的那块网络栈已经变了可能出现进程活着但网络断了的诡异状态。所以这个模式我不建议用在长时间运行的服务上用在短生命周期的辅助任务里更合适。3. 自定义 bridge 网络正式环境绕不开它的三个理由3.1 内嵌 DNS 是真正的分水岭默认 bridge 网络最让人难受的地方是容器之间只能用 IP 互访。早年有--link参数能做名称关联但那个机制本质是往容器的 hosts 文件里写死记录容器重启 IP 一变就失效而且 link 是单向的A link B 之后 B 看不见 A。这个参数现在已经被标记为遗留特性新项目不该再用。自定义 bridge 网络解决这个问题的方式是内嵌一个 DNS 解析器监听在127.0.0.11:53。容器内的/etc/resolv.conf里 nameserver 会指向这个地址Docker 通过 iptables 规则把 53 端口的查询重定向到内嵌 DNS。这个 DNS 认识同一网络里所有容器的名字、服务名和--network-alias指定的别名还负责把不属于容器名的查询转发给宿主配置的上游 DNS。这个机制带来的直接好处是容器名就是主机名。你docker run --name mysql-db --network appnet同网络里另一个容器ping mysql-db就能通不用关心它实际分到的是172.18.0.2还是172.18.0.7。在 compose 里更是如此服务名自动成为可解析的主机名这也是为什么 compose 项目里写depends_on加服务名当连接串能直接跑通。有个细节值得记一下内嵌 DNS 只在自定义网络里生效默认 bridge 上的容器用的是宿主机的 DNS 配置解析不了容器名。这就是开头那个换 IP 就通问题的根因。判断方法很简单进容器看一眼/etc/resolv.conf如果 nameserver 是127.0.0.11说明在自定义网络里如果是宿主那几个 DNS 地址那就是默认 bridge。3.2 网络隔离与容器生命周期自定义网络天然带隔离。不同自定义 bridge 网络的容器之间默认完全不通哪怕它们在同一个宿主上、IP 段相邻。这个特性在做多租户或者多环境共存时特别有用把前端、后端、数据库划到三张网络里只有需要通信的两张网络才把容器同时接进去其余的组合天然隔离比写一堆 iptables 规则靠谱得多。生命周期这块也值得说。自定义网络是独立对象容器删了网络还在重新起容器挂上去之前接入的其它容器不受影响。默认 bridge 虽然也是长期存在的但你没法给它加参数也没法控制它的子网更没法在 compose 里声明。而自定义网络可以在创建时指定子网、网关、IP 范围、MTU甚至指定--internal让它完全不能出网。--internal这个参数我要单独提一句它创建出来的网络没有默认路由容器只能和同网络的容器通信出不了外网。做只有数据库层能互相访问、绝对不能连外网这种架构时直接建一个 internal 网络把数据库容器塞进去比事后配防火墙简单得多而且不会因为规则疏漏被绕过。3.3 从零创建一个自定义网络并验证操作本身不复杂但每一步都有验证手段我习惯一步步确认而不是一把梭。# 创建带明确子网的自定义网络 docker network create \ --driver bridge \ --subnet 172.28.0.0/16 \ --gateway 172.28.0.1 \ --opt com.docker.network.bridge.namebr-app \ appnet这里指定com.docker.network.bridge.name是为了让宿主上的网桥有个可读的名字。不指定的话宿主上会多出一块br-加 12 位随机字符的网卡抓包和排查时你得先docker network inspect去查它对应哪张网络很麻烦。指定成br-app之后tcpdump -i br-app就能直接抓这张网络的流量。# 起两个容器接入同一网络 docker run -d --name svc-a --network appnet alpine sleep 3600 docker run -d --name svc-b --network appnet alpine sleep 3600 # 验证名称解析和连通性 docker exec svc-a ping -c 2 svc-b docker exec svc-a nslookup svc-b # 确认容器挂载情况 docker network inspect appnet --format {{range .Containers}}{{.Name}} {{.IPv4Address}}{{\n}}{{end}}docker network inspect的输出里有个Containers字段列出所有接入的容器和它们的地址这个比一个个docker inspect快得多。我平时排查某个容器到底在不在这个网络里就用这条命令一眼就能看出来。还有一个容易踩的点容器运行中不能直接切换网络只能用docker network connect追加一张网卡。一个容器可以同时接入多张自定义网络容器内的eth0、eth1分别对应不同的网络。这个能力在需要跨网络访问的场景里很有用比如让一个网关容器同时接前端网络和后端网络但要注意接多张网络之后容器的默认路由会指向第一张网络出网走哪条链路需要确认清楚。4. 端口映射的完整链路-p 到底改了几张表4.1 从参数到 iptables 规则的对应写-p 8080:80的时候大部分人的心智模型是宿主 8080 转发到容器 80这个理解没错但太粗。实际发生的事情分几步Docker 先在nat表的DOCKER链里插一条 DNAT 规则把目标为宿主 8080 的包改写成容器 IP 的 80 端口同时在filter表的DOCKER链里插一条放行规则另外还会启动一个用户态转发进程监听宿主 8080作为兜底路径。你可以自己验证# 查看 DNAT 规则 sudo iptables -t nat -nL DOCKER # 查看转发放行规则 sudo iptables -nL DOCKER # 看用户的转发进程 sudo ss -lntp | grep 8080iptables -t nat -nL DOCKER的输出里能看到类似DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.28.0.2:80的规则。这条规则解释了为什么容器重启换了 IP 之后端口映射还能继续工作——Docker 会同步更新这条规则。但这个同步依赖 Docker 守护进程正常工作如果有别的工具在管理 iptables两边打架就会出现规则还在但指向了不存在的地址的情况现象是连接被拒绝或者超时。4.2 用户态转发进程与回环地址的处理默认情况下 Docker 会起一个用户态转发进程。很多人不理解为什么有了 iptables 还需要它原因在于nat表的 DNAT 只在 PREROUTING 和 OUTPUT 链生效本机进程访问127.0.0.1:8080时走的是 OUTPUT 链而 Docker 为了兼容这种情况会额外加一条针对回环来源的规则同时保留用户态进程作为兜底。这个进程有时候会带来麻烦。比如你把 Docker 的转发进程关掉配置里userland-proxy: false性能会好一点但某些特殊网络配置下从本机访问映射端口可能就不通了。反过来如果你在宿主上用ss看到一堆转发进程占着端口而对应容器已经删了那就是规则和进程没清理干净需要重启 Docker 服务。端口绑定地址也值得注意。-p 8080:80默认绑定所有网卡包括公网网卡-p 127.0.0.1:8080:80只绑定回环。后者在只允许本机其他服务访问、不允许外部直连的场景里应该成为默认选择。我见过不少配置直接写-p 3306:3306把数据库暴露到公网然后靠数据库密码强度硬扛这个习惯得改。4.3 端口冲突与 DOCKER-USER 链的正确用法端口冲突的报错信息很明确Bind for 0.0.0.0:8080 failed: port is already allocated。但有时候端口明明没被别的容器占用还是报这个错那多半是宿主的其它进程占了或者之前删掉的容器残留了转发进程。查法就是ss -lntp | grep 8080看清楚是谁占的。如果要限制谁能访问映射出来的端口别去改DOCKER链那是 Docker 自己维护的重启动就没了。正确的位置是DOCKER-USER链Docker 会在FORWARD链的最前面跳转到它你在这里加的规则对所有容器转发流量生效而且容器重启不会覆盖。典型用法是只允许某个网段访问某个端口# 只允许 10.10.0.0/16 访问宿主 3306 映射端口 sudo iptables -I DOCKER-USER -p tcp --dport 3306 -s 10.10.0.0/16 -j RETURN sudo iptables -A DOCKER-USER -p tcp --dport 3306 -j DROP注意这里第一条用-I插到最前面第二条用-A追加到末尾顺序不能反。RETURN表示放行并结束这条链的匹配走回 Docker 自己的规则DROP则直接丢弃。这个顺序搞反了会把所有请求都干掉包括白名单里的。改完记得用iptables-save把规则持久化不然重启就丢。5. 跨主机组网overlay 与 macvlan 的适用边界5.1 VXLAN 封装带来的 MTU 账单机上的网络问题都好办一旦容器要跨主机通信复杂度立刻上一个台阶。overlay 网络是 Docker 原生支持的跨主机方案原理是把容器的二层帧封装进 VXLAN通过宿主之间的 UDP 隧道传输。容器以为自己在同一个二层网络里实际上包在宿主之间被套了一层壳。这层壳是有成本的。VXLAN 头部加外层以太头、IP 头、UDP 头一共约 50 字节。宿主网卡 MTU 是 1500 的话隧道里能过的大包就被压到 1450如果容器网卡还按 1500 配一个 1500 字节的包封装后变成 1550超过物理链路 MTU就会被丢弃或者分片。表现出来就是小请求一切正常一旦返回大数据量就卡死TCP 连接建得起来但传不完。这个坑我踩过一次排查了很久。当时一个接口在小数据量下完全正常一旦查询结果超过几 KB 就超时。最后用ping -M do -s 1472一步步试出来的1472 加 28 字节头部正好 1500本地通换成 overlay 网络后 1472 就不通了得降到 1422。结论是 MTU 没配对。解决办法是在创建网络时显式指定 MTUdocker network create \ --driver overlay \ --opt com.docker.network.driver.mtu1450 \ myoverlay或者直接在 Docker 守护进程配置里全局设mtu: 1450。别指望自动探测Docker 不会帮你做路径 MTU 发现这个值必须自己算清楚。5.2 macvlan 让容器直接拿到二层地址macvlan 是另一条路不封包直接给容器分配一个和宿主同网段的 IP每个容器有自己独立的 MAC 地址在物理交换机看来就是一台独立的机器。好处是性能几乎无损容器可以直接被同网段其它设备访问不需要端口映射也不需要 NAT。代价是配置要求高。宿主网卡必须支持混杂模式很多虚拟化平台的默认网卡不支持容器的 IP 需要从物理网段里划一块出来还得确保不和 DHCP 池冲突最麻烦的是宿主和容器之间默认不通因为 macvlan 的设计里同一张网卡上的父子接口不能直接通信需要在宿主上再建一个 macvlan 子接口才能打通。# 创建 macvlan 网络 docker network create -d macvlan \ --subnet192.168.10.0/24 \ --gateway192.168.10.1 \ --ip-range192.168.10.128/25 \ -o parenteth0 \ macnet这里的--ip-range很关键它把容器的地址限制在网段的后半段避免和物理机、DHCP 分配冲突。-o parenteth0指定挂在哪块物理网卡上。这套配置在机房环境里效果很好但在云主机上经常行不通因为云平台的网卡通常不允许随意改 MAC 或者开启混杂模式。5.3 三种跨主机方案的选型对照方案性能开销配置复杂度适用场景主要限制overlayVXLAN中等有封装开销中需要管理集群多主机服务互访动态扩缩容MTU 需手动对齐隧道端口要放通macvlan很低几乎无损高依赖底层网络需要容器直接暴露在物理网段宿主与容器通信需额外配置host 模式 外部路由最低中需要网络侧配合高性能、固定拓扑端口冲突失去隔离性选型的核心判断是你需要的是逻辑上的一张网还是物理上的一台机器。前者用 overlay容器之间按名字互相找扩缩容无感后者用 macvlan容器在网络设备眼里就是一台真实主机。两者都不完美实际项目里我更多是 overlay 打底个别对延迟极敏感的服务单独用 macvlan 或者 host 模式。6. 排错链路从名字解析到丢包的分层定位6.1 第一层名字到底能不能解析网络不通先分清楚是解析不了还是连不上这两个问题的排查路径完全不同。进容器先看三样东西docker exec -it svc-a sh -c cat /etc/resolv.conf; echo ---; cat /etc/hosts; echo ---; nslookup svc-b如果/etc/resolv.conf里的 nameserver 不是127.0.0.11说明容器不在自定义网络里容器名解析必然失败这时候要么换网络要么加--add-host手动写 hosts不推荐IP 一变就废。如果 nameserver 正确但nslookup查不到那要看两个容器是否真在同一张网络上docker network inspect一查就知道。还有一种情况是解析出来的地址不对。比如容器名和某个外部域名撞了内嵌 DNS 优先返回容器地址导致本该访问外部服务的请求打到了容器上。这种问题极难发现现象是偶尔连到奇怪的服务。规避办法是容器命名时加项目前缀别用redis、mysql这种通用词当容器名。/etc/hosts里 Docker 也会写几条记录包括容器自己的主机名。如果你手动改过 hosts注意这些记录在容器重启后会被重写任何手改都不是持久方案。6.2 第二层包出不出去路由对不对解析通了还连不上下一步看路由和连通性。在容器里依次做这几件事# 看接口和地址 ip addr show # 看路由表确认默认网关 ip route show # 测网关通不通 ping -c 2 172.28.0.1 # 测目标端口通不通不依赖应用层协议 nc -zv svc-b 6379ip route的输出能看出很多问题。正常情况下应该有一条default via 172.28.0.1 dev eth0。如果没有默认路由容器出不了网如果默认路由指向了错误的网关包发出去就石沉大海。这种情况在多网络接入的容器上特别常见——接了eth0和eth1两张网络默认路由只指向其中一张访问另一张网络里的服务就得靠静态路由或者明确指定出口。nc -zv比ping更有价值因为很多环境禁了 ICMP 但 TCP 是通的。端口通说明链路层和传输层没问题问题在应用端口不通就往网络层查。这个习惯帮我省了很多时间别一上来就ping先测 TCP。宿主机侧也别忽略。tcpdump -i br-app -nn在自定义网桥的网卡上抓包能看到容器发出来的原始帧tcpdump -i any port 6379能看全链路。如果容器发出来了但目标没收到问题在转发规则如果容器压根没发问题在容器内部。6.3 第三层大包黑洞与 conntrack 表满前面两层都通但业务还是偶发失败这时候要看两个隐蔽问题。大包黑洞表现是小请求正常大响应卡死。判断方法是在容器里用不同大小的包做不分片测试# 1472 28 1500标准以太网 MTU ping -M do -s 1472 -c 2 目标地址 # overlay 环境下应该能通的最大值 ping -M do -s 1422 -c 2 目标地址如果小包通、大包不通几乎可以确定是 MTU 问题。修复方式是把容器网络的 MTU 调成和隧道匹配的值或者让应用层控制单次传输的数据量。TCP 有 MSS 协商机制理论上能自适应但如果中间有设备丢弃了 ICMP 的需要分片消息协商就会失败。conntrack 表满现象是服务运行一段时间后新建连接开始随机失败内核日志里出现nf_conntrack: table full, dropping packet。查看当前用量cat /proc/sys/net/netfilter/nf_conntrack_count cat /proc/sys/net/netfilter/nf_conntrack_max高并发短连接的场景很容易把表打满。临时调大是sysctl -w net.netfilter.nf_conntrack_max1048576同时把nf_conntrack_tcp_timeout_time_established从默认的 5 天调小。但根治还是从架构上减少不必要的连接数比如开启连接池、把高频服务改到 host 模式绕开 NAT。7. 一套贴近业务的 compose 网络划分模板7.1 分区的思路把网络当权限边界用很多人写 compose 的时候把服务全塞一个默认网络里省事但等于放弃了网络层面的隔离。我更倾向把网络当权限边界来用前端网络只放对外暴露的服务和接入层后端网络放业务服务和缓存数据网络放数据库每条边界上只放必须跨界的那个容器。这样做的直接收益是爆炸半径变小。万一某个前端服务被攻破它所在的网络里只有接入层组件碰不到数据库。而且这种隔离不需要额外配置防火墙规则靠 Docker 网络默认不通的特性就能实现。划分粒度也不用太细一个中等规模的系统三到四张网络足够。网络太多反而增加维护成本每次加服务都得想清楚该挂哪张网。我的习惯是按是否需要互相通信分组而不是按技术栈分组——同一个业务域的服务放一张网络哪怕它们是不同语言写的。7.2 完整的 compose 网络配置services: gateway: image: nginx:alpine networks: - frontend - backend ports: - 127.0.0.1:8080:80 api: image: myapp/api:latest networks: - backend - data environment: DB_HOST: db CACHE_HOST: cache cache: image: redis:7-alpine networks: - backend command: [redis-server, --maxmemory, 256mb] db: image: postgres:16-alpine networks: - data volumes: - pgdata:/var/lib/postgresql/data networks: frontend: driver: bridge ipam: config: - subnet: 172.30.0.0/24 backend: driver: bridge ipam: config: - subnet: 172.30.1.0/24 data: driver: bridge internal: true ipam: config: - subnet: 172.30.2.0/24 volumes: pgdata:这份配置有几个设计意图值得说明。gateway同时挂frontend和backend它是唯一能同时看见两边的容器负责转发api挂backend和data能访问数据库cache只挂backend数据库访问不到它db单独在data网络上而且data网络声明了internal: true意味着这个网络里的容器没有默认路由出不了外网。internal: true这一条是我最看重的。数据库容器最常见的风险不是被外部直接攻击而是容器内的进程被诱导去访问外部地址比如拉取恶意依赖、回连某个地址。把网络封死之后这类路径直接断掉。需要给数据库做备份之类的外联操作时临时用docker network connect接一张外网网络操作完再断开比长期开着口子好得多。子网显式指定也很有必要。不指定的话 Docker 会自己从172.18.0.0/16开始分配宿主机上多跑几个项目就可能撞上网段冲突尤其是和公司内网或者家庭网络的网段重叠时现象是容器内访问某个内网地址莫名其妙地打到了容器上。显式规划成172.30.x.0/24这种不常用的段能省掉很多后续麻烦。compose 启动之后容器之间的解析直接用服务名。注意depends_on只保证启动顺序不保证服务就绪连接还得在应用层做重试。这一点跟网络配置无关但经常一起出问题我索性在这里一并提一句。最后补一个实用技巧排查 compose 网络问题时docker compose exec api getent hosts db比nslookup更接近应用的真实解析路径因为很多精简镜像里根本没装nslookup但getent基本都有。这个命令能同时告诉你解析结果和走的是哪条路径排查 DNS 问题时比装一堆工具快。我在实际使用中最大的体会是Docker 网络的复杂度不在配置本身而在于它把好几层抽象的边界藏起来了——容器里的localhost、宿主的localhost、网桥上的地址、物理网卡的地址四者关系不清就会陷入反复试错的循环。把这四层在心里画成一张图遇到问题先定位在哪一层绝大多数故障几分钟内就能收敛。
阅读完成 · 觉得有帮助?
咨询建站