序言
随着容器化和微服务架构在企业中愈发普及,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
7docker0: 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 | nginx_default |
或者1
2nginx_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
- A 项目网络:
- ③ 支持多网络配置(更灵活)
host 模式是什么?什么时候用?
host 模式比较常用于高性能网络或服务需要真实 IP 的业务场景。
如果你运行:
1 | docker run --network host nginx |
特点:
- 不使用 bridge,不使用 docker0
- 容器直接跟宿主机共享 network namespace
- 容器没有独立 IP(使用宿主机 IP)
- 宿主机端口冲突会导致容器无法运行
在 docker-compose:
1 | services: |
⚠️ 注意:host 模式下 docker-compose 不会创建默认网络。
启动过程中到底发生了什么?(底层步骤)
这里我按顺序详细讲下 Docker 在创建容器时的真实流程:
docker run(bridge 模式)发生了什么?
- 创建容器的 Network Namespace
- 创建一对 veth pair
- veth1 → 放入容器(成为 eth0)
- veth2 → 接入 docker0 网桥
- 为 container eth0 分配 IP(172.17.x.x)
- 设置路由,让容器默认路由到 docker0
- 设置 NAT 规则(iptables MASQUERADE)
- 容器能访问宿主机、内网、外网
bridge 网络本质:
1 | 容器 <--veth--> docker0 <--路由/NAT--> 宿主机/外网 |
docker-compose(会新建自定义 bridge)发生了什么?
启动前:
- 查看 compose 项目是否已有
<name>_default - 若无,创建一个新的自定义 bridge
- 创建网段(通常 172.18.x.x)
- 启动每个 service 时:
- 创建 veth pair
- 将一端加入新建 bridge
- 为容器分配 IP
- 注册容器 DNS 名称(服务名)
- 服务之间可以通过 service 名互相解析并访问
自定义网络特点:
- 内置 DNS
- 不用使用 docker0(干净、隔离)
- 支持多网络拓扑
名称访问
为什么 docker-compose 的多个容器服务可以通过容器名的方式访问,docker run 方式的可以互相通过容器名访问嘛?如何使得 docker 容器能够通过容器名的方式访问?
我们下面来探讨探讨。
为什么 docker-compose 的容器可以通过“容器名/服务名”互相访问?
因为 docker-compose 会自动创建一个自定义 bridge 网络,该网络内带有内置的 DNS(Embedded DNS),每个服务名/容器名都会自动注册到这个 DNS 中。
例如:
1 | services: |
compose 会做三件事:
- 创建网络:
<project>_default - 将两个容器加入同一个网络
- 自动为容器注册 DNS 记录:
1 | app → 172.20.0.2 |
因此你可以直接在 app 容器里访问http://redis:6379
docker run 的容器是否能通过容器名互相访问?
答案:默认情况下不可以,但是配置后可以,只需要满足前提条件
前提:这些容器必须在“同一个用户自定义的 bridge 网络”中。
具体解释如下,默认情况下(使用 docker0),不能通过容器名访问,如果这样创建:
1 | docker run -d --name app1 nginx |
这两个容器都加入 bridge(docker0) 网络,但 docker0 没有内置 DNS,即:
- 容器能通过 IP 互相访问
- 但 不能通过容器名访问
因此通过ping app2在 app1 里会失败。
自定义网络支持容器互相访问
如果想让 docker run 的容器支持“容器名访问”,需要用户自定义一个网络
例如创建自定义网络:
1 | docker network create my-net |
然后启动容器并加入该网络:
1 | docker run -d --name app1 --network my-net nginx |
这时你会看到:
1 | docker network inspect my-net |
里面会有:
1 | "Containers": { |
现在执行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 | "Networks": { |
查看网络详情,确认有哪些容器在使用它
列出所有 docker 网络:1
docker network ls
1 | NETWORK ID NAME DRIVER SCOPE |
查看 docker compose 项目网络
1 | docker-compose config | grep networks -A 20 |
1 | networks: |
进阶概念
前面我们知道了一些基础知识,但是在实际的使用过程中,我们可能需要:
- 自定义网络名:不想使用 docker-compose 创建的默认网络,希望自定义一个网络名称
- 加入已有网络:想对
docker-compose新启动的服务,都加入到已有已创建过的网络中 - 跨机器网络通信:希望横跨多个机器,组建一个新的网络,允许加入网络中的机器互相通信
这些,都需要使用到networks配置,下面为一份配置示例:1
2
3
4
5
6
7
8
9
10
11networks:
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、macvlanname:网络的真实名称(覆盖<project>_network默认命名)external:指定是否使用外部网络,不由 compose 创建driver_opts:给 driver 传参数,比如 bridge 的 MTU、子网等ipam:指定 IP 地址池配置,包括子网 subnet、gatewayattachable:在 swarm overlay 网络中允许独立容器加入labels:给网络打标签
这些参数,我们下面来根据具体例子进行了解。
自定义网络名
docker-compse 如果想自定义网络,可以使用name参数。
比如以下配置中创建了一个my_custom_network网络,test_nginx容器通过mynet逻辑名称加入了my_custom_network网络(如果这个网络不存在,会自动创建)。
1 | services: |
加入已有网络
通过external参数,可以加入已有网络,比如下面的配置中,demo_nginx容器加入了已有的city_net网络。
1 | services: |
注意,如果已有网络不存在,则容器会启动失败,可以通过下面的命令提前创建网络:
创建网络命令
1 | docker network create city_net |
也可以指定 driver:
1 | docker network create -d bridge city_net |
扩展知识
逻辑网络的理解
前面我们的配置中通过mynet作为逻辑网络名,对于mynet而言,其会在不同的网络配置下有不同的含义,有时候它代表逻辑网络名,但有时候它又代表实际网络名。
我们来看个例子,现在有下面 A、B两个docker-compose配置文件:
1 | services: |
1 | services: |
配置完成后,我们通过docker-compose up -d命令启动。
A 配置中的mynet代表对应内部网络标识符,a_nginx 通过mynet加入了my_custom_net网络,而在 B 配置中,代表 b_nginx 加入了外部网络mynet,很明显上面我们没有创建这个网络,所以会加入失败。
如果想要成功,需要修改为如下配置:
1 | services: |
external 取值 false 的网络
如果你的docker-compose.yaml网络配置中external为false,比如下面的情况:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15services:
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_net,b_nginx为对应目录
1 | [root@localhost b_nginx]# docker-compose up -d |
跨机器网络通信
如果想跨机器进行网络通信,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”。
- Service:是你的命令(“我要 3 个 Nginx”)。
- Manager:创建 3 个 Task(任务槽位)。
- 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 | ┌───────────────────────────────────────┐ |
Stack 语法说明
docker stack 使用的配置文件格式也是 YAML,和 docker-compose.yml 几乎一模一样(version 3+)。
最大的区别在于: Swarm 忽略 build 指令(你需要提前构建好镜像推送到仓库),并新增了 deploy 模块。
deploy 模块是 Swarm 的精髓,它告诉集群“怎么跑这个服务”。
示例配置
1 | version: '3.8' |
参数详解
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 | x-logging: |
执行启动命令
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 | # worker 节点设置 |
镜像拉取
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
4volumes:
- ./logs/gateway:/logs
- ./front:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf
版本号问题
上面的镜像版本号全是 latest,这对回滚存在影响,生产建议加上版本号进行发版,方便代码回滚。
文章信息
| 时间 | 说明 |
|---|---|
| 2025-11-07 | 初稿 |
| 2025-11-21 | 增加 Docker Swarm 一节 |