Docker 网络模式详解

Docker 网络模式详解

写在前面

Docker 网络大概是容器化里最容易踩坑的部分——很多问题表象是"服务连不上",根因却在网络层。本篇会讲清楚 Docker 提供的几种网络模式,然后用公司一个真实故障案例做完整复盘——一个困扰团队三天的"容器连不上宿主 Redis"问题。

一、四种原生网络驱动

1.1 bridge(默认)

docker run -d --name web nginx

不指定网络时,容器会加入默认的 bridge 网络。

⚠️ 很多教程里写的"同一 bridge 网络内能用容器名访问"是错的——只有自定义网络才支持容器名解析。

1.2 host

docker run -d --name web --network host nginx

适用场景:

不适用:对外提供服务的容器(安全隔离差)。

1.3 none

docker run -d --name isolated --network none alpine

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

生产环境应该总是用自定义网络。

二、容器间通信的三种方式

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 这次故障的根因复盘

  1. 临时凑合的架构:Redis 是历史遗留服务,没容器化,API 容器化时偷懒直接连宿主
  2. 网络拓扑混乱:容器服务与宿主服务混用,出问题难定位
  3. 缺乏监控:Redis bind 配置改了之后没通知任何依赖方,也没有连通性监控

5.6 我们后续的改进

  1. 所有中间件容器化:Redis、RabbitMQ、Elasticsearch 全部 docker-compose 化
  2. 禁止容器连宿主:code review 时检查,有没有写 localhost / 127.0.0.1
  3. 加网络连通性监控:用 Prometheus blackbox exporter 定期检测关键端口
  4. .env 里强制用域名:不写 IP,IP 会变,域名是稳定的

六、DNS 解析的几个常见坑

6.1 自定义网络里 DNS 解析失败

docker exec api nslookup redis
# server can't find redis: NXDOMAIN

可能原因:

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 容器化迁移的真实过程,以及各种备份/恢复的坑。

参考资料