很多人一提到 Docker 集群管理,第一反应就是 K8s,但真到自己维护的时候才发现:光是理解一堆控制器和网络插件,就已经劝退了不少人。我在过去几年里,给多个团队搭过 Docker Swarm 集群,周边配合 Harbor 私有仓库、Docker Stack 部署、Portainer 可视化,这套组合覆盖了从镜像管理到服务编排再到日常巡检的完整链路。这篇文章就把我实际操作中的环境规划、部署步骤、避坑点和运维心得整理一遍,适合那些机器数量在几台到二十台左右、不想一上来就上 K8s 的团队参考。
1. 为什么选 Swarm:先想清楚你到底需要什么
1.1 Swarm 解决的核心问题
单机 Docker 模式下,所有容器都跑在一台机器上,一旦机器宕机,服务全部挂掉;就算你手动把容器迁到另一台机器,IP、数据卷、网络配置全都得重来。Swarm 解决的核心问题,是把多台 Docker 主机抽象成一个集群,让服务可以在节点之间调度、漂移、扩缩容。
它原生支持两类角色:管理节点(Manager)和工作节点(Worker)。管理节点负责集群状态维护、任务调度、API 入口;工作节点只负责运行容器任务。你不再需要关心“这个容器在哪个 IP 上”,只需要声明“我要跑 3 个副本”,Swarm 的调度器会自己找合适的节点去放。
还有一点是内置的 Overlay 网络。Swarm 集群里的服务可以通过自定义网络跨主机通信,网络上的服务名就是 DNS 名字,服务之间直接通过服务名访问,不用再手工维护 IP 映射。我见过不少团队用 Swarm 之前,服务间的调用地址写死在配置文件里,扩容一个副本还要改配置,相当痛苦。
1.2 为什么这个规模下没直接上 K8s
肯定有人问:K8s 不是更主流吗?如果团队已经有人非常熟悉 K8s,并且需要复杂的弹性伸缩策略、CRD、服务网格这些能力,那 K8s 没问题。但大多数中小团队的真实情况是:容器化刚起步,DevOps 人力有限,线上服务也就十几个。这时候 K8s 带来的运维成本会明显高于收益。
Swarm 的优势在于简单直接。docker stack deploy可以把一套 Compose 文件直接变成集群服务;docker service scale可以一条命令扩缩容;负载均衡和滚动更新都内置。学习曲线比 K8s 平缓很多,一个人花一个下午就能把三节点集群跑起来。
踩过几次坑之后我的建议是:先问自己两个问题。第一,你的服务数量未来一年内会不会超过二十个?第二,团队里有没有人能长期负责 K8s 的版本升级和故障排查?如果答案都是否,那 Swarm 这套组合足够用很久。它不是“玩具”,生产环境跑一两年完全没问题,重点在于你愿不愿意把镜像仓库、监控、备份这些配套做扎实。
1.3 整套环境的角色划分
以我常用的部署结构为例,总共 4 台云主机,规划如下:
| 节点 | 角色 | 配置参考 | 部署组件 |
|---|---|---|---|
| node-1 | Swarm Manager | 4C 8G | Manager、Portainer、Harbor 对外访问入口 |
| node-2 | Swarm Manager | 2C 4G | Manager(Raft 仲裁节点) |
| node-3 | Swarm Manager + Worker | 4C 8G | Manager 兼任 Worker,运行业务容器 |
| node-4 | Swarm Worker | 4C 8G | 运行业务容器、缓存类服务 |
| registry-host | 独立仓库节点 | 4C 8G | Harbor(也可以单独部署) |
这套结构里,Harbor 我倾向于单独放在一台机器上,不要塞进 Swarm 集群本身。原因后面会说,主要是镜像仓库一旦成为集群里的服务,集群故障会影响镜像拉取能力,形成恶性循环。
Portainer 部署在 manager 节点上,通过 Agent 方式管理整个集群。这样管理人员不需要 SSH 到每台机器敲命令,浏览器里就能看到所有节点的状态、服务的启动情况、日志输出。
2. 前置准备:服务器规划、Docker 安装与 Harbor 仓库自建
2.1 节点命名、防火墙与 Docker 版本
开始部署之前,先把主机名和网络规划清楚。我习惯把主机名改成node-1、node-2这种简洁格式,Swarm 集群里面显示的名字会更清爽。每台机器上配置好/etc/hosts,把所有节点的主机名和 IP 对应关系写进去,避免后期节点互相解析不到。
防火墙方面,Swarm 管理节点需要开放 2377/TCP(集群管理通信)、7946/TCP/UDP(节点间通信)、4789/UDP(Overlay 网络 VXLAN),如果你用了ingress模式的服务端口,还需要把对应业务端口放通。我这里环境内部使用安全组管理,测试时就先把这些端口测试开放,等集群稳定后再收拢规则。
Docker 版本建议统一。Swarm 集群对版本差异容忍度不算高,尤其是 Docker Engine 大版本不一致时,调度行为可能出现怪异现象。我在生产环境固定使用同一版本,升级时也安排节点逐个滚动升级。安装方式直接使用 Docker 官方源,这里不做赘述。
2.2 配置 Docker 数据目录与日志轮转
很多集群跑了一段时间后磁盘被撑爆,根源是容器日志没有限制。建议在每台节点上创建/etc/docker/daemon.json:
{ "data-root": "/data/docker", "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" }, "storage-driver": "overlay2" }把>hostname: registry.example.local http: port: 80 https: port: 443 certificate: /data/cert/registry.example.local.crt private_key: /data/cert/registry.example.local.key harbor_admin_password: YourStrongPassword database: password: DbPassword123 data_volume: /data/harbor
第一次安装用./install.sh --with-trivy这样带上漏洞扫描组件,镜像安全这一块最好从一开始就保留。装完访问https://registry.example.local,用 admin 账号登录,创建两个项目:一个apps用来放业务镜像,一个base用来放基础依赖镜像。项目访问级别我先设为私有,只有仓库登录账号才能拉取,减少误拉公共镜像的风险。
2.4 让每个 Swarm 节点都能信任 Harbor 证书
这是最容易漏掉的一步。Swarm 集群的节点在拉取镜像时,如果 Harbor 用的是自签或内网 CA 证书,Docker 默认会拒绝连接。每个节点都要创建目录/etc/docker/certs.d/registry.example.local/,把 CA 证书复制进去,然后重启 Docker。
mkdir -p /etc/docker/certs.d/registry.example.local cp ca.crt /etc/docker/certs.d/registry.example.local/ systemctl restart docker千万别只配置管理节点就觉得完事。工作节点同样需要这个证书,否则服务调度到工作节点后,那个节点拉取镜像会失败。验证方法是每台节点都执行:
docker login registry.example.local judging-login-successful-without-error如果企业环境不允许把 Harbor 的 hostname 直接暴露,也可以在 daemon.json 里配置insecure-registries,把仓库地址加入白名单。但这种方式会跳过 HTTPS 校验,仅建议内网可信环境使用。
3. 初始化 Swarm 集群:从 join 到节点规划
3.1 初始化管理节点与工作节点接入
在 node-1 上执行:
docker swarm init --advertise-addr 192.168.10.11 --listen-addr 0.0.0.0:2377--advertise-addr必须是其他节点能访问到的管理节点 IP。我见过有人服务器上配置了多网卡,默认网卡是公网地址,结果 init 之后节点间通信异常,执行docker node ls经常卡住。指定内网地址后,集群内流量走内网,稳定性和速度都会更好。
初始化完成后,输出会包含 join 命令和 token。在 node-2、node-3、node-4 上直接执行对应 token:
docker swarm join --token SWMTKN-1-xxx 192.168.10.11:2377加入完成后,回到管理节点查看节点状态:
docker node ls正常情况下每个节点的STATUS都是Ready,MANAGER STATUS栏位会显示Leader或Reachable。如果你希望 node-2 也作为管理节点参与 Raft 仲裁,在 node-1 上执行docker node promote node-2。
3.2 管理节点数量与 Raft 一致性
生产环境建议管理节点是奇数个:3 个最稳妥,5 个是极限,再多反而降低效率。原因是 Swarm 管理节点之间使用 Raft 协议,要求绝大多数节点正常才允许修改集群状态。3 个管理节点允许挂 1 个,5 个允许挂 2 个。
我确实遇到过管理节点只剩 2 个、其中一个又宕机的情况,整个集群无法接受新任务,服务状态查询也会卡住。当时应急处理是把宕机节点的角色先降级,再把集群拉回可用状态。如果你刚开始搭测试环境,可以先用 1 个管理节点,但生产环境请务必给到 3 个。
3.3 节点标签规划:让服务落位更可控
Swarm 调度默认是“哪里有空放哪里”,但实际场景里,有些服务希望固定跑在高配置节点,有些服务不想和数据库节点混跑。节点标签就是为了解决这个问题。
查看节点后,给节点打标签:
docker node update --label-add role=groovy node-1 docker node update --label-add role=worker node-4 docker node update --label-add storage=fast node-1然后在 stack 文件里的deploy.placement.constraints中引用标签。比如 Web 服务要求跑在role=groovy的节点上,Redis 要求跑在storage=fast的节点上,调度器就会根据标签筛选节点。
这一步的重要性在一开始看不出来,等到你新增节点或者调整服务副本数时,标签可以减少很多“漂移”带来的问题。我建议至少把节点分成compute和storage两大类,再细分是否允许运行数据库类服务。
4. 用 Docker Stack 部署服务:把 Compose 变成集群服务
4.1 Stack 与 Compose 的关系
Docker Stack 本质上是把 Compose 文件转换为 Swarm 服务定义。它的好处是你仍然用熟悉的 YAML 描述服务、网络、挂载卷,但depends_on这种单机概念会失效,资源限制、滚动更新、副本数这些需要在deploy段落里写。
我第一次用 stack 时误以为可以沿用旧的 Compose 写法,把ports写在容器映射那里,服务确实能起,但想在集群层面做滚动更新时总是不符合预期。后来才理解:Compose 文件在 Swarm 里只是模板,真正生效的是deploy配置。
4.2 一个真实可用的 stack 文件
下面是一套实际项目中用过的例子,镜像存放在 Harbor,服务名和网络名都走内部 DNS。
version: "3.8" services: web: image: registry.example.local/apps/web:1.3.2 environment: REDIS_ADDR: redis:6379 ports: - target: 80 published: 8080 mode: host deploy: replicas: 3 update_config: parallelism: 1 delay: 10s failure_action: rollback order: start-first restart_policy: condition: on-failure max_attempts: 3 placement: constraints: - node.labels.role == groovy networks: - app-net redis: image: registry.example.local/base/redis:6.2-alpine volumes: - redis-data:/data deploy: replicas: 1 placement: constraints: - node.labels.storage == fast networks: - app-net volumes: redis-data: driver: local networks: app-net: driver: overlay attachable: true这里有几个细节要强调。
order: start-first表示更新时先启动新容器,等新容器健康后再停止旧容器,适合对可用性要求高的服务。failure_action: rollback表示一旦更新过程出现容器启动失败,Swarm 会自动回滚到上一个版本。如果你的服务还没有配置健康检查,建议先在镜像里加入/healthz之类的接口,再结合健康检查让更新过程更安全。
网络这块我特意加了attachable: true,作用是让独立容器也能加入这个 Overlay 网络,方便调试或临时起一个排障工具容器。
4.3 部署、滚动更新与回滚
部署集群服务:
docker stack deploy -c docker-compose.yml demodemo是 stack 名称,所有服务名会带上demo_前缀,比如demo_web、demo_redis。查看服务状态:
docker service ls docker service ps demo_web滚动更新服务到新镜像:
docker service update --image registry.example.local/apps/web:1.3.3 demo_web如果更新后发现问题,直接回滚:
docker service rollback demo_web这些命令几乎每天都在用。实际维护中我最常犯的错是更新时忘记--image加完整的仓库地址,只写了web:1.3.3,Docker 会从公共源去找镜像,结果自然是找不到。镜像 tag 一定要带上 Harbor 地址,这是整个集群能稳定拉取私有镜像的前提。
4.4 Swarm 内置 DNS 与服务发现
Container 之间跨主机通信,很多人习惯用 IP。但在 Swarm 里,服务实例会漂移,IP 是变化的,正确做法是直接用服务名。
上面例子中,web服务访问redis这个服务名,Swarm 内置 DNS 会把它解析到redis服务的某个实例 IP。如果redis服务有两个副本,DNS 会自动负载均衡。你不需要知道 Redis 具体跑在哪台机器上,也不需要配置额外注册中心,这就是 Overlay 网络自带的能力。
我之前排查过一个问题:服务 A 在容器里访问服务 B 时偶发超时,查了网络也没有异常,最后发现服务 A 的代码里写了“连接 10.0.1.12:6379”,而那个 IP 正好是服务 B 之前某个容器的临时 IP。改成服务名之后问题彻底消失。在 Swarm 集群里,请把所有服务地址配置成服务名,不要硬编码 IP。
5. Harbor 与 Swarm 的联动:镜像分发、版本管理与清理
5.1 为什么集群必须走私有仓库
很多人图方便,直接用公共镜像仓库跑业务,甚至用 docker hub 的私有仓库。但 Swarm 场景下,私有仓库不只是“存镜像”这么简单。
集群里有十个节点,如果都用公共源,每次上线新版本,每个节点都要去外网拉取,速度慢且依赖外部网络。而 Harbor 部署在机房内网,节点拉取速度是秒级的。另一个重要因素是权限审计。Harbor 可以给不同团队分配不同项目权限,谁推了哪个镜像、哪些节点拉过镜像,都有操作记录。在故障排查和安全合规时,这个能力很有价值。
5.2 镜像 tag 策略:别用 latest
Swarm 的滚动更新机制依赖镜像 tag 变化。如果所有版本都写成latest,更新时 Swarm 可能认为镜像没变化而跳过重启容器。生产环境我强制要求使用语义化版本号,例如1.3.2、1.3.3,需要更新时明确指定版本。
还有一种做法是使用 commit hash 作为 tag,比如web-20250215-8f3a2b。这种 tag 可读性好,还能追溯到源码提交。Harbor 的复制规则里也支持按 tag 匹配,对新的提交自动复制到备份仓库。
5.3 Harbor 复制管理:多集群同步与容灾
如果团队有多个环境,比如测试环境、预发环境、生产环境,每个环境都有自己的 Swarm 集群,我建议给每个环境部署一个 Harbor,然后在 Harbor 之间配置复制规则。
复制规则可以按项目配置,例如把apps项目中的web-*镜像从源仓库复制到目标仓库。这样测试环境验证通过的镜像,不经过手工 docker push,自动同步到生产仓库,减少误操作和版本混乱。初次配置时我犯过一个错:设置成“复制所有镜像”,结果把一堆临时调试镜像也同步过去了,仓库白占空间。后来改成按项目加 tag 前缀,干净很多。
5.4 镜像清理与 GC 策略
Harbor 本身可以做每日清理,但真正释放磁盘空间要靠垃圾回收(GC)。GC 会清理掉未被任何镜像引用的 blob 层。我设置每周日凌晨执行一次 GC,保留最近 10 个版本的镜像,其余旧版本自动清理。规则如下:
- 保留最近 10 个 tag。
- 保留时间超过 30 天的 tag 全部删除。
- 每月清理一次无 tag 的镜像。
这个策略执行了半年,效果很好:Harbor 的数据盘体积一直稳定在可控范围,没有出现镜像越堆越多导致磁盘告警的情况。
6. Portainer 可视化:集群运维的“驾驶舱”
6.1 为什么要用 Portainer 接入 Swarm
Swarm 命令行足够强大,但一个十几台节点、几十个服务的集群,靠命令行逐台查看效率很低。Portainer 算是我在运维中离不开的工具,它的实时性比命令行更好,而且对团队里非深度运维的同事比较友好。
Portainer 有两种接入集群的方式。一种是通过 Docker API 直接连接,需要暴露 Docker socket,权限范围等于 root,风险较高。另一种是用 Agent 模式:每台节点跑一个轻量的 portainer agent 容器,agent 只暴露 9001 端口,Portainer 通过 agent 去操作目标节点。我推荐 Agent 方式,权限更可控,而且可以统一管理多套集群。
6.2 Portainer 服务端与 Agent 部署
先在每个 Swarm 节点上启动 agent 容器:
docker run -d \ --name portainer_agent \ --restart=always \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /var/lib/docker/volumes:/var/lib/docker/volumes \ -p 9001:9001 \ portainer/agent:latest然后在 node-1 上部署 Portainer 服务端:
docker run -d \ --name portainer \ --restart=always \ -p 9000:9000 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer_data:/data \ portainer/portainer-ce:latest首次访问http://node-1:9000会要求创建管理员账号,这里设置复杂密码。接着选择连接环境,填入其他节点的 agent 地址,例如192.168.10.14:9001,Portainer 就会通过该节点的 agent 建立连接。
终端服务器数量多时,我不建议全部用一个全局管理员,而是按团队划分:开发账号只有查看权限,运维账号有部署权限。Portainer 支持团队和用户授权,可以给每个 endpoint 单独配置权限,这一块值得花半小时配置好,后期省掉很多“某人手滑点了删除”的麻烦。
6.3 用 Portainer 完成日常操作
Portainer 的“Stacks”页面可以直接提交 stack 文件,也可以修改现有服务的镜像版本并触发更新。我在实际操作中,很多常规上线流程都是直接在 Portainer 里完成的:
- 在 Harbor 构建并推送新镜像。
- Portainer 选择对应 stack,修改镜像 tag。
- 点击更新,观察每个节点的新容器替换情况。
- 如果服务状态异常,直接点击回滚按钮。
这套流程比命令行更适合给值班同学用,减少了误操作概率。Portainer 的日志查看功能也很实用,选择某个服务,可以直接看所有副本的日志流,不需要docker service logs逐个容器去翻。
6.4 资源监控与告警思路
Portainer 自带可视化的节点资源监控,包含 CPU、内存、磁盘占用,对日常巡检足够了。但它不是完整监控系统,更严格的告警建议配合独立的指标采集工具。如果团队没有 Prometheus 体系,我建议至少给关键服务配置存活探针和健康检查,Portainer 在容器状态异常时能及时展示出来。
我有一次发现某个服务状态一直是 running,但业务日志完全不输出,端口探活也失败。后来排查发现是容器内的健康检查命令没有配置,导致 Swarm 认为容器是健康的。建议在 stack 文件里为每个服务配置健康检查,例如:
healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"] interval: 30s timeout: 5s retries: 37. 常见故障排查:这些坑我基本都踩过
7.1 工作节点拉取 Harbor 镜像失败
故障现象:docker service ps显示某个任务Failed,日志是pull access denied或x509: certificate signed by unknown authority。
排查链路:
- 先在工作节点手动执行
docker login registry.example.local,确认能登录。 - 检查节点上
/etc/docker/certs.d/registry.example.local/是否存在 CA 证书。 - 没有则复制过去并重启 Docker,再试。
- 能登录但仍然拉取失败,检查 stack 文件里的镜像地址是否写错,常见问题是把
registry.example.local/apps/web:1.3.2写成了apps/web:1.3.2。
这个问题最容易出现在后面新增节点的时候,新节点加入前必须先做证书分发。我在入职新环境时吃过一次亏:集群跑了三个月,加了一台新工作节点,忘了配置证书,服务调度到新节点就开始失败。所以建议把证书分发加入节点初始化脚本里。
7.2 服务一直处于 Update 状态
故障现象:docker service ls看到demo_web的REPLICAS是 3/3,但状态一直是Update,实际业务可能中断。
排查链路:
- 查看
docker service ps demo_web确认每个副本的最新状态,是否有Starting、Complete或Rollback记录。 - 检查镜像 tag 是否写错,导致调度后节点无法拉取。
- 检查健康检查配置。如果容器启动后一直处于 starting 状态而未进入 running,Swarm 会认为更新还没完成,不会标记为已完成。
我遇到过最典型的场景是服务里有初始化任务,执行耗时超过健康检查的 timeout,健康检查一直失败,Swarm 不断重启容器。解决方法是把初始化逻辑放到 entrypoint 脚本里,提升健康检查 timeout 时间,或者改用独立的 job 服务执行一次性任务。
7.3 Raft 故障导致命令卡死
故障现象:docker node ls长时间无响应,执行任何docker service命令都卡住。
排查链路:
- 检查管理节点之间网络连通性,防火墙 2377 端口是否被安全组拦截。
- 查看管理节点 Docker 进程是否正常。
- 如果只有两个管理节点且其中一台宕机,Raft 仲裁无法达到多数,会出现这种卡死。
修复方法是把宕机节点的管理角色移除,或者重新调整管理节点数量。日常维护中,我至少保证三个管理节点,并且管理节点不部署高负载业务容器,降低它挂掉的风险。有过一次管理节点因磁盘写满导致 Docker 无响应,整个集群调度基本瘫痪,最后只能重启该节点并清理 Docker 数据目录。从那以后,我在管理节点上加了磁盘空间监控和自动清理脚本。
7.4 磁盘空间膨胀问题
Fab 现象:节点/data/docker目录越来越大,垃圾桶、镜像层堆积导致磁盘告警。
排查链路:
- 先看有没有没有清理的悬空镜像,执行
docker images检查。 - 执行
docker system df看镜像、容器、本地卷占用。 - 对不再使用的镜像清理:
docker image prune -a --filter until=72h docker system prune --volumes但这条命令要谨慎。docker system prune --volumes会删除所有未被容器引用的卷,如果卷里存了重要数据,结果会很惨。我一般在没有状态服务、或者数据已备份的前提下才执行。更安全的做法是只清理镜像:
docker image prune -a --filter until=72h对服务的历史版本镜像,我在 CI 中会定期删除超过 10 个版本的镜像 tag,再用 Harbor 的 GC 释放空间。这样既保证本地缓存够用,也不会让磁盘无限增长。
7.5 调试小技巧:临时容器加进 Overlay 网络
当你在一个服务容器里想访问另一个服务,但排查网络问题时,可以通过 attachable 网络直接起一个调试容器:
docker run -it --rm \ --network demo_app-net \ --entrypoint sh \ registry.example.local/base/busybox:latest进入容器后,就可以ping redis、telnet redis 6379验证服务名解析和端口连通性。这个技巧在 Swarm 网络调试里非常有用,十次里有七八次能快速定位问题。
写在最后:一点个人体会
前后搭了好几套 Swarm 集群之后,我的核心体会是:这套方案的上限取决于你的配套做得多扎实,而不是 Swarm 功能本身。Harbor 的版本策略决定了上线流程是否顺畅;节点标签和 Overlay 网络决定了服务架构是否清晰;Portainer 的权限配置决定了团队协作是否安全。
如果你正准备在公司内部落地这套组合,我建议不要一上来就把所有服务都迁进去。先用一个非核心业务跑通“Harbor 推送镜像 -> stack 部署 -> Portainer 查看状态 -> 滚动更新 -> 回滚”的完整链路,再逐步把其他服务纳入集群。等这套流程变成肌肉记忆之后,Swarm 日常维护的负担其实比大多数人的想象要低得多。