在深入理解 Docker 的宿主机网关之前,我们需要先理解网络中「网关」的基本概念。

在传统网络中,网关 (Gateway) 是连接两个不同网络的设备或节点。它就像是两个网络之间的「门」,负责在不同网络之间转发数据包。举个生活中的例子:

  • 你家里的路由器就是一个网关
  • 你的电脑(如 192.168.1.100)想访问互联网上的网站
  • 数据包必须先发送到路由器(如 192.168.1.1)
  • 路由器再将数据包转发到互联网
1
2
[你的电脑] ---> [家庭路由器/网关] ---> [互联网]
192.168.1.100 192.168.1.1 外部网络

由此可见,网关有如下作用:

  1. 路由转发: 将数据包从一个网络转发到另一个网络
  2. 地址转换: 进行 NAT (网络地址转换)
  3. 协议转换: 在不同协议的网络之间转换
  4. 访问控制: 控制哪些数据可以通过

Docker 网络架构中的网关

Docker 桥接网络 (Bridge Network)

当你使用 Docker 时,Docker 会创建一个虚拟网桥 (Virtual Bridge),默认名为 docker0。这个虚拟网桥的工作原理类似于物理交换机,它连接了:

  • 宿主机的网络
  • 各个容器的虚拟网络接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
        宿主机 (Host OS)
┌─────────────────────────────────┐
│ │
│ 物理网卡 (eth0) │
│ IP: 192.168.0.10 │
│ ↕ │
│ ┌─────────────────┐ │
│ │ docker0 │ │
│ │ (虚拟网桥) │ │
│ │ 172.17.0.1 ←── 这是网关! │
│ └────────┬────────┘ │
│ │ │
│ ┌─────┴──────┐ │
│ │ │ │
│ ┌──┴──┐ ┌──┴──┐ │
│ │veth1│ │veth2│ │
│ └──┬──┘ └──┬──┘ │
└─────┼──────────┼────────────────┘
│ │
┌─────┴──────────┴─────┐
│ │
┌───────┴────────┐ ┌───────┴──────────┐
│ 容器 A │ │ 容器 B │
│ eth0 │ │ eth0 │
│ 172.17.0.2 │ │ 172.17.0.3 │
│ │ │ │
│ 网关: 172.17.0.1│ │ 网关: 172.17.0.1 │
└─────────────────┘ └──────────────────┘

网关 IP 的含义

Docker 网关 IP (172.17.0.1) 实际上是:

  1. 虚拟网桥 docker0 在容器网络中的 IP 地址
  2. 容器访问外部网络的出口
  3. 容器访问宿主机的入口

为什么网关可以访问到宿主机?

这是最关键的理解点。让我们一步步分解:

1. 网桥在宿主机上

docker0 虚拟网桥实际上是宿主机上的一个网络设备,你可以在宿主机上看到它:

1
2
3
4
5
6
7
8
# 在宿主机上执行
ip addr show docker0

# 输出类似:
# 4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
# link/ether 02:42:5e:8f:32:a1 brd ff:ff:ff:ff:ff:ff
# inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
# valid_lft forever preferred_lft forever

2. 网关既在容器网络中,也在宿主机网络中

docker0 网桥是一个特殊的存在,它同时属于两个「世界」:

  • 在容器的视角: 172.17.0.1 是网关,是通往外部的出口
  • 在宿主机的视角: 172.17.0.1 是本机的一个网络接口

这就像是一扇门:

  • 从容器内看,门通往外面(宿主机)
  • 从宿主机看,门就在自己家里

3. 数据包的流转过程

当容器访问 172.17.0.1 时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
步骤 1: 容器发送数据包
[容器 172.17.0.2] ---> 目标: 172.17.0.1
"我要访问网关"

步骤 2: 数据包通过 veth pair (虚拟网线)
[容器的 eth0] ---> [veth1] ---> [docker0 网桥]

步骤 3: docker0 收到数据包
docker0 发现: "这个包是发给我的 (172.17.0.1)"

步骤 4: 包被宿主机的网络栈处理
由于 docker0 是宿主机上的设备,数据包实际上被宿主机的内核处理

步骤 5: 如果访问的是宿主机端口 (如 6379)
数据包被转发到宿主机上监听 6379 端口的服务

详细示例:容器访问宿主机上的 Redis

让我们通过一个完整的例子来理解:

场景设置

1
2
3
4
5
# 1. 宿主机上启动 Redis (监听所有接口)
docker run -d -p 6379:6379 redis

# 2. 查看 Docker 网络信息
docker network inspect bridge

输出类似:

1
2
3
4
5
6
7
8
9
10
11
{
"Name": "bridge",
"IPAM": {
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
}
}

数据包流转详解

假设 OpenResty 容器 (172.17.0.5) 要访问宿主机的 Redis:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
第 1 步: OpenResty 容器内的代码
────────────────────────────────
red:connect("172.17.0.1", 6379)
┌─────────────────────────┐
│ OpenResty 容器 │
│ IP: 172.17.0.5 │
│ │
│ 发送: SYN 包 │
│ 源: 172.17.0.5:随机端口 │
│ 目标: 172.17.0.1:6379 │
└─────────────────────────┘

│ 数据包通过容器的网络命名空间


第 2 步: 通过虚拟网卡对 (veth pair)
────────────────────────────────

[容器内 eth0]

│ (veth pair 就像一根虚拟网线)

[宿主机上的 veth-xxx]



第 3 步: 到达 docker0 网桥
────────────────────────────────
┌──────────────────────────┐
│ docker0 虚拟网桥 │
│ IP: 172.17.0.1 │
│ │
│ 收到数据包: │
│ 目标地址是我自己! │
│ 目标端口是 6379 │
└──────────────────────────┘

│ docker0 是宿主机上的设备
│ 所以数据包实际上已经"到达"宿主机


第 4 步: 端口转发规则生效
────────────────────────────────
宿主机网络栈处理这个包:
- 目标 IP: 172.17.0.1 (这是本机 IP)
- 目标端口: 6379

宿主机检查: "有程序在监听 6379 吗?"
- 发现 Redis 容器通过 -p 6379:6379 映射了端口
- iptables 规则将请求转发到 Redis 容器

第 5 步: 到达 Redis 容器
────────────────────────────────


┌─────────────────────────┐
│ Redis 容器 │
│ IP: 172.17.0.2 │
│ │
│ Redis 服务收到连接请求 │
└─────────────────────────┘

关键的 iptables 规则

Docker 在宿主机上创建了一系列 iptables 规则来实现端口映射:

1
2
3
4
5
# 在宿主机上查看
sudo iptables -t nat -L -n | grep 6379

# 会看到类似规则:
# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:6379 to:172.17.0.2:6379

这条规则的意思是:

  • 任何到达宿主机 6379 端口的 TCP 连接
  • 都会被 DNAT (目标地址转换) 到 172.17.0.2:6379 (Redis 容器)

不同场景下的网关

1. 默认 bridge 网络

1
docker network inspect bridge
  • 网关: 通常是 172.17.0.1
  • 子网: 172.17.0.0/16

2. 自定义 bridge 网络

1
2
docker network create --subnet=172.20.0.0/16 my-network
docker network inspect my-network
  • 网关: 通常是 172.20.0.1 (子网的第一个 IP)
  • 子网: 172.20.0.0/16

3. docker-compose 创建的网络

1
2
3
4
version: '3'
services:
app:
image: myapp

Docker Compose 会自动创建一个网络,名称类似 projectname_default:

1
docker network inspect projectname_default

输出可能是:

1
2
3
4
5
6
7
8
9
10
11
{
"Name": "projectname_default",
"IPAM": {
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
}
}

这时容器的网关就是 172.18.0.1

如何查找容器的网关 IP

方法 1: 在容器内查看路由表

1
2
3
4
5
6
7
8
9
10
# 进入容器
docker exec -it container_name sh

# 查看默认网关
ip route | grep default

# 输出:
# default via 172.18.0.1 dev eth0
# ^^^^^^^^^^^
# 这就是网关 IP

方法 2: 查看容器网络信息

1
2
3
4
5
# 在宿主机上
docker inspect container_name | grep Gateway

# 输出:
# "Gateway": "172.18.0.1"

方法 3: 在容器内读取环境变量

有些情况下,网关 IP 也可以通过解析 DNS 获取:

1
2
# 在容器内
getent hosts host.docker.internal

为什么通过网关能访问宿主机端口

总结一下关键点:

1. 网关 IP 是宿主机的一部分

1
2
3
172.17.0.1 不是一个"远程"地址
它是宿主机上 docker0 设备的 IP 地址
就像宿主机的 127.0.0.1 或其他网卡 IP 一样

2. 端口映射创建了转发规则

1
docker run -p 6379:6379 redis

这个命令做了什么?

  1. 在宿主机的所有网络接口上监听 6379 端口
  2. 包括 docker0 (172.17.0.1) 接口
  3. 创建 iptables 规则: 所有到 6379 的连接 → 转发到 Redis 容器

3. 容器访问网关 = 访问宿主机

1
2
3
4
5
6
7
容器访问 172.17.0.1:6379

实际上是访问宿主机的 docker0 接口的 6379 端口

宿主机的 iptables 规则生效

连接被转发到 Redis 容器的 6379 端口

常见误区

误区 1: 「网关就是宿主机」

错误理解: 网关 IP 就是宿主机 IP

正确理解:

  • 网关 IP 是 docker0 虚拟网桥在容器网络中的 IP
  • 宿主机还有其他 IP (如物理网卡 IP: 192.168.0.10)
  • 但 docker0 确实是宿主机上的一个设备

误区 2: 「容器可以直接访问宿主机的 127.0.0.1」

错误:

1
red:connect("127.0.0.1", 6379)  -- 不会工作

正确:

1
2
3
red:connect("172.18.0.1", 6379)  -- 通过网关
-- 或
red:connect("host.docker.internal", 6379) -- Docker Desktop

误区 3: 「只有映射了端口才能访问」

部分正确:

  • 通过网关访问宿主机端口,需要端口映射 (-p)
  • 但容器之间直接通信不需要端口映射
  • 容器可以直接访问其他容器的内部端口

实际应用示例

场景: OpenResty 需要连接宿主机上的多个服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- config.lua
local _M = {}

-- 动态获取网关 IP
local function get_gateway_ip()
local f = io.popen("ip route | grep default | awk '{print $3}'")
local gateway = f:read("*line")
f:close()
return gateway or "172.17.0.1"
end

_M.gateway = get_gateway_ip()

-- 服务配置
_M.redis_host = _M.gateway
_M.redis_port = 6379

_M.mysql_host = _M.gateway
_M.mysql_port = 3306

_M.elasticsearch_host = _M.gateway
_M.elasticsearch_port = 9200

return _M

场景: 在不同的 Docker Compose 项目间通信

最佳方案是使用共享网络,而不是通过网关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 创建共享网络
docker network create shared-net

# Redis 项目
# docker-compose.yml
version: '3'
services:
redis:
image: redis
networks:
- shared-net

networks:
shared-net:
external: true

# OpenResty 项目
# docker-compose.yml
version: '3'
services:
openresty:
image: openresty/openresty
networks:
- shared-net

networks:
shared-net:
external: true

这样 OpenResty 可以直接使用服务名:

1
red:connect("redis", 6379)  -- 直接通过服务名

总结

Docker 宿主机网关本质上是:

  1. 虚拟网桥 (docker0) 在容器网络中的 IP 地址
  2. 容器访问宿主机的桥梁
  3. 宿主机网络栈的一部分

通过网关访问宿主机端口的完整路径:

1
容器 → 容器网关 (docker0) → 宿主机网络栈 → iptables 规则 → 目标容器/服务

理解了网关的本质,就能明白:

  • 为什么容器内 127.0.0.1 不能访问宿主机服务
  • 为什么要使用网关 IP 才能访问
  • 为什么共享网络是更好的解决方案

我在遇到前述问题之后,好好查找整理了这个文档,希望能帮助你我彻底理解 Docker 网络架构中的网关概念。