Docker 网络初探

序言

  随着容器化和微服务架构在企业中愈发普及,Docker 已成为现代应用交付的核心工具。然而,很多团队在成功将应用容器化之后,仍对 Docker 的网络机制感到困惑。尤其在生产环境中,我们常常会遇到一些实际问题,例如:

  • 容器之间如何高效互通?
  • 容器存在几种网络模式?
  • 服务应该选择何种模式?

  网络是容器运行的基础,不同的 Docker 网络模式直接影响着 通信方式、性能、安全性以及系统的扩展能力。网络选择不当,可能导致端口冲突、性能瓶颈、架构复杂化,甚至潜在的安全风险。

  本文将从实践角度出发,对 Docker 的各种网络模式进行系统梳理,并结合典型业务场景,提供切实可行的选型和部署策略。无论你是开发人员还是运维工程师,都能从中获得指导,帮助你在容器化架构设计中做出更合理、更高效的网络决策。

Docker 网络架构

  Docker 提供多种网络模式,不同模式代表不同的网络栈挂载方式和通信隔离能力:

网络模式 是否独立IP 是否共享宿主机端口 性能 隔离性 典型场景 常用程度
bridge(默认) 微服务、Web 应用 ⭐⭐⭐⭐⭐
host 🚀 高 网关、反向代理、高性能入口 ⭐⭐⭐⭐⭐
overlay 分布式、跨机器集群 ⭐⭐⭐
macvlan 工控、IoT、真实IP需求
container 同 host Sidecar、共享网络栈
none 🚀 最强 沙箱、安全审计、离线任务

bridge 模式(默认)

  bridge 模式是大多数部署场景的首选方式。每个容器获得一个在虚拟网桥下的独立 IP,通过 Docker 内部 DNS 实现服务互通。

适用场景

  • 同机容器需要互访
  • 端口映射灵活、有隔离要求
  • 一般 Web 应用、业务微服务

优势

  • 支持多实例部署
  • 服务之间隔离良好
  • 网络可控,易管理

劣势

  • 对外访问需端口映射
  • 需 NAT 转发,存在少量性能损耗

host 模式

  在 host 模式下,容器直接使用宿主机网络,没有虚拟网桥、没有端口映射,是性能最高的模式。

适用:

  • 高性能网络 I/O
  • Nginx / API Gateway
  • 监控 Agent(如 node-exporter)

不适用于:

  • 普通业务系统(易端口冲突)

overlay 模式

  用于 Swarm、Kubernetes 等分布式场景,通过 VXLAN 创建虚拟网络,使不同宿主机中的容器像在同一局域网一样通信。

适用场景

  • 集群部署
  • 跨主机容器间通信
  • 服务自动发现

优势

  • 对业务透明
  • 网络隔离良好
  • 天然支持分布式架构

劣势

  • 配置复杂度高
  • 比 host 性能稍弱

macvlan 模式(了解)

  macvlan 让 Docker 容器获得物理网络的真实 IP,而不是使用虚拟网桥。

适用场景

  • IoT、工业网络
  • 与物理设备直通通信
  • 必须让容器直接出现在物理网络上的系统

优势

  • 无 NAT、性能高
  • 容器与外部设备通信最自然

劣势

  • 网络设计复杂,对运维要求高
  • 主机与容器之间通信需要额外配置

none 模式(了解)

  完全禁用网络,只提供 loopback。

适用场景:

  • 沙盒执行场景
  • 离线数据处理
  • 高安全性测试环境

业务选型

业务类型 推荐模式 理由
普通微服务 bridge 隔离好、适合多实例
日志/监控 Agent host 需要访问宿主机网络
高性能入口(Nginx/Gateway) host 性能最优,无需 NAT
跨主机集群 overlay 分布式、自动发现
物理设备交互 macvlan 与局域网设备直通
安全沙箱 none 网络隔离最强

生产实践

  假设你的系统为以下架构构成:

  • Nginx(外网入口)
  • Nacos 注册发现
  • Gateway(微服务)
  • Auth(微服务)
  • System(微服务)
  • Bus 服务(微服务)

  如果是全部服务部署多台服务器,推荐都用 host 模式,如果都部署在一台服务器推荐如下,推荐如下:

服务 网络模式 原因
Nginx host 对外暴露 80/443,高性能
Nacos host 对外暴露 端口,高性能
微服务 bridge 微服务标准做法,可多实例

基础概念

  从前面的说明中,我们大概知道了 docker 的几种网络模式,但是,在实际的使用过程中,比如说我们:

  • 通过 docker run 命令启动了一个容器
  • 通过 docker-compose 配置并运行了多个容器服务

  这些容器启动之后,docker 网络是怎么起作用的,比如说我们可能会有以下疑问:

  • 网络互通:为什么 docker 容器可以和宿主机网络访问,可以访问外网?外网可以访问容器
  • 模式处理:容器以不同方式启动(docker run方式或者docker-compose方式 ),对应的 docker 网络创建了几个,是哪种模式,birdge,host?创建过程又发生了什么
  • 名称访问:为什么 docker-compose 的多个容器服务可以通过容器名的方式访问,docker run 方式的可以互相通过容器名访问嘛?如何使得 docker 容器能够通过容器名的方式访问?
  • 网络归属:怎么知道运行的 docker 容器加入了哪些网络?

网络互通

  Docker 容器之所以能够与宿主机网络通信,并访问外部网络(外网),主要归功于以下几个关键机制

容器网络与宿主机网络的连接 (Bridge 模式)

  Docker 默认使用的网络模式是 Bridge(桥接)模式。

docker0 桥接网卡

  当你安装 Docker 时,它会在宿主机上创建一个虚拟网桥,通常命名为 docker0。这个网桥充当一个虚拟交换机

  docker0 会被分配一个私有 IP 地址(例如 172.17.0.1),作为容器网络的网关

  Linux 系统上

1
2
3
4
5
6
7
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:b1:45:d9:cd txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

虚拟网卡对 (veth pair)

  每当你启动一个 Docker 容器时,Docker 都会为它创建一对 虚拟以太网卡(veth pair

  • 这对网卡的一端(通常命名如 eth0)被放置在容器的网络命名空间内。
  • 这对网卡的另一端被放置在宿主机(Host)的网络命名空间内,并被连接到宿主机上的 docker0 虚拟网桥上。

容器 IP 地址分配

  • 容器内部的 eth0 会从 docker0 网段(例如 172.17.0.0/16)中获取一个私有 IP 地址(例如 172.17.0.2)。
  • 容器的默认路由指向 docker0 的 IP 地址(例如 172.17.0.1)。

结果:容器之间的互访

  由于所有容器的虚拟网卡都连接在同一个 docker0 虚拟交换机上,它们就像连接在同一个局域网内的多台机器一样,可以直接通过彼此的私有 IP 地址进行通信。

容器访问外网 (NAT/IP Masquerade)

  容器要访问外网,涉及到将容器的私有 IP 地址转换为宿主机的公网 IP 地址的过程,这主要通过 NAT (网络地址转换)IP Masquerade(IP 伪装)实现。

iptables 规则

  Docker 会在宿主机上自动配置 Linux 的 iptables 防火墙规则,通常在 POSTROUTING 链中添加一条规则:

当数据包从容器网络 (172.17.0.0/16) 发出,并打算通过宿主机的公网接口(如 eth0)离开宿主机时,将该数据包的源 IP 地址从容器的私有 IP 转换为宿主机的公网 IP 地址

IP 伪装

  这条 iptables 规则实现了 源地址 NAT (SNAT)IP 伪装

  • 发送请求: 容器发出一个请求,源地址是 $172.17.0.2$,目标地址是外网服务器 $X$。
  • NAT 转换: 数据包到达宿主机,iptables 将源地址 $172.17.0.2$ 转换为宿主机的公网 IP $A$,并记录这个映射关系。
  • 离开: 数据包(源 IP 为 $A$,目标 IP 为 $X$)通过宿主机公网接口发送到外网。
  • 接收响应: 外网服务器 $X$ 将响应发回给宿主机 $A$。
  • 反向 NAT: 响应到达宿主机,iptables 根据记录的映射关系,将目标地址从宿主机 $A$ 转换为容器 $172.17.0.2$,并将数据包转发给容器。

结果:访问外网

  通过这个机制,对外网来说,容器发出的请求看起来就像是宿主机发出的,因此容器可以成功访问外网。

外网访问容器 (端口映射/Port Mapping)

  注意: 默认情况下,外网不能主动访问容器,因为容器使用的是私有 IP。如果需要外网访问容器内部的应用,就需要用到端口映射-p--publish 选项)。

  当你运行 docker run -p 8080:80 image_name 时,Docker 会自动添加 iptables 规则。这条规则的作用是:当有人访问宿主机的8080端口时,将流量转发到容器的80端口

模式处理

不同启动方式下的网络

  对于 docker 容器而言,可以会以不同方式启动(docker run方式或者docker-compose方式 ),这些不同的启动方式,对网络的处理会不太一样。

默认网络(docker run 方式)

  若用docker run -d nginx启动了一个 nginx 容器,如果没有指定--network,那么Docker 会让容器加入默认网络bridge(默认网桥 docker0),不会额外创建新的 bridge。

项目专属 bridge 网络(docker-compose 方式)

  如果存在一个正确的docker-compose.yml文件,之后以docker-compose up -d启动容器后,那么docker-compose会自动创建 一个新的自定义 bridge 网络,这个默认的默认网络名格式为<项目名>_default

  如何理解<项目名>_default呢?

<项目名>其实就是目录名,举几个例子:

  • 例如目录名是 myapp,则生成myapp_default网络
  • 例如目录名是 nginx(/data/nginx),则生成nginx_default网络
扩展──重复的目录名下的网络名称

  如果项目名重复了呢,比如我在/data/nginx/data2/nginx都通过docker-compose命令启动了nginx容器,此时他们的网络是什么名称,会重复嘛?

  答案是不会重复。

  docker-compose 不会只用目录名或 docker-compose.yml 所在路径名作为网络名。
它使用的是一个“项目名(project name)”,而项目名的取值有明确的优先级规则,所以即使你在/data/nginx/data2/nginx两个目录里都运行:docker-compose up -d,它们的网络名称 不会冲突,不会都叫 nginx_default。

  docker-compose的 Project Name 的优先级如下:

  • 显式环境变量:COMPOSE_PROJECT_NAME
  • 命令参数:docker-compose -p
  • 目录名(最终兜底使用)

  也就是说:你运行命令时所在目录名,只有在你没指定前两项时,才被用作项目名。

  如果遇到例子中的情况,Compose 会自动检测冲突,并自动使用一个唯一名称!

  实际步骤:

  • Compose 发现 nginx_default 网络已存在
  • 第二个 compose 项目无法复用这个网络(网络包含不同的容器,会冲突)
  • Compose 会自动生成一个新的唯一网络名,例如:
1
2
nginx_default
nginx_default_2

或者

1
2
nginx_default
nginx_default_1

  这取决于 Docker 的版本,但总之 不会重名。

小结

启动方式 默认网络 网络是否新建? 类型
docker run bridge ❌ 不新建 bridge(docker0)
docker-compose myapp_default ✅ 会新建 自定义 bridge

docker-compose 为什么要新建网络?

  主要原因:

  • ① 服务间可以用容器名互相访问:因为 docker-compose 默认启用了 DNS(embedded DNS),每个服务名被注册为一个可解析的主机名
  • ② 避免多个项目互相污染:比如你有两个 compose 项目 A、B,则 A 内部的 redis:6379 不会被 B 访问到,除非明确加入同一个网络。:
    • A 项目网络:a_default
    • B 项目网络:b_default
  • ③ 支持多网络配置(更灵活)

host 模式是什么?什么时候用?

  host 模式比较常用于高性能网络或服务需要真实 IP 的业务场景。

  如果你运行:

1
docker run --network host nginx

  特点:

  • 不使用 bridge,不使用 docker0
  • 容器直接跟宿主机共享 network namespace
  • 容器没有独立 IP(使用宿主机 IP)
  • 宿主机端口冲突会导致容器无法运行

  在 docker-compose:

1
2
3
services:
app:
network_mode: host

⚠️ 注意:host 模式下 docker-compose 不会创建默认网络。

启动过程中到底发生了什么?(底层步骤)

  这里我按顺序详细讲下 Docker 在创建容器时的真实流程:


docker run(bridge 模式)发生了什么?

  1. 创建容器的 Network Namespace
  2. 创建一对 veth pair
  3. veth1 → 放入容器(成为 eth0)
  4. veth2 → 接入 docker0 网桥
  5. 为 container eth0 分配 IP(172.17.x.x)
  6. 设置路由,让容器默认路由到 docker0
  7. 设置 NAT 规则(iptables MASQUERADE)
  8. 容器能访问宿主机、内网、外网

  bridge 网络本质:

1
容器 <--veth--> docker0 <--路由/NAT--> 宿主机/外网

docker-compose(会新建自定义 bridge)发生了什么?

  启动前:

  1. 查看 compose 项目是否已有 <name>_default
  2. 若无,创建一个新的自定义 bridge
  3. 创建网段(通常 172.18.x.x)
  4. 启动每个 service 时:
    • 创建 veth pair
    • 将一端加入新建 bridge
    • 为容器分配 IP
    • 注册容器 DNS 名称(服务名)
  5. 服务之间可以通过 service 名互相解析并访问

  自定义网络特点:

  • 内置 DNS
  • 不用使用 docker0(干净、隔离)
  • 支持多网络拓扑

名称访问

  为什么 docker-compose 的多个容器服务可以通过容器名的方式访问,docker run 方式的可以互相通过容器名访问嘛?如何使得 docker 容器能够通过容器名的方式访问?

  我们下面来探讨探讨。

为什么 docker-compose 的容器可以通过“容器名/服务名”互相访问?

  因为 docker-compose 会自动创建一个自定义 bridge 网络,该网络内带有内置的 DNS(Embedded DNS),每个服务名/容器名都会自动注册到这个 DNS 中。

  例如:

1
2
3
4
5
services:
app:
image: app
redis:
image: redis

  compose 会做三件事:

  1. 创建网络:<project>_default
  2. 将两个容器加入同一个网络
  3. 自动为容器注册 DNS 记录:
1
2
app      → 172.20.0.2
redis → 172.20.0.3

  因此你可以直接在 app 容器里访问http://redis:6379

docker run 的容器是否能通过容器名互相访问?

  答案:默认情况下不可以,但是配置后可以,只需要满足前提条件

前提:这些容器必须在“同一个用户自定义的 bridge 网络”中。

  具体解释如下,默认情况下(使用 docker0),不能通过容器名访问,如果这样创建:

1
2
docker run -d --name app1 nginx
docker run -d --name app2 redis

  这两个容器都加入 bridge(docker0) 网络,但 docker0 没有内置 DNS,即:

  • 容器能通过 IP 互相访问
  • 不能通过容器名访问

  因此通过ping app2在 app1 里会失败。

自定义网络支持容器互相访问

  如果想让 docker run 的容器支持“容器名访问”,需要用户自定义一个网络

  例如创建自定义网络:

1
docker network create my-net

  然后启动容器并加入该网络:

1
2
docker run -d --name app1 --network my-net nginx
docker run -d --name app2 --network my-net redis

  这时你会看到:

1
docker network inspect my-net

  里面会有:

1
2
3
4
5
6
7
8
"Containers": {
"app1": {
"IPv4Address": "172.18.0.2"
},
"app2": {
"IPv4Address": "172.18.0.3"
}
}

  现在执行ping app2或者curl http://app2:6379都能成功。

扩展疑问:为什么 docker-compose 默认就有 DNS?

  因为 docker-compose 一定会创建“自定义 bridge”,而自定义 bridge 自带 DNS

  但 docker run 默认使用的 bridge(docker0) 是 Docker 的“三个默认网络之一”,这个网络为了兼容历史原因:

  • 不自动注册容器名 DNS
  • 不提供容器名互访能力

  总结一句话:

docker-compose 能用容器名互相访问,是因为它会自动创建自定义 bridge 网络(自带 DNS)。
docker run 默认不能容器名互访,但如果把多个容器加入同一个自定义网络,也可以通过容器名访问。

小结

  docker-compose 与 docker run 的根本区别

特性 docker run 默认 docker run + 自定义网络 docker-compose 默认
网络类型 bridge(docker0) 自定义 bridge 自定义 bridge
是否有 DNS 支持 ❌ 无 ✔ 有 ✔ 有
能否用容器名互访 ❌ 不能 ✔ 能 ✔ 能
网络自动创建 ❌ 不创建 ❌ 需手动创建 ✔ 自动创建

网络归属

  要查看 Docker 容器正在使用哪些网络,有以下方法。

查看容器详细信息(最常用)

1
docker inspect <容器名或ID>

  然后在输出中查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"MacAddress": "de:7f:7b:66:62:01",
"DriverOpts": null,
"GwPriority": 0,
"NetworkID": "f6adcbe9496ff48fc955d521440248142586416e96a0ee2f13ff53e395474626",
"EndpointID": "7ab0444c01c32849c6332e26266e2d467186a5243f194ded409a82dafb4180b1",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"DNSNames": null
}
}
}

  这表示容器加入了 bridge 网络。

直接查看容器的网络名

1
docker inspect -f '{{json .NetworkSettings.Networks}}' <容器名>

  输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"MacAddress": "de:7f:7b:66:62:01",
"DriverOpts": null,
"GwPriority": 0,
"NetworkID": "f6adcbe9496ff48fc955d521440248142586416e96a0ee2f13ff53e395474626",
"EndpointID": "7ab0444c01c32849c6332e26266e2d467186a5243f194ded409a82dafb4180b1",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"DNSNames": null
}
}

查看容器连接的网络列表(清晰易读)

1
docker container inspect <容器名> | grep -A 5 Networks

  输出示例:

1
2
3
4
5
6
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"MacAddress": "de:7f:7b:66:62:01",

查看网络详情,确认有哪些容器在使用它

  列出所有 docker 网络:

1
docker network ls

1
2
3
4
5
6
7
8
9
NETWORK ID     NAME                    DRIVER    SCOPE
67c7b50d427a bitwarden_default bridge local
f6adcbe9496f bridge bridge local
910dfa5b08ce docker_default bridge local
ebcc87b90cbe elasticsearch_default bridge local
451c37f9c4a3 host host local
e7188136b92c kafka_default bridge local
088e42e89e3d none null local
b0ea2cf35751 rocketmq_rocketmq bridge local

查看 docker compose 项目网络

1
docker-compose config | grep networks -A 20
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
networks:
default: null
ports:
- mode: ingress
target: 80
published: "80"
protocol: tcp
- mode: ingress
target: 443
published: "443"
protocol: tcp
restart: unless-stopped
volumes:
- type: bind
source: /data/nginx/logs
target: /var/log/nginx
bind:
create_host_path: true
- type: bind
source: /data/nginx/front
target: /usr/share/nginx/html

networks:
default:
name: nginx_default

进阶概念

  前面我们知道了一些基础知识,但是在实际的使用过程中,我们可能需要:

  • 自定义网络名:不想使用 docker-compose 创建的默认网络,希望自定义一个网络名称
  • 加入已有网络:想对docker-compose新启动的服务,都加入到已有已创建过的网络中
  • 跨机器网络通信:希望横跨多个机器,组建一个新的网络,允许加入网络中的机器互相通信

  这些,都需要使用到networks配置,下面为一份配置示例:

1
2
3
4
5
6
7
8
9
10
11
networks:
mynet:
driver: bridge
name: my_custom_network # ❗ 实际 docker 网络名
driver_opts:
com.docker.network.bridge.name: mybridge0
ipam:
driver: default
config:
- subnet: 172.30.0.0/16
gateway: 172.30.0.1

  其各参数含义如下:

  • driver:网络类型,常见:bridge、host、overlay、macvlan
  • name:网络的真实名称(覆盖 <project>_network默认命名)
  • external:指定是否使用外部网络,不由 compose 创建
  • driver_opts:给 driver 传参数,比如 bridge 的 MTU、子网等
  • ipam:指定 IP 地址池配置,包括子网 subnet、gateway
  • attachable:在 swarm overlay 网络中允许独立容器加入
  • labels:给网络打标签

  这些参数,我们下面来根据具体例子进行了解。

自定义网络名

  docker-compse 如果想自定义网络,可以使用name参数。

  比如以下配置中创建了一个my_custom_network网络,test_nginx容器通过mynet逻辑名称加入了my_custom_network网络(如果这个网络不存在,会自动创建)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
test_nginx:
image: nginx:stable-alpine
container_name: test_nginx
ports:
- "180:80" # HTTP
- "1443:443" # HTTPS
volumes:
- ./logs:/var/log/nginx
networks:
- mynet

networks:
mynet:
name: my_custom_network

加入已有网络

  通过external参数,可以加入已有网络,比如下面的配置中,demo_nginx容器加入了已有的city_net网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
demo_nginx:
image: nginx:stable-alpine
container_name: demo_nginx
ports:
- "180:80" # HTTP
- "1443:443" # HTTPS
volumes:
- ./logs:/var/log/nginx
networks:
- city_net

networks:
city_net:
external: true

  注意,如果已有网络不存在,则容器会启动失败,可以通过下面的命令提前创建网络:

创建网络命令

1
docker network create city_net

也可以指定 driver:

1
docker network create -d bridge city_net

扩展知识

逻辑网络的理解

  前面我们的配置中通过mynet作为逻辑网络名,对于mynet而言,其会在不同的网络配置下有不同的含义,有时候它代表逻辑网络名,但有时候它又代表实际网络名。

  我们来看个例子,现在有下面 A、B两个docker-compose配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services:
a_nginx:
image: nginx:stable-alpine
container_name: a_nginx
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
volumes:
- ./logs:/var/log/nginx
networks:
- mynet

networks:
mynet:
driver: bridge
name: my_custom_net
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
b_nginx:
image: nginx:stable-alpine
container_name: b_nginx
ports:
- "180:80" # HTTP
- "1443:443" # HTTPS
volumes:
- ./logs:/var/log/nginx
networks:
- mynet

networks:
mynet:
external: true

  配置完成后,我们通过docker-compose up -d命令启动。

  A 配置中的mynet代表对应内部网络标识符,a_nginx 通过mynet加入了my_custom_net网络,而在 B 配置中,代表 b_nginx 加入了外部网络mynet,很明显上面我们没有创建这个网络,所以会加入失败。

  如果想要成功,需要修改为如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
b_nginx:
image: nginx:stable-alpine
container_name: b_nginx
ports:
- "180:80" # HTTP
- "1443:443" # HTTPS
volumes:
- ./logs:/var/log/nginx
networks:
- my_custom_net

networks:
my_custom_net:
external: true

external 取值 false 的网络

  如果你的docker-compose.yaml网络配置中externalfalse,比如下面的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
b_nginx:
image: nginx:stable-alpine
container_name: b_nginx
ports:
- "180:80" # HTTP
- "1443:443" # HTTPS
volumes:
- ./logs:/var/log/nginx
networks:
- my_custom_net

networks:
my_custom_net:
external: false

  则创建的网络名称为<project_name>_<network_key>,其中:

  • project_name = 当前目录名(或 -p 指定的项目名称)
  • network_key = YAML 中的 network 名(例如 city_net)

  上面创建的网络为b_nginx_my_custom_netb_nginx为对应目录

1
2
3
4
[root@localhost b_nginx]# docker-compose up -d
[+] Running 2/2
✔ Network b_nginx_my_custom_net Created
✔ Container b_nginx Started

跨机器网络通信

  如果想跨机器进行网络通信,Docker 中可以使用 Docker Swarm 进行处理,我们单独使用一节文章进行介绍。

Docker Swarm

  Docker Swarm 是 Docker 官方提供的原生集群与容器编排工具,作用类似于 Kubernetes(K8s),但比 K8s 更轻量、更易上手。

简介

  Docker Swarm 是 Docker 内置的 分布式集群管理与服务编排系统。
  你可以把多个 Docker 主机(节点)组成一个“集群(Swarm)”,然后像在单台机器上一样部署服务,它会自动:

  • ✔ 负载均衡
  • ✔ 自愈
  • ✔ 滚动更新
  • ✔ 服务扩容缩容
  • ✔ 多节点容器调度
  • ✔ 服务发现 / 内置 DNS

架构

  在 Docker Swarm 集群中,物理机(或虚拟机)被称为 Node(节点)。节点存在两种角色:

角色 描述
Manager 管理集群状态,调度任务,类似 K8s 的 Master
Worker 负责运行容器,不参与调度

  通常数量如下:

  • manager 节点:3~5 个(奇数,为了 Raft 选举)
  • worker 节点:若干,可以很多

Manager Node (管理节点) - “大脑”

  • 职责:负责集群的管理、任务调度(决定哪个容器跑在哪个机器上)、维护集群状态。
  • 特性
    • Manager 节点默认情况下也会运行业务容器(除非你显式禁止)。
    • 为了高可用,生产环境通常建议有 3 个或 5 个 Manager(奇数个),以防止单点故障(利用 Raft 算法选举)。

Worker Node (工作节点) - “干活的”

  • 职责:只负责接收并执行 Manager 分派下来的任务(容器),并上报状态。
  • 特性:Worker 挂了不影响集群的大脑,Manager 会自动把它上面的任务转移到其他活着的节点上。

核心概念

概念 说明
Node 集群节点(一台 Docker 主机)
Service “服务”,用来定义容器模板(镜像、端口、环境变量)
Task 容器的具体实例
Stack 多服务应用(类似 docker-compose)
Secret 密文管理
Overlay Network 跨主机通信网络

思维过渡

  从单机 Docker 到 Swarm,你的思维模式需要做一个转换:

单机 Docker (Docker Compose) Docker Swarm (Docker Stack) 说明
Container (容器) Task (任务) Swarm 中,最小调度单位叫 Task,一个 Task 里面包含一个容器
Service (服务) Service (服务) 这是 Swarm 的核心。比如“Nginx 服务”,它定义了用什么镜像、要启动几个副本
docker-compose docker stack Stack 是一组关联的 Service(比如 Web+DB),用一个 YAML 文件统一部署

  举个例子,当你告诉 Swarm:“我要启动一个 Nginx 服务,副本数是 3”。

  1. Service:是你的命令(“我要 3 个 Nginx”)。
  2. Manager:创建 3 个 Task(任务槽位)。
  3. Worker:领取 Task,在各自机器上启动真正的 Container

搭建集群系统

  对于 Swarm 集群系统的搭建,非常简单,只需要按步骤执行两个命令。

步骤

初始化 Swarm(在 Manager 上)

1
docker swarm init --advertise-addr 192.168.1.10

输出类似:

1
docker swarm join --token SWMTKN-xxxx 192.168.1.10:2377

加入 Worker 节点

  在另一台服务器执行上面的 join 命令,加入到同一个集群。

扩展──两个命令具体做了什么?

  docker swarm init:创建整个 Swarm 集群的大脑(Manager 节点)

  当你在第一台服务器上执行:

1
docker swarm init --advertise-addr 192.168.1.10

  此命令会做以下事情:

  • 1)创建 Swarm 集群,让这台服务器成为 Swarm Manager(管理节点),即:这台服务器成了整个集群的大脑。它会负责:
    • 分配任务(调度)
    • 跟踪节点状态
    • 服务发现
    • 管理 Overlay 网络
    • 心跳监测
  • (2)生成加入集群的 token,比如docker swarm join --token SWMTKN-1-xxxxxxx 192.168.1.10:2377相当于一个“集群访问密码 + 地址”。
  • (3)开启 Swarm 端口(2377、7946、4789):Swarm 自动配置集群通信的端口,overlay 网络能跨服务器通信就是通过 4789/VXLAN 实现的:
端口 用途
2377/tcp 管理通信(Manager ↔ Worker)
7946/tcp/udp 节点发现
4789/udp Overlay 网络数据传输(VXLAN)

  docker swarm join xxxx:加入集群成为 Worker 节点

  此命令会做以下事情:

  • 1)注册到 Manager:Worker 节点向 Manager 汇报,“我加入了,请给我分配任务”,并且会定期发送心跳
  • 2)自动加入 Swarm 的底层 Mesh 网络,Swarm 会让这台 Worker 自动加入:
    • 分布式 Gossip 网络(7946):用于节点发现与同步。
    • Overlay 网络(VXLAN over 4789):这是跨主机 Docker 网络的核心!
  • 3)接受 Manager 的任务调度,例如:
    • 部署容器
    • 拉取镜像
    • 健康检查失败则重新部署

  简单概括成一句话,就是swarm init + swarm join = 在多台服务器上构建了一个 分布式集群 + 分布式网络 + 容器调度平台,不仅仅是一般意义上的网络互联,而是完整的集群系统。

形象理解

  把多台服务器连成了一个 虚拟大服务器

1
2
3
4
5
6
7
8
9
10
11
┌───────────────────────────────────────┐
│ Swarm 集群(逻辑上是一个超大服务器) │
│ │
│ Manager (192.168.1.10) │
│ Worker1 (192.168.1.11) │
│ Worker2 (192.168.1.12) │
│ │
│ 容器们通过 Overlay 网络互通 │
│ nacos → gateway → bus-service │
│ 互相用“服务名”访问,不关注IP变化 │
└───────────────────────────────────────┘

Stack 语法说明

  docker stack 使用的配置文件格式也是 YAML,和 docker-compose.yml 几乎一模一样(version 3+)。

  最大的区别在于: Swarm 忽略 build 指令(你需要提前构建好镜像推送到仓库),并新增了 deploy 模块。

  deploy 模块是 Swarm 的精髓,它告诉集群“怎么跑这个服务”。

示例配置

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
version: '3.8'

services:
business-service:
image: my-registry/business:v1
deploy:
# 1. 运行模式
mode: replicated # (默认) 指定副本数。另一种是 'global' (每个节点强制跑一个)
replicas: 2 # 启动 2 个实例

# 2. 放置约束 (Placement Constraints) - 指定跑在哪
placement:
constraints:
# 常用约束条件:
# - node.role == manager (只跑在管理节点)
# - node.hostname == host-b (只跑在特定主机名)
# - node.labels.zone == backend (只跑在打标签的节点,推荐!)
- node.labels.zone == backend

# 偏好 (Preferences) - 尽量均匀分布
preferences:
- spread: node.labels.zone

# 3. 资源限制 (Resources) - 防止服务把机器资源吃光
resources:
limits:
cpus: '0.50' # 最多使用 50% 的 CPU
memory: 512M # 最多使用 512M 内存
reservations: # 预留资源 (保证机器至少有这么多资源才分配)
cpus: '0.10'
memory: 128M

# 4. 重启策略 (Restart Policy)
restart_policy:
condition: on-failure # 只有失败时才重启 (可选: any, none)
delay: 5s # 重启间隔
max_attempts: 3 # 最多尝试重启 3 次
window: 120s # 在 120s 内判断是否重启成功

# 5. 更新策略 (Update Config) - 核心功能:滚动更新
update_config:
parallelism: 1 # 每次更新 1 个容器 (逐步替换)
delay: 10s # 更新完一个,等待 10s 再更新下一个
order: start-first # 先启动新容器,健康后再停掉旧容器 (实现零停机)
failure_action: rollback # 如果更新失败,自动回滚到上一个版本

networks:
- my_overlay_net

参数详解

replicas─副本数

  Swarm 会自动监控这 2 个副本。如果机器 B 宕机导致少了一个,Swarm 会立刻在机器 C 上补齐一个,确保总数始终是 2。

placement─放置

  这是你之前问到的 A/B/C 分离部署的关键。

  • Constraints (硬约束): 必须满足条件,否则服务起不来。
  • Preferences (软策略): 尽量满足。比如 spread 可以让服务尽量分散在不同机房。

update_config ─滚动更新

  这是生产环境的神器。

  • 当你修改镜像版本 v1 -> v2 并重新部署时,Swarm 不会一次性杀掉所有服务。
  • 它会根据 parallelism 配置,先升一个,观察 10s,没问题再升下一个。
  • 配合 order: start-first,可以做到用户无感知的平滑发布。

常用命令

命令 说明
docker node ls 查看所有机器状态
docker node update –label-add zone=backend worker-1 给节点打标签(Map K-V 格式)
docker swarm join-token manager 查看 Manager 加入 Token
docker swarm join-token worker 查看 Worker 加入 Token
docker stack deploy -c docker-swarm.yml <STACK_NAME> 创建或更新服务(修改 yaml 后再次运行此命令即可)
docker service ls 查看所有服务的副本数状态
docker stack rm <STACK_NAME> 删除 <STACK_NAME> 下的所有服务
docker service scale <SERVICE_NAME>=0 停止一个服务(副本数设置为0)
docker service ps <SERVICE_NAME> 查看某个服务具体跑在哪台机器上,以及历史运行记录
docker service logs -f <SERVICE_NAME> 查看聚合日志(包含了所有副本的日志)
docker service scale <STACK_NAME>=5 弹性伸缩,瞬间将 STACK_NAME 服务扩容到 5 个副本
docker service ps <SERVICE_NAME> –no-trunc 查询 <SERVICE_NAME> 服务详情

  其他命令:

  • 查询所有节点及标签(批量)
1
docker node ls -q | xargs -n 1 docker node inspect -f '{{ .Description.Hostname }} {{ json .Spec.Labels }}'

实战案例

  假设我们现在存在一个以下技术栈的服务架构:

  • Nginx:独立部署
  • Nacos:独立部署
  • MySQL:独立部署
  • Redis:独立部署
  • 多个微服务:Gateway、Auth、System、File、Gen

  为了保证高性能,其他中间件单独部署,而对于微服务,可以使用 Docker Swarm 方式部署。

  具体落地步骤如下:

  • 编写 stack 文件
    • 相关服务想跑在固定的节点使用命令打标签
  • 执行命令启动

编写 stack 文件

  以下为一个swarm-stack-service.yml文件示例:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
x-logging: &loggingStrategy
logging:
driver: "json-file"
options:
max-size: "512m"
max-file: "3"
x-host: &host
extra_hosts:
- "nacos:192.168.3.42"

networks:
ruoyi-net:
driver: overlay

services:
ruoyi-gateway:
image: 192.168.3.36:28080/test/ruoyi-gateway:latest
environment:
JAVA_OPTS: "-Xms512m -Xmx512M -Dfile.encoding=UTF-8"
volumes:
- /data/ruoyi/logs/gateway:/logs
- /etc/localtime:/etc/localtime:ro
- /usr/share/fonts:/usr/share/fonts/
networks:
- ruoyi-net
ports:
- "8080:8080"
deploy:
replicas: 2
placement:
constraints:
- node.labels.zone == backend
restart_policy:
condition: on-failure # 失败自动重启
delay: 5s
max_attempts: 3
window: 120s
resources:
limits:
memory: 666M
cpus: '0.50'
reservations:
memory: 256M
cpus: '0.20'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
<<: [*host, *loggingStrategy]

ruoyi-auth:
image: 192.168.3.36:28080/test/ruoyi-auth:latest
environment:
JAVA_OPTS: "-Xms512m -Xmx512M"
volumes:
- /data/ruoyi/logs/auth:/logs
- /etc/localtime:/etc/localtime:ro
- /usr/share/fonts:/usr/share/fonts/
networks:
- ruoyi-net
deploy:
replicas: 2
placement:
constraints:
- node.labels.zone == backend
restart_policy:
condition: on-failure # 失败自动重启
delay: 5s
max_attempts: 3
window: 120s
resources:
limits:
memory: 666M
cpus: '0.50'
reservations:
memory: 256M
cpus: '0.20'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9200/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
<<: [*host, *loggingStrategy]

ruoyi-system:
image: 192.168.3.36:28080/test/ruoyi-modules-system:latest
environment:
JAVA_OPTS: " -Xms512m -Xmx512M -Dfile.encoding=UTF-8 -Duser.country=CN -Duser.language=zh"
volumes:
- /data/ruoyi/logs/system:/logs
- /etc/localtime:/etc/localtime:ro
- /usr/share/fonts:/usr/share/fonts/
networks:
- ruoyi-net
deploy:
replicas: 2
placement:
constraints:
- node.labels.zone == backend
restart_policy:
condition: on-failure # 失败自动重启
delay: 5s
max_attempts: 3
window: 120s
resources:
limits:
memory: 666M
cpus: '0.50'
reservations:
memory: 256M
cpus: '0.20'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9201/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
<<: [*host, *loggingStrategy]

ruoyi-file:
image: 192.168.3.36:28080/test/ruoyi-modules-file:latest
environment:
JAVA_OPTS: " -Xms512m -Xmx512M -Dfile.encoding=UTF-8 -Duser.country=CN -Duser.language=zh"
volumes:
- /data/ruoyi/logs/file:/logs
- /etc/localtime:/etc/localtime:ro
- /usr/share/fonts:/usr/share/fonts/
networks:
- ruoyi-net
deploy:
replicas: 2
placement:
constraints:
- node.labels.zone == backend
restart_policy:
condition: on-failure # 失败自动重启
delay: 5s
max_attempts: 3
window: 120s
resources:
limits:
memory: 666M
cpus: '0.50'
reservations:
memory: 128M
cpus: '0.10'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9300/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
<<: [*host, *loggingStrategy]

ruoyi-gen:
image: 192.168.3.36:28080/test/ruoyi-modules-gen:latest
environment:
JAVA_OPTS: " -Xms512m -Xmx512M -Dfile.encoding=UTF-8 -Duser.country=CN -Duser.language=zh"
volumes:
- /data/ruoyi/logs/gen:/logs
- /etc/localtime:/etc/localtime:ro
- /usr/share/fonts:/usr/share/fonts/
networks:
- ruoyi-net
deploy:
replicas: 2
placement:
constraints:
- node.labels.zone == backend
restart_policy:
condition: on-failure # 失败自动重启
delay: 5s
max_attempts: 3
window: 120s
resources:
limits:
memory: 666M
cpus: '0.50'
reservations:
memory: 256M
cpus: '0.20'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9202/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
<<: [*host, *loggingStrategy]

执行启动命令

1
docker stack deploy -c swarm-stack-service.yml ruoyi

踩🕳事项

多机器 hostname 相同问题处理

  如果是用虚拟机构建的服务器,可能多个服务器的 hostname 会一致,比如下面这三个虚拟机服务器的 hostname 在未设置的情况下就是一样的:

1
2
3
4
5
[root@localhost docker]# docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
quayexceico1zvpcdtzmrdt6c * localhost.localdomain Ready Active Leader 26.1.4
r552gwq09t8e2k5f2mb5bo66l localhost.localdomain Ready Active Reachable 26.1.4
ryukyi1ohif76652ww50xqc18 localhost.localdomain Ready Active Reachable 26.1.4

  这会导致一个问题,即docker node update --label-add type=backend worker-1无法执行,因为worker-1用的是机器的 hostname。

  那么这个问题需要如何处理呢?

解决方案

  在 Linux(CentOS 或 RHEL)修改 hostname,比如 master、worker 节点分别设置:

1
2
# master 节点
hostnamectl set-hostname swarm-master

1
2
3
# worker 节点设置
hostnamectl set-hostname swarm-worker1
hostnamectl set-hostname swarm-worker2

镜像拉取

  docker stack deploy 执行的容器镜像默认不会自动拉取(Pull)镜像,它只负责调度任务。它期望镜像要么在所有节点上已存在,要么能够从可访问的仓库中拉取。

  如果镜像在目标节点上不存在,并且该节点无法访问到 Docker Hub 或其他私有仓库,那么任务将一直处于 Rejected 或 Pending 状态,并且会不断失败。

  解决方案有两种:

  • 提前在各个服务器准备好镜像
  • 启动命令增加--with-registry-auth参数,此参数可以让 Docker Swarm 在部署/更新服务时,把私有镜像仓库的登录凭据分发给所有节点使用的参数

  建议使用第二种方案,以之前实战例子命令为例,修改为如下:

1
docker stack deploy -c swarm-stack-service.yml ruoyi --with-registry-auth

目录提前创建

  docker stack deploy 时容器映射的目录需要提前创建,因为服务在哪个节点运行,bind mount 就要求那个节点上路径必须存在。

  因此,如下相对路径的配置不建议配置在 stack 文件中:

1
2
3
4
volumes:
- ./logs/gateway:/logs
- ./front:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf

版本号问题

  上面的镜像版本号全是 latest,这对回滚存在影响,生产建议加上版本号进行发版,方便代码回滚。

文章信息

时间 说明
2025-11-07 初稿
2025-11-21 增加 Docker Swarm 一节
0%