Docker 教程

Docker 网络管理

在容器化应用的世界里,单个容器往往只是一个庞大系统中的组件,它们需要相互通信才能发挥完整的功能。虽然我们在前面的章节中探讨过如何“暴露端口”来允许外部访问容器,但对于真正健壮且安全的多容器应用来说,我们需要一种更高级的内部通信方式。

这正是 Docker 强大的网络功能大显身手的地方。它允许我们为服务定义隔离的、灵活的且高效的通信渠道。了解如何创建和管理这些网络,是编排复杂的分布式应用的基础。它能确保你的服务可靠地相互发现和交互,同时又不会把内部端口不必要地暴露给宿主机或外部网络。本章将带你彻底掌握 Docker 网络管理。

1. 掌握 Docker 的网络管理系统

Docker 提供了一个复杂的网络栈,允许容器之间、容器与宿主机之间,以及容器与外部世界之间进行通信。虽然容器可以通过暴露的端口进行通信,但通过专用网络来管理这些连接,能为多服务应用提供更好的隔离性、更优秀的名称解析(DNS)以及更高的灵活性。

1.1 回顾:Docker 默认网络驱动

在深入学习创建自定义网络之前,我们先简单回顾一下上一章介绍的 Docker 核心网络驱动。这些驱动决定了网络的底层实现方式以及连接在上面的容器的行为:

  • Bridge (桥接网络): 如果没有指定其他网络,这是新创建容器的默认网络。在同一个桥接网络上的容器可以通过 IP 地址,或者更方便地通过容器名称(得益于 Docker 内置的 DNS 服务)相互通信。宿主机可以通过端口映射访问桥接网络上的容器,容器也可以访问外部世界。相比默认的 bridge 网络,用户自定义的桥接网络 (User-defined bridge) 提供了更出色的隔离性和 DNS 解析功能。
  • Host (主机网络): 使用 host 网络的容器直接共享宿主机的网络命名空间。这意味着容器的网络栈与宿主机完全一致,它不会获得自己的独立 IP,而是直接使用宿主机的服务和端口。虽然它避免了网络地址转换 (NAT) 从而带来了高性能,但也降低了网络隔离度。
  • None (无网络): 连接到 none 网络的容器是完全隔离的。除了本地回环接口 (loopback) 外,没有任何配置好的网络接口。除非你手动添加网卡,否则它们无法与任何其他容器或外部世界通信。这适用于不需要网络连接的纯计算任务,或用于极其高级的自定义网络配置。
  • Overlay (覆盖网络): 专为 Docker Swarm(一个容器编排工具)设计。Overlay 网络允许运行在不同 Docker 宿主机上的容器进行无缝通信,就好像它们在同一台机器上一样。

1.2 docker network 命令集

Docker 提供了一个专门的命令行接口 docker network 来管理网络。这个命令族允许你列出 (list)、检查 (inspect)、创建 (create)、连接 (connect)、断开 (disconnect) 和删除 (remove) 网络,让你对容器通信架构拥有细粒度的控制权。

1.3 查看与检查 Docker 网络

在创建新网络之前,先看看你的 Docker 守护进程中已经存在哪些网络是非常有用的。

查看现有网络

你可以使用 docker network ls 命令查看由 Docker 守护进程管理的所有网络列表。

docker network ls

此命令将输出一个类似如下的表格:

NETWORK ID     NAME      DRIVER    SCOPE
178f5a11d044   bridge    bridge    local
095388c3a7f8   host      host      local
f05ee0d98453   none      null      local
  • NETWORK ID: 网络的唯一标识符。
  • NAME: 适合人类阅读的网络名称。
  • DRIVER: 使用的网络驱动(例如:bridge, host, null)。
  • SCOPE: 指示网络是局部的 local(仅在当前 Docker 宿主机上可用)还是集群范围的 swarm(在 Swarm 集群的多个主机间可用)。

检查网络详情

要获取特定网络的详细信息(包括它的子网、网关以及当前连接了哪些容器),请使用 docker network inspect 命令。

docker network inspect bridge

这个命令会返回一段包含全面细节的 JSON 格式输出。例如,对于默认的 bridge 网络,你可能会看到关于它的 IPAM(IP 地址管理)配置、网关以及所有已连接容器的信息。

[
    {
        "Name": "bridge",
        "Id": "178f5a11d044...",
        "Created": "2023-10-26T10:00:00.000000000Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ],
            "Options": null
        },
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.name": "docker0"
        },
        "Labels": {}
    }
]

其中 Containers 部分特别有用,它显示了连接到该网络的所有容器的 ID、名称、IP 地址和 MAC 地址。这能让你快速排查连接问题或验证网络配置。

2. 创建自定义桥接网络 (User-Defined Bridge)

虽然 Docker 默认的 bridge 网络能应付简单场景,但在实际开发中,强烈建议为你的应用创建“自定义桥接网络”。自定义桥接网络提供了以下巨大优势:

  • 自动 DNS 解析: 在自定义网络上的容器,可以直接通过它们的名称互相解析(比如直接执行 ping my-db-container),而不需要去记住会动态变化的 IP 地址。默认的 bridge 网络不提供这种容器名到 IP 的自动解析功能。
  • 更好的隔离性: 每个自定义网络都是一个隔离的网络命名空间。这确保了网络 A 上的容器绝对无法直接与网络 B 上的容器通信,除非你明确地将它们连接起来。
  • 可移植性: 为某个应用定义的网络可以轻松地在不同环境中重新创建,提升了部署的一致性。

2.1 基础网络创建

要创建一个新的自定义桥接网络,使用 docker network create 命令,后跟你要取的网络名称。

docker network create my-app-network

这条命令会创建一个新的桥接网络,Docker 会自动为它分配一个随机的子网和网关。

2.2 自定义子网和网关

如果你需要对 IP 地址范围进行更精确的控制,可以使用 --subnet--gateway 参数来显式指定。当你需要将 Docker 网络与现有的公司基础设施集成,或者想避免 IP 冲突时,这非常有用。

docker network create --driver bridge --subnet 172.20.0.0/16 --gateway 172.20.0.1 custom-app-network
  • --driver bridge: 显式指定使用 bridge 驱动。虽然这是 docker network create 的默认行为,但写出来会更清晰。
  • --subnet 172.20.0.0/16: 定义该网络的 IP 地址范围。连接到此网络的所有容器都将获得这个范围内的 IP。
  • --gateway 172.20.0.1: 为此网络设置网关 IP 地址。

真实业务场景: 假设你有多个开发团队,分别在开发不同的微服务。为了防止 IP 冲突并确保边界清晰,每个团队都可以创建一个具有唯一子网的专属 Docker 网络(例如,团队 A 使用 172.20.0.0/16,团队 B 使用 172.21.0.0/16)。这在 Docker 容器需要访问配置了特定 IP 白名单的外部资源时特别好用。

假设场景: 某公司有一个运行在特定 IP 段(如 10.0.1.0/24)的老旧遗留系统,且该网段无法轻易更改。现在他们想容器化一个新应用,且这个新应用需要与老系统通信。通过创建一个自定义子网(避开老系统的网段)的 Docker 网络,他们就能确保当容器试图通过宿主机网络访问老系统时,不会发生 IP 路由冲突。

3. 将容器连接到网络

你可以选择在创建容器的那一刻就将其接入网络,也可以在容器运行后随时接入。

3.1 在创建容器时连接

最常用的方式是在执行 docker run 命令时使用 --network 参数。

docker run -d --name my-web-server --network my-app-network nginx:latest

在这个例子中,my-web-server 容器启动后会被立即连接到 my-app-network 网络,并从该网络的子网中获取一个 IP 地址。

3.2 连接已存在的容器

如果你有一个正在运行的容器,它当初启动时没有指定自定义网络(比如挂在默认的 bridge 上),你可以使用 docker network connect 将其接入新网络。

# 启动一个挂在默认网络的容器
docker run -d --name legacy-app alpine/git

# 将其连接到我们自定义的网络
docker network connect my-app-network legacy-app

现在,legacy-app 容器同时连接在两个网络上:默认的 bridge 网络和 my-app-network 网络。一个容器可以同时连接到多个网络,这非常适合用来为应用的不同层级搭建“桥梁”(例如,一个充当网关的容器可能需要同时连接暴露给外网的网络和内部后端服务专用的网络)。

4. 断开容器与删除网络

就像你可以随时连接网络一样,你也可以在不需要时断开连接并清理网络。

4.1 断开容器连接

要将容器从特定网络中移出,请使用 docker network disconnect 命令。

docker network disconnect my-app-network my-web-server

这会将 my-web-servermy-app-network 剥离。如果该容器还连着其他网络,那些连接不受影响。

4.2 删除自定义网络

要删除一个用户自定义网络,使用 docker network rm 命令。

docker network rm my-app-network

重点注意: 如果一个网络上还有任何容器连着,你是无法删除它的。在尝试删除之前,必须先断开所有容器的连接(或删除那些容器)。如果硬删,Docker 会返回一个错误,提示你先去断开它们。

5. 综合实战演练

让我们通过一个常见的实战场景走一遍全流程:构建一个包含 Web 前端(Nginx)和后端服务(返回简单消息)的极简 Web 应用。我们将使用自定义网络来实现它们之间的内部通信。

场景描述:带有后端的简单 Web 服务

我们的应用包含两部分:

  • 后端服务 (Backend): 一个运行 nginxdemos/hello 镜像的容器,它会输出一个简单的 "Hello from Nginx" 页面。我们将只在内部访问它。
  • 前端 Web 服务器 (Frontend): 一个 Nginx 容器,充当反向代理,将请求转发给我们的后端服务。

第一步:创建一个自定义网络

首先,为我们的应用创建一个专用的桥接网络。

# 创建一个名为 'web-tier-network' 的新桥接网络
docker network create web-tier-network

确认创建成功:

docker network ls

你应该能在列表中看到 web-tier-network

第二步:运行后端服务

现在,我们运行后端服务并将其接入刚创建的网络。注意,我们不会将这个后端的任何端口暴露给宿主机,因为它只需要被前端访问。

# 运行后端服务,连接到 'web-tier-network'
# '--name' 指定的名称将被前端用来做 DNS 解析
docker run -d --name backend-service --network web-tier-network nginxdemos/hello

验证容器是否运行并正确连接:

docker ps
docker network inspect web-tier-network

inspect 输出的 Containers 块中,你应该能看到 backend-service 被分配了 web-tier-network 内的一个 IP。

第三步:运行前端 Web 服务器 (Nginx 反向代理)

接下来,启动前端 Nginx。我们将配置它作为反向代理,把访问 80 端口的请求在内网转发给我们的 backend-service。同时,我们需要把这个前端 Nginx 的 80 端口暴露给宿主机的 8080 端口。

为了配置 Nginx,在当前目录下创建一个名为 nginx.conf 的配置文件,内容如下:

# nginx.conf
events {
    worker_connections 1024;
}
http {
    server {
        listen 80;
        location / {
            # 使用容器名称 'backend-service' 进行内部 DNS 解析
            # Docker 内部的 DNS 会把 'backend-service' 解析成它在 'web-tier-network' 上的 IP
            proxy_pass http://backend-service:80;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

现在,挂载这个配置文件,连接网络,并运行前端 Nginx 容器:

# 运行前端 Nginx,挂载我们写好的配置,
# 连接到 'web-tier-network',并将宿主机的 8080 映射给它
docker run -d --name frontend-nginx --network web-tier-network \
    -p 8080:80 \
    -v "$(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro" \
    nginx:latest

第四步:测试应用

打开浏览器或使用 curl 访问前端暴露的端口:

curl http://localhost:8080

你应该能看到来自 backend-service 的输出内容:

Hostname: <某段容器ID>
IP: 127.0.0.1
...

这个实战完美证明了:你的前端 Nginx 在 web-tier-network 网络上,仅仅通过后端的容器名称,就成功找到了后端服务并获取了内容。你既不需要知道后端的动态 IP,也无需把后端的端口暴露给外界。

第五步:清理环境

养成用完就清理资源的好习惯。

# 停止并删除容器
docker stop frontend-nginx backend-service
docker rm frontend-nginx backend-service

# 删除网络
# 只有当所有容器都断开/删除后,这一步才会成功
docker network rm web-tier-network