简单理解 Docker 网络

简单理解 Docker 网络

概述

在现代微服务开发中,我们经常使用 Docker Compose 将多个容器编排成一个完整的应用系统。然而,在处理不同容器间的连接以及端口监听时,开发者常会遇到“明明端口映射了却连不上”或“容器之间无法通信”的诡异问题。

要彻底解决这些问题,我们需要厘清三个层面的网络上下文:宿主机Docker Compose 网络以及容器内部

容器内的网络隔离与“localhost”陷阱

首先,我们需要建立一个认知:每个 Docker 容器本质上都是一个独立的网络命名空间。

这就好比每个容器都是一台独立的小型服务器,拥有自己独立的协议栈、路由表、IP 地址和网络接口(如 eth0lo)。

一个常见的错误场景是: 1. 在容器内启动了一个 Web 服务,配置它监听 localhost:8080(即 127.0.0.1:8080)。 2. 在 docker-compose.yml 中,将容器端口映射到宿主机:8080:8080。 3. 结果:在宿主机访问 http://localhost:8080 时,连接被拒绝。

为什么会这样?

Docker 的端口映射(Port Mapping)机制,本质上是将宿主机端口的流量转发到容器的 eth0 网卡接口上。如果应用仅监听容器内的 127.0.0.1(本地回环接口),它就只会处理来自容器内部本身的流量,而完全忽略从 eth0 进来的外部请求。

网络接口与监听地址

为了理解这个问题,我们回顾一下操作系统处理网络通信的抽象模型:

graph TD
    A[应用程序] --> B[端口 Port]
    B --> C[IP 地址]
    C --> D[网络接口 Network Interface]
    D --> E[物理/虚拟链路]

操作系统中的 TCP/IP 通信必须依赖网络接口(Network Interface)。这些接口各有其 IP,语义完全不同:

  • lo (Loopback Interface): 对应的 IP 是 127.0.0.1 (localhost)。这是一个纯逻辑的虚拟接口,数据包直接在内核中“自发自收”,不经过任何物理设备。
  • eth0 (Ethernet Interface): 对应容器的虚拟网卡,通常拥有一个 Docker 子网 IP(例如 172.18.0.x)。这是容器与外界(包括宿主机和其他容器)通信的桥梁。

因此,服务监听地址(Bind Address)的选择决定了谁能访问它:

  • 监听 127.0.0.1:8080
    • 仅限本机访问。只有容器内部的进程可以通过 localhost 访问该服务。
    • 宿主机转发过来的流量(来自 eth0)会被内核丢弃或拒绝。
  • 监听 192.168.x.x:8080 (特定网卡 IP)
    • 仅限该网卡所在的网络访问。
  • 监听 0.0.0.0:8080 (最佳实践)
    • 绑定所有接口。这意味着服务同时监听 loeth0 等所有可用接口。
    • 无论是容器内部访问,还是宿主机转发进来的外部流量,都能被正确处理。

所以结论就是,在 Docker 容器化环境中,如果服务需要被外界(宿主机、其他容器)访问,配置中的监听地址(Listen Address / Bind Address)必须设置为 0.0.0.0

Docker Compose 网络与服务发现

当我们从单个容器扩展到使用 Docker Compose 编排多个服务(例如 App + Database + Gateway)时,情况又有所不同。我们不仅需要关注端口映射,还需要关注容器间的互联

当执行 docker compose up 时,Docker 会执行以下网络操作: 1. 创建一个默认的桥接网络(Bridge Network):这相当于在逻辑上创建了一个虚拟交换机。 2. 分配子网:为这个网络分配一个 IP 段(如 172.20.0.0/16),并设置网关(Gateway)。 3. 连接容器:将每个服务容器都连接到这个虚拟交换机上,并分配独立的 IP。

基于 DNS 的服务发现

在 Docker Compose 网络中,我们不需要(也不应该)硬编码 IP 地址。Docker 内置了 DNS 服务器,实现了基于服务名的服务发现

docker-compose.yml 中定义的 services 名称,会自动成为该网络中的主机名(Hostname)。

示例 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3.8'

services:
# 消息队列服务
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:latest
container_name: redpanda-1
command:
- redpanda start
# 关键点:这里必须监听 0.0.0.0,否则网关和其他容器无法连接
- --kafka-addr internal://0.0.0.0:9092
ports:
- "9092:9092"

# 控制台服务
console:
image: docker.redpanda.com/redpandadata/console:latest
environment:
# 关键点:直接使用服务名 "redpanda" 作为域名进行连接
- KAFKA_BROKERS=redpanda:9092
depends_on:
- redpanda

在这个例子中: * 互通原理console 容器可以通过域名 redpanda 解析到 redpanda-1 容器的内部 IP。 * 端口访问console 访问 redpanda:9092 时,流量直接在 Docker 子网内传输,不经过宿主机的端口映射。 * 前提条件redpanda 容器内的进程必须在 0.0.0.0:9092 上监听,才能接收来自 console(也就是来自 eth0)的连接请求。

总结:全链路网络架构图

为了更直观地理解流量是如何流转的,我们可以将网络架构分层可视化:

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
       [ 外部互联网 / 用户 ]
|
| (访问宿主机 IP:9092)
v
+---------------------------------------+
| 宿主机 (Host) |
| |
| +---[ 端口映射 (Port Mapping) ]--+ | <-- Docker Proxy / DNAT
| | 监听 0.0.0.0:9092 -> 转发到容器 | |
| +--------------+-----------------+ |
| | |
| (流量进入 Docker 网桥) |
| | |
| +-----------[ Docker Compose Subnet (虚拟交换机) ]-----------+
| | IP 段: 172.20.0.0/16 |
| | 网关: 172.20.0.1 |
| | DNS: 自动解析服务名 (redpanda -> 172.20.0.2) |
| +-------+----------------------------------+-----------------+
| | |
| v (eth0) v (eth0)
| +---------------------+ +---------------------+
| | Container A | | Container B |
| | (Redpanda) | | (Console) |
| | IP: 172.20.0.2 | <--------> | IP: 172.20.0.3 |
| | | (内部互联) | |
| | [进程监听] | | [进程发起连接] |
| | 0.0.0.0:9092 (√) | | 目标: redpanda:9092 |
| | 127.0.0.1:9092 (x) | | |
| +---------------------+ +---------------------+
+---------------------------------------+

关键点总结:

  1. 对外暴露:如果容器需要被宿主机外部访问,必须做端口映射,且容器内进程需监听 0.0.0.0
  2. 内部互联:在 Docker Compose 中,服务之间通过服务名(如 redpanda)互相访问,流量走 Docker 内部子网,不依赖宿主机端口映射。
  3. 监听原则:无论哪种情况,容器内的服务若想接收“非本机”流量(包括来自网关或其他容器的流量),必须监听 0.0.0.0,而不是 localhost