Docker 网络模式详解
Docker 网络模式详解
写在前面
Docker 网络大概是容器化里最容易踩坑的部分——很多问题表象是"服务连不上",根因却在网络层。本篇会讲清楚 Docker 提供的几种网络模式,然后用公司一个真实故障案例做完整复盘——一个困扰团队三天的"容器连不上宿主 Redis"问题。
一、四种原生网络驱动
1.1 bridge(默认)
docker run -d --name web nginx
不指定网络时,容器会加入默认的 bridge 网络。
- 网络结构:每个容器分配独立 IP,通过 docker0 网桥与宿主机通信
- 容器间互通:必须用 IP,不能用容器名(默认 bridge 没有 Docker DNS)
- 适用场景:单容器独立运行,或者多容器用
docker run --link(已不推荐)
⚠️ 很多教程里写的"同一 bridge 网络内能用容器名访问"是错的——只有自定义网络才支持容器名解析。
1.2 host
docker run -d --name web --network host nginx
- 网络结构:容器直接共享宿主机的网络栈,没有网络隔离
- 优势:性能最高(零网络转换),适合网络密集型应用
- 劣势:端口冲突风险;容器与宿主机共享端口空间
适用场景:
- 网络密集型应用(Redis、消息队列的某些场景)
- 需要监听大量端口(某些 P2P 协议)
- 需要看到宿主 IP(容器日志里直接是真实 IP)
不适用:对外提供服务的容器(安全隔离差)。
1.3 none
docker run -d --name isolated --network none alpine
- 网络结构:除了 loopback,啥都没有
- 适用场景:完全不需要网络的批处理任务、数据导入脚本
1.4 custom(自定义网络)
docker network create --driver bridge \
--subnet 172.20.0.0/16 \
--gateway 172.20.0.1 \
--ip-range 172.20.0.0/24 \
my-net
- 网络结构:用户自定义的 bridge
- 核心优势:
- 容器名 DNS 解析(自动)
- 可配置子网、网关
- 支持多个容器互联
- 支持网络隔离(internal: true)
生产环境应该总是用自定义网络。
二、容器间通信的三种方式
2.1 通过 IP
最原始的方法,生产环境基本不用——IP 会在容器重启后变化。
# 启动容器,获取 IP
docker run -d --name api my-api
docker inspect --format '{{.NetworkSettings.IPAddress}}' api
# 172.17.0.2
# 另一个容器连这个 IP
docker run -d --name client --add-host api.local:172.17.0.2 my-client
2.2 通过容器名(DNS)
只在自定义网络内有效:
docker network create my-net
docker run -d --name api --network my-net my-api
docker run -d --name client --network my-net my-client
# client 里可以直接用 api 这个名字
docker exec -it client ping api
# PING api (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.123 ms
实现原理:每个自定义网络启动一个嵌入式 DNS 服务器(127.0.0.11:53),容器解析容器名时走这个 DNS。容器的别名也可以:
services:
api:
networks:
default:
aliases:
- backend
- api-service
2.3 通过 link(已废弃)
docker run -d --name client --link api:backend my-client
--link 已经被 Docker 官方标记为 deprecated,生产代码不要再用。完全可以用自定义网络替代。
三、容器与宿主通信
3.1 宿主访问容器
直接访问容器映射的端口:
docker run -d --name web -p 8080:80 nginx
# 浏览器访问 http://localhost:8080
3.2 容器访问宿主
这是关键问题——容器里访问宿主的服务,IP 是多少?
不同操作系统的差异
| OS | 宿主 IP | 说明 |
|---|---|---|
| Linux | 172.17.0.1(默认 bridge) |
网桥 IP |
| Linux(自定义网络) | 宿主的对应网关 IP | 通常是子网第一个 IP |
| Mac/Windows(Docker Desktop) | host.docker.internal |
Docker Desktop 提供的特殊域名 |
| WSL2 | host.docker.internal(需配置) |
在容器里看宿主 IP
Linux 默认 bridge 下:
docker run -it --rm alpine sh
ip route # 看默认网关,就是宿主 IP
# default via 172.17.0.1 dev eth0
所以容器里访问宿主 Redis:
redis-cli -h 172.17.0.1 -p 6379
但这样太脆弱——换一个网络就变了。推荐用 Docker 提供的特殊域名:
Linux 自定义网络下
宿主没有固定 IP,但 docker0 网桥的 IP 一般是 172.17.0.1 或者自定义网络的网关 IP。
最佳实践:用 docker compose,把宿主当成一个 service,起一个 sidecar。比如:
services:
api:
networks:
- app-net
# 把宿主的端口"代理"成容器可用的服务
redis-host-proxy:
image: alpine/socat
command: tcp-listen:6379,fork,reuseaddr tcp-connect:host.docker.internal:6379
networks:
- app-net
API 容器里直接连 redis-host-proxy:6379。
Mac/Windows
# 容器里直接用 host.docker.internal
docker run -it --rm alpine sh
ping host.docker.internal
# PING host.docker.internal (192.168.65.2): 56 data bytes
Mac/Windows 下宿主 IP 就是 host.docker.internal。
四、跨主机网络
单机编排用 bridge 就够了,跨主机编排需要 Overlay 网络:
# 初始化 Swarm 集群
docker swarm init
# 创建 overlay 网络
docker network create --driver overlay \
--subnet 10.0.0.0/24 \
my-overlay
# 任意节点上的容器加入这个网络,都可以互联
Overlay 网络用 VXLAN 封装,底层走 UDP。多用于 Swarm 模式,K8s 用的是自己的 CNI 插件(Flannel/Calico/Cilium)。
五、实战排错:“容器连不上宿主 Redis"完整复盘
5.1 故障描述
时间:2024 年 3 月某个周四晚上 8 点
现象:生产环境的 API 服务(容器化部署)突然报 Redis connection refused
影响:订单创建接口全部失败,持续 17 分钟
恢复:运维手动重启 Redis 容器
5.2 第一轮排查:网络层
# 进 API 容器
docker exec -it api sh
# 看 IP
ip addr
# 172.18.0.5/16
# 看路由
ip route
# default via 172.18.0.1 dev eth0
# ping 宿主
ping 172.18.0.1
# 通
# ping 宿主的 Redis 端口
nc -zv 172.18.0.1 6379
# Connection refused
Connection refused 说明端口根本没监听,或者被防火墙挡住。
5.3 第二轮:宿主机层面
# 在宿主机看 Redis 状态
redis-cli ping
# PONG ← Redis 在运行
# 监听端口
ss -tlnp | grep 6379
# 127.0.0.1:6379 ← 只监听 localhost!
找到了。
Redis 的 bind 127.0.0.1 导致它只接受本机连接。容器访问宿主用的是宿主在 docker0 网桥上的 IP(172.17.0.1),不是 127.0.0.1,所以连不上。
5.4 修复方案
方案 A:改 Redis 配置(短期)
# /etc/redis/redis.conf
bind 0.0.0.0 -::*
protected-mode yes
requirepass yourpassword
方案 B:用 socat 端口转发(中期)
services:
redis-proxy:
image: alpine/socat
command: tcp-listen:6379,fork,reuseaddr tcp-connect:127.0.0.1:6379
networks:
- app-net
API 连 redis-proxy:6379,Redis 保持只监听 127.0.0.1 不变。
方案 C:Redis 也容器化(长期,推荐)
services:
redis:
image: redis:7-alpine
networks:
- backend # 只在内部网络
volumes:
- redis-data:/data
把 Redis 彻底容器化,不再有"宿主服务"的概念,所有服务都在 Docker 网络里互相通信。
5.5 这次故障的根因复盘
- 临时凑合的架构:Redis 是历史遗留服务,没容器化,API 容器化时偷懒直接连宿主
- 网络拓扑混乱:容器服务与宿主服务混用,出问题难定位
- 缺乏监控:Redis
bind配置改了之后没通知任何依赖方,也没有连通性监控
5.6 我们后续的改进
- 所有中间件容器化:Redis、RabbitMQ、Elasticsearch 全部 docker-compose 化
- 禁止容器连宿主:code review 时检查,有没有写
localhost/127.0.0.1 - 加网络连通性监控:用 Prometheus blackbox exporter 定期检测关键端口
- .env 里强制用域名:不写 IP,IP 会变,域名是稳定的
六、DNS 解析的几个常见坑
6.1 自定义网络里 DNS 解析失败
docker exec api nslookup redis
# server can't find redis: NXDOMAIN
可能原因:
redis容器没启动redis容器没加入同一个自定义网络- 容器名拼写错(注意大小写敏感)
6.2 DNS 解析超时
docker exec api nslookup redis
# ;; connection timed out; no servers could be reached
嵌入式 DNS 没启动,可能是网络驱动不兼容。检查:
docker network inspect my-net | grep Driver
必须是 bridge 或 overlay,不能是其他。
6.3 容器重启后 DNS 缓存问题
容器重启后,旧的 IP 失效,需要等 DNS 刷新。嵌入式 DNS 的 TTL 比较长(600s),生产环境可以缩短:
services:
api:
dns_opt:
- "ndots:2"
- "timeout:3"
- "attempts:2"
七、本篇小结
| 主题 | 关键点 |
|---|---|
| 四种网络驱动 | bridge/host/none/custom |
| 自定义网络 | 必须用,支持 DNS 解析 |
| 容器间通信 | 优先用容器名(DNS),不用 IP |
| 容器访问宿主 | Linux 用网关 IP,Mac/Win 用 host.docker.internal |
| 生产建议 | 所有中间件容器化,禁止容器连宿主 |
| 跨主机 | Swarm Overlay 或 K8s CNI |
下一篇讲 数据持久化与卷管理,实战 MySQL 容器化迁移的真实过程,以及各种备份/恢复的坑。