这篇是 Swarm 系列里拖了最久的一篇,也是我认为在实际交付时价值最高的一篇:docker-stack 的企业级部署。前几篇我已经把集群初始化、节点打标签、overlay 网络规划、服务发现这些基础都写过了,按理说你可以用一堆 docker service create 命令把业务一个一个拉起来,但真要在生产环境长期维护几十个服务,谁还靠手工敲命令谁就是给自己埋雷。docker-stack 的核心价值其实一句话就能讲明白:把整个应用拓扑写进一个 YAML 文件,然后一条 docker stack deploy 命令完成创建、更新、扩缩容、密钥下发和滚动发布。这篇文章我打算直接讲透它,包括配置文件怎么写、滚动更新怎么控制、密钥怎么管理,以及企业落地时那些文档里不会写的坑。
不管你是刚把 Swarm 搭起来的小团队,还是在公司里负责多套集群的运维,这篇都应该值得你收藏。我不会绕弯子讲理论,全部按我实际部署过的方案来。
1. 为什么我把 docker-stack 当成 Swarm 集群的企业发布工具
1.1 先理清 docker-stack 和 docker-compose 的关系
很多人刚接触 Swarm 时会有一个误区:以为 docker-compose.yml 可以直接套用到集群上,写上 version、services、volumes 然后 docker-compose up 就完事了。其实 docker-compose 是单机时代的产物,它的核心工作是"在这一台机器上把容器编排起来",服务之间的依赖、网络、卷都是站在单机视角设计的。而 docker-stack 是 Swarm 模式下的部署层,它把同一份 Compose 风格的 YAML 交给 Swarm 调度器去解析,然后由集群内的 manager 节点把任务分发给 worker 节点。
这两者有一个非常关键的区别:docker-compose up 会负责 build 镜像,你可以在本地没有镜像的情况下让它现场构建;但 docker-stack deploy 完全不关心 build,它只认 registry 里的现成镜像。因为集群可能有十几个 worker 节点,调度器不可能在每个节点上都临时给你 build 一次。所以使用 docker-stack 的前提是:镜像必须提前推到所有节点都能访问的镜像仓库里。
还有一个容易被忽略的点:同一份 YAML 里,很多字段在 docker-compose 和 docker-stack 中的行为完全不一样。比如 deploy.replicas 在 docker-compose 下会被直接忽略,你必须用 docker-compose up --scale 或者 docker-compose scale 来指定副本数;而在 docker-stack 里,deploy 下的 replicas、update_config、placement、resources 才是决定服务怎么跑的核心。平时写习惯了单机 Compose 文件的人,第一次把文件搬到 Swarm 上很容易踩这个哑巴坑。
我把两者的差异整理成了下面这个表格,方便你对照:
| 对比项 | docker-compose up | docker stack deploy |
|---|---|---|
| 构建镜像 | 支持,会现场 build | 不支持,必须用仓库里已有镜像 |
| 服务副本数 | 由 --scale 参数控制 | 由 deploy.replicas 控制 |
| 滚动更新 | 需要手动 docker-compose up 再次执行 | 由 deploy.update_config 自动控制 |
| 密钥管理 | 不能直接使用 Swarm Secret | 支持 secrets 和 configs 专属字段 |
| 跨节点网络 | 走 bridge 或自定义单机网络 | 走 overlay 网络 |
| .env 文件 | 会默认读取项目目录的 .env | 不会默认读取,需显式传递环境变量或 --env-file |
| 扩容操作 | docker-compose up --scale 重建服务 | docker service scale 在线调整 |
| 回滚 | 重新部署旧版本镜像 | docker service update --rollback 一键回滚 |
我早期吃过一次亏:把一份带 deploy 字段的 compose 文件直接 docker-compose up,结果副本数死活都是 1,我还以为是网络问题。后来才明白,Compose 单机解析器碰到 deploy 字段就直接跳过,压根不报错。这个"静默忽略"的设计很坑,也恰恰说明 docker-stack 才是 Swarm 集群下真正该用的发布工具。
1.2 docker-stack 解决了企业部署的哪些具体问题
先说服务编排的一致性。在没有 docker-stack 之前,你在 Swarm 里部署一个服务要写一大堆 docker service create 参数:--replicas、--network、--mount、--env、--constraint、--update-delay、--publish……二十几个参数挤在一条命令里,写完自己都不想看第二眼。有了 docker-stack,所有配置都收敛到一个或多个 YAML 文件里,代码入库、评审、回溯都方便,这一点对团队协作是决定性的。
其次是滚动更新的可控性。docker-stack 的 deploy.update_config 可以精确控制每次并行更新几个副本、每批之间等多久、新副本的观察期是多长、失败时是暂停还是回滚。手工 docker service update 也能设置这些参数,但每次都要敲一长串,而且容易漏。把更新策略固化到 YAML 里,等于把发布流程的"安全性"变成了配置的一部分,而不是靠人肉记忆。
再一个就是密钥分发。企业部署绕不开密码、Token、证书。如果塞进 environment 字段,跑 docker inspect 就能看到明文;如果塞进镜像里,更是灾难。Swarm 的 secrets 机制可以把密钥以私有服务的形式下发到指定节点的内存文件系统里,只对需要它的服务可见。docker-stack 的配置文件和 secrets/configs 的配合,是我愿意在生产环境长期使用它的最重要理由之一。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一份可落地的企业级 stack 配置,逐段拆给你看
2.1 一个经过实战检验的完整配置示例
下面这份 YAML 是我在一套三节点 Swarm 集群上跑过一段时间的简化版配置。它覆盖了一个 Web 服务和一个后端 API 服务,包含副本控制、约束、健康检查、资源限制、滚动更新、密钥和配置下发。你可以直接复制下来改改镜像名和标签去验证,但建议先理解每一段在干什么。
yaml复制version: "3.8"
services:
web-front:
image: registry.example.com/web-app:2.4.1
environment:
TZ: "Asia/Shanghai"
volumes:
- nginx-conf:/etc/nginx/conf.d:ro
secrets:
- db_password
configs:
- nginx_site
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1/healthz"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
ports:
- target: 80
published: 80
mode: ingress
networks:
- public-net
deploy:
replicas: 4
placement:
constraints:
- node.labels.role == web
resources:
limits:
cpus: "0.50"
memory: 512M
reservations:
cpus: "0.10"
memory: 128M
update_config:
parallelism: 2
delay: 10s
failure_action: rollback
monitor: 30s
order: start-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
api-backend:
image: registry.example.com/api-server:2.4.1
environment:
TZ: "Asia/Shanghai"
secrets:
- db_password
- jwt_signing_key
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
networks:
- public-net
- backend-net
deploy:
replicas: 3
placement:
constraints:
- node.labels.role == api
resources:
limits:
cpus: "1.00"
memory: 1G
reservations:
cpus: "0.25"
memory: 256M
update_config:
parallelism: 1
delay: 15s
failure_action: pause
monitor: 30s
order: stop-first
restart_policy:
condition: any
delay: 5s
max_attempts: 5
networks:
public-net:
external: true
backend-net:
driver: overlay
driver_opts:
encrypted: "true"
volumes:
nginx-conf:
driver: local
secrets:
db_password:
external: true
jwt_signing_key:
external: true
configs:
nginx_site:
file: ./configs/nginx-site.conf
这份文件里最有讲究的不是 services 写了多少,而是每个 deploy 字段背后都对应一类生产问题。下面我拆开讲几个最核心的。
2.2 deploy 区块:副本、约束、资源三者是怎样联动的
先看 web-front 的 placement。我要求它只能调度到打了 role=web 标签的节点上。怎么打标签?提前在节点上执行:
bash复制docker node update --label-add role=web node-01
docker node update --label-add role=web node-02
这样做的目的很直接:把不同职责的服务分到不同硬件配置的节点上。比如 web 层对带宽和内存要求高,api 层对 CPU 要求高,数据库类节点可能要在磁盘 IO 上做特殊优化。如果你不对服务做节点约束,Swarm 调度器只会按照资源余量随便放,一台机器上可能同时堆满 Web 和 API,互相干扰,出问题的时候排查链路也会变长。
然后是 resources。limits 和 reservations 的区别经常有人搞混。limits 是一个硬上限,超过这个阈值容器会被 OOM 杀或者 CPU 被限流;reservations 则是调度器在决定要不要把任务放到某个节点上时必须预留的资源。比如 api-backend 每个副本 reservation 了 0.25 CPU 和 256M 内存,那么调度器会优先把任务放到空闲资源足够满足这个需求的节点上。这在企业环境里非常重要,不然你开了十个副本,调度器可能把任务堆在一台节点上,另一台节点反而空着,负载严重不均。
healthcheck 和 update_config 是另外一对容易理解错的组合。Swarm 的滚动更新不是"新容器起来了就立刻认为成功"——它默认会等待 monitor 指定的观察时间,并且参考容器是否处于 healthy 状态。如果新版镜像一启动就 crash,或者健康检查一直 probe 失败,更新策略的 failure_action: rollback 会触发自动回滚到上一版。我见过不少人配置了滚动更新,却没写 healthcheck,结果新版本接口返回 500,Swarm 照样把流量切过去,因为容器进程还活着。所以请记住:没有 healthcheck 的滚动更新,只能算"重启",不能算"发布"。
2.3 端口模式和网络选择:ingress 与 host 怎么选
配置里 web-front 的 ports 写的是:
yaml复制ports:
- target: 80
published: 80
mode: ingress
这里有个关键参数 mode。ingress 是默认模式,全称是 routing mesh:集群内任何一个节点的 80 端口收到流量后,Swarm 的路由层会把请求转发给真正运行 web-front 副本的某个节点。好处是用户只需要访问任意一台节点就行,无需关心副本具体落在哪台机器上;坏处是源 IP 会被做一层 SNAT,对于需要拿到客户端真实 IP 的服务会有影响。如果是代理层要做真实 IP 透传,通常会把 Nginx Ingress 之类的入口服务设置成 host 模式,让端口直接绑定到运行它的节点上,去掉一层转发。
再往后看两个网络:public-net 我声明为 external,backend-net 则是 overlay 且开启加密。external 的意思是这个 overlay 网络必须提前创建好,stack 不会替你建:
bash复制docker network create -d overlay --attachable public-net
我习惯把公网入口网络和内部网络分开,因为不同服务的暴露范围不一样。api-backend 同时挂在 public-net 和 backend-net 上,意味着它可以被外部流量到达,也能和 web-front 走内部加密网络通信。内部网络开启 encrypted 会增加一点性能开销,但在承载敏感业务时值得。注意,Swarm 的 overlay 加密是控制面层面的加密,不是数据面逐包加密,这一点不要误会成端到端加密,但在集群内部防嗅探已经够用了。
3. 发布、滚动更新与回滚:三板斧一次讲清
3.1 发布前的准备工作清单
docker-stack 部署看起来只有一条命令,但真正跑之前有四个前置项必须确认,缺一个都能让部署过程变得很难看。
第一,镜像要全部推送到仓库。因为 stack deploy 必须在 manager 节点上执行,再由调度器把任务发给各节点,各节点要从镜像仓库拉取。如果你只在 manager 上 build 了一个本地镜像,其他节点根本没有,任务就会一直卡在 Pull 阶段。这条建议养成习惯:任何服务上线前,先保证镜像在仓库里存在且标签明确,不要用 latest,要用完整的版本号,这样回滚时才知道上一个版本到底是哪个。
第二,外部依赖要提前创建。配置里 external: true 的网络和 secret,必须在执行 stack deploy 之前创建好。如果你忘记建 network,Swarm 会直接报错,告诉你网络不存在;如果你忘建 secret,也会在创建服务时报错。这个检查成本很低,但经常有人漏。
第三,节点标签要提前打好。deploy.placement 里的约束不满足时,服务副本会进入 Pending 状态,一直等不到调度器分配节点。你执行 docker service ps 会看到错误信息,但很多人第一眼只会看到副本一直没起来,容易误判成网络超时。
第四,确认所有 worker 节点能访问镜像仓库。这里的"能访问"包括网络可达、证书信任、登录凭据。对于私有仓库,要么提前在每个节点上 docker login,要么在 stack deploy 时加上 --with-registry-auth 参数,把当前机器的 registry 登录信息一并传给集群节点。这个参数方便,但意味着登录凭据会随任务下发,企业内网可用,公网环境要谨慎。
3.2 stack 部署命令与常用参数
准备好之后,发布命令其实简单得有点朴素:
bash复制docker stack deploy -c docker-stack.yml -c docker-stack.prod.yml prod
-c 可以传多个文件,后面的文件覆盖前面的同名配置,所以生产环境和测试环境的差异可以用这个机制做叠加。执行完这条命令之后,Swarm 只会创建一个名为 prod 的 stack,所有服务的名称都会带有 prod_ 前缀,比如 prod_web-front、prod_api-backend。这个前缀是你的服务在集群里的命名空间,也决定了 docker service ps 后面要接的服务名。
发布完成后,用下面这几条命令查看状态:
bash复制docker stack ls
docker stack services prod
docker stack ps prod
docker service logs prod_web-front --tail 200
docker stack services 的输出里会显示每个服务期望副本数和当前运行副本数,如果二者不一致,说明有副本没起来。docker stack ps 更细,会列出每个任务的调度节点、当前状态、错误信息。先养成看这两条命令输出的习惯,比什么都强。不要一上来就去 docker service logs,因为你可能连服务名叫什么都还没看清。
3.3 滚动更新到底怎么滚动
滚动更新由 deploy.update_config 驱动。我用一个例子解释它的工作过程。假设 prod_api-backend 当前是 3 个副本,新镜像版本 2.4.2 需要发布上去,配置是 parallelism=1、delay=15s、order=stop-first。
Swarm 调度器会这样操作:先挑一个副本,按 stop-first 顺序,先停掉旧容器,再启动一个新容器;新容器起来后进入 monitor 的 30 秒观察期,同时检查健康状态;如果健康检查通过,等待 delay 的 15 秒,再更新下一个副本;如果失败,根据 failure_action 的选择处理。failure_action 的取值有三档:pause 表示暂停整个更新,留给你排查;continue 表示忽略失败继续更新后面的副本;rollback 表示检测到失败后回滚到当前版本的上一版。
企业生产环境,我通常把前端服务的 failure_action 设为 rollback,因为前端副本多、无状态、回滚成本低;把后端服务设为 pause,因为后端改动往往涉及数据兼容,失败原因需要人工判断,自动回滚有风险。这里的取舍,说到底是一个工程判断问题,不完全是技术问题。
如果你想临时把某个服务扩到 5 个副本,不用改 YAML 文件,直接:
bash复制docker service scale prod_web-front=5
但这个操作只是临时改了副本数,stack 里面的 YAML 定义不会变。下次重新 deploy 文件时,副本数会回到 YAML 里声明的值,所以临时扩容要记得同步改文件,不然配置漂移早晚坑你一次。
3.4 回滚的两种姿势
回滚分两种。一种是在滚动更新失败后由策略自动触发,Swarm 会自己把服务恢复到上一个镜像版本,同时保留失败更新的状态记录。你执行:
bash复制docker service ps prod_api-backend --no-trunc
可以看到上一次更新的失败原因、哪一步失败的、在哪个节点上失败的。这个信息非常重要,不要急着把服务回滚完就跑,先把失败原因记下来,否则下次再发同版本镜像还会踩同一个坑。
另一种是手动回滚。如果新版本运行了半小时后才发现问题,而更新策略的 monitor 早就结束,自动回滚已经不会触发了。这时你需要手动执行:
bash复制docker service update --rollback prod_api-backend
这个命令会把服务回滚到更新之前定义的参数和镜像版本。注意它回滚的是"整个服务定义",不只是镜像。如果你在更新过程中还改了资源限制或环境变量,这些改动也会跟着回滚。
这里我必须提醒一个 Swarm 的经典坑:如果你在部署新版本之后又对服务执行过 docker service scale 扩容,回滚命令不会自动把副本数恢复到旧状态。因为 Swarm 回滚的是服务定义,但 scale 操作改的是当前运行的副本数量,两者分离。所以回滚完成后一定要再执行 docker stack deploy -c 你的YAML文件 把服务定义重新校正一遍,确保最终状态和仓库里的配置完全一致。
4. 密钥与配置:secrets/configs 的企业级用法
4.1 为什么不建议把敏感信息写进 environment
我在很多企业内部看到过一种做法:把数据库密码写进服务环境变量,然后把配置仓库权限设得死死的以为就安全了。实际上,只要你在 YAML 的 environment 字段里写明文密码,任何能在节点上执行 docker inspect 的人都能把它读出来。这是不安全的,没有任何讨论余地。
Swarm 提供的最正经做法是 secrets。它的工作方式和单机 bind mount 完全不同:当 Swarm 把一个 secret 调度到任务时,它会在目标节点的内存文件系统中创建一个只读文件,挂载到容器的 /run/secrets/ 目录下。节点磁盘上不会留下明文文件,容器停止后临时文件也会销毁。用户访问的不是环境变量,而是一个文件路径。应用需要主动去读取这个文件内容。
4.2 如何创建和使用 Swarm Secret
secret 的创建非常简单,从文件或标准输入都行:
bash复制printf 'YourDBPassword123' | docker secret create db_password -
docker secret create jwt_signing_key ./jwt.key
创建后,在 stack 配置里引用它。如果 secret 已经在集群里存在,对应配置段要写 external: true:
yaml复制secrets:
db_password:
external: true
然后在 service 里用 secrets 字段挂载:
yaml复制secrets:
- db_password
默认情况下,容器内会有一个文件 /run/secrets/db_password,文件内容就是密码。如果你的应用读取的是环境变量,可以让 Swarm 把 secret 以环境变量形式注入,但要在 service 里指定 target 和环境变量名:
yaml复制secrets:
- source: db_password
target: DB_PASSWORD
environment: PROD_DB_PASSWORD
文件形式和 environment 形式各有场景,但无论如何,明文密码都不再出现在 docker inspect 的输出里。
在企业里我建议给 secret 加轮换机制。Swarm secret 是创建后不可变的,如果你想改密码,通常的做法是创建一个新 secret,比如 db_password_v2,然后重新 deploy stack。旧 secret 可以留着,等所有任务都切到新 secret 后再 docker secret rm 删除。注意,正在被某个服务使用的 secret 是无法删除的,所以顺序一定是先改配置、再 deploy、最后删旧。
4.3 configs 适合放什么内容,怎么做到接近热更新
secrets 负责敏感数据,configs 负责非敏感的配置文件。二者机制很像,也是只读挂载,默认路径 /
回到我前面那份配置中的 nginx_site:
yaml复制configs:
nginx_site:
file: ./configs/nginx-site.conf
docker stack deploy 执行时,会在当前机器上读取文件内容,自动创建或更新 config。注意一个限制:config 的内容也是不可变的,更新文件不触发自动同步。所以你改了本地的 nginx-site.conf,必须重新 deploy 一次,Swarm 才会用新内容替换已有 config。如果嫌频繁重启服务麻烦,可以把配置文件放到共享卷里,再配一个 reload 脚本,但这已经不属于 docker-stack 的标准玩法了,属于按需定制的部分。
4.4 权限和排错上最容易忽略的细节
secret 和 config 的挂载权限默认是 0444,容器内所有用户可读,这点要提醒应用团队。如果应用是以非 root 身份启动,读取 /run/secrets 没有太大问题,因为权限是只读且全局可读的;但如果你想限制到只能被特定 UID 读取,Swarm 没有提供细粒度权限配置,要么在容器启动命令里处理,要么用 init 容器做权限转换,这不是 docker-stack 能直接解决的问题。
排错时有一类很隐蔽的报错:secret 名称写错了,或者 secret 还没创建就执行 stack deploy,Swarm 会报 "failed to create service: secret not found"。但如果你用的是多文件合并部署,比如 -c 基础文件 -c 环境文件,同一个 secret 在两个文件里的定义不一致,也可能出现无法预料的覆盖问题。我建议把 secret/config 的定义集中放在一个环境文件里,部署时只要指定对应环境的文件,不要散布在多个配置里,这样排查起来容易很多。
5. 有状态服务能不能用 stack 跑:先说实话再给方案
5.1 Swarm 对有状态服务先天不足在哪里
这个系列写到现在,有几个人提了一个非常现实的问题:既然 stack 这么方便,能不能直接把 Kafka、Redis、数据库这些有状态服务也用 docker-stack 部署进 Swarm?我的答案是:技术上可以,实践上要特别小心,默认情况下别这么做。
Swarm 的调度器设计上就是为无状态服务准备的。它的核心逻辑是"副本可以被杀、被迁移、被重新调度",所以对持久化存储的处理方式很弱。虽然你可以把宿主机目录挂载进容器,但当任务被调度到另一台节点时,旧的本地目录不会跟着走,数据就断了。企业里跑 Kafka 至少要 3 个节点、每个节点一份数据副本,如果 Swarm 把任务调度得东一个西一个,再加上本地卷的路径混乱,你会在半夜被数据不一致的问题叫醒。
还有 overlay 网络对某些有状态协议不友好。比如 Kafka 的客户端依赖 broker 的 advertised.listeners 做协议地址协商,默认情况下它拿到的可能不是客户端能访问的地址。Swarm 的 VIP 模式还会把请求负载均衡到多个副本,这对无状态的 HTTP 很友好,但对 Kafka、Redis 这类要求客户端与固定实例保持会话的协议就是灾难。
5.2 非要跑,至少要满足这几个前置条件
第一个条件是固定节点。通过 placement constraints 把每个有状态服务钉死在指定节点上,比如给节点打上 role=kafka 的标签,然后用约束强制调度。这样任务不会被随意迁移,但这意味着你放弃了 Swarm 的故障迁移优势,等于把有状态应用当作"跑在 Swarm 里的普通容器"来管理,你要想清楚这个妥协。
第二个条件是 host 模式的端口发布。前面提到 ingress 模式会做负载均衡转发,对于有状态服务应该使用 host 模式,让端口直接暴露在对应节点的宿主机上。客户端连接时也要直接连接该节点 IP,而不是访问 Swarm 任意节点。这样端口不会重复占用,节点之间的通讯也保持直连。
第三个条件是 external 存储。本地 volume 不跨节点,如果你一定要让任务有机会迁移到其他节点,就必须使用 NFS、CephFS 这类共享存储。但企业共享存储的配置、权限、延迟都是额外的运维成本,性能也可能打折。我见过一些团队用 NFS 挂载跑 Kafka,吞吐量直接腰斩,最后不得不换回裸金属磁盘。
下面这段是我当年在一个测试环境里强行跑 Kafka 时用过的约束配置片段,仅供参考,不建议照抄生产:
yaml复制services:
kafka:
image: registry.example.com/kafka:3.6.2
hostname: "{{.Node.Hostname}}"
volumes:
- kafka-data:/var/lib/kafka/data
ports:
- target: 9092
published: 9092
mode: host
networks:
- kafka-net
deploy:
replicas: 3
endpoint_mode: dnsrr
placement:
constraints:
- node.labels.role == kafka
这里使用了 endpoint_mode: dnsrr,意思是关闭 VIP 负载均衡,让 Docker DNS 只做 DNS 轮询解析,把"连哪个实例"的决策权交给客户端。这是对 Kafka 这类服务最关键的调整。即便如此,这个方案仍然需要你在 Kafka 配置里手动指定每个节点的 advertised.listeners,否则集群内部通信还是会乱。
5.3 我的真实建议
以我自己的经验,docker-stack 最适合承载的是无状态的 Web 服务、API 服务、任务调度类服务、消息消费者,以及各种代理和入口组件。它们可以随时横扩、随时替换、随时回滚,完全发挥 Swarm 的优势。至于 Kafka、Redis Cluster、数据库这类有状态组件,我更推荐用传统方式部署在集群外部的专用机器上,或者用专门的有状态编排平台来管理。硬塞进 Swarm 不是不能跑,但你会花大量时间处理调度、存储、网络层面的边缘问题,而这些时间本可以省下来。
如果你已经在 Swarm 集群外部跑了一套 Kafka 或 Redis,业务服务通过 overlay 网络的 attachable 方式去连接它们也是一条可行路线。也就是说,docker-stack 管的是集群内的无状态应用,有状态组件留在集群外,两者通过内部网络互通,这样既享受了 docker-stack 的发布便利,又避开了有状态服务的编排黑洞。
6. 企业落地时躲不开的运维细节与踩坑记录
6.1 镜像分发:别把私有仓库证书配置成一场事故
docker-stack 部署不负责构建镜像,这就意味着镜像仓库的可达性是全集群的隐性地基。如果你用自签证书的私有仓库,每个 worker 节点的 Docker daemon 都要信任对应 CA。最简单的配置方式是在每台节点的 /etc/docker/daemon.json 里加 insecure-registries 或者 registry-mirrors 配置,然后重启 Docker 服务。insecure-registries 适合内网测试,生产环境我建议使用标准 CA 签发的证书,或者在每台节点上安装私有 CA 证书,这样不会降低 TLS 安全性。
还有一类常见事故:节点离线。Swarm 调度器只会把任务分配给处于 Ready 状态的节点。如果有一台 worker 节点网络中断,任务并不会自动迁移,它会一直处于分配失败的 Pending 状态,直到节点重新上线。此时不要反复 docker stack deploy,应先用 docker node ls 检查所有节点的状态,确认节点在线后再重新发布。
6.2 日志和监控:docker service logs 之外还缺什么
docker service logs 是排查问题的一把好手:
bash复制docker service logs prod_web-front --since 30m --tail 300
它会把调度到不同节点上的所有副本的 stdout 和 stderr 汇总到一处,省去你逐台节点登录翻 docker logs 的功夫。但它默认依赖容器的 json-file 日志驱动,容器被重新调度后旧日志不会跟着跑。生产环境里,建议在每台节点上配一个轻量的日志采集客户端,把容器日志转发到统一的日志系统。这不属于 docker-stack 的范畴,但如果你要长期运维 Swarm,这一步早晚得补。
关于健康状态的监控,Swarm 有一个简单实用的接口:
bash复制docker service inspect prod_api-backend --format '{{json .UpdateStatus}}'
滚动更新期间,这个字段会显示当前更新状态、开始时间、完成状态。如果更新卡住了,可以从这里看到是不是 monitor 没有结束,或者失败任务卡在某个节点上。我建议把这条命令写进你的发布检查脚本里,回滚和发布时都会用到。
6.3 多环境管理和配置漂移
企业往往至少有两套环境:测试和生产。docker-stack 支持一次传多个 -c 文件,后文件覆盖前文件,你就可以把公共配置放在 base 文件,把环境差异放在 overlay 文件:
bash复制docker stack deploy -c docker-stack.yml -c docker-stack.test.yml test
docker stack deploy -c docker-stack.yml -c docker-stack.prod.yml prod
这样处理环境和生产环境之间的镜像版本、副本数、资源限制差异都非常顺手。但要注意一个问题:两个文件里如果出现同名服务,Swarm 合并配置时是逐字段覆盖,不是整体替换。比如 prod 文件只想改一个环境变量,也要把服务的关键字段写上,否则可能不小心把副本数、约束这些配置丢了。为了避免这个问题,我习惯在环境文件顶部写注释,标明覆盖了哪几个字段。
配置漂移是我在公司里强调最多的问题。有一次排查线上服务,发现副本数是 10,但仓库里 YAML 只有 4 个副本。原因是当晚有人手动 docker service scale 扩到了 10,没同步文件。这种问题不会自己消失,最好的方式是每次手动操作服务后,马上回到仓库更新 YAML,并且定期执行 docker stack deploy -c 文件 做一次"校正"。Swarm 在执行 deploy 时,如果发现实际副本数和 YAML 声明不一致,会自动调整到声明值。利用这个特性,你可以让仓库配置成为最终权威。
6.4 一张速查表,记住常见报错的含义
我在下面整理了一张表,都是实际运维中出现的典型报错和定位方向,适合贴在你团队的运维手册里:
| 现象/报错 | 可能原因 | 排查路径 |
|---|---|---|
| 服务一直 Pending,docker service ps 提示 no suitable node | placement 约束不满足,或节点资源不足 | docker node inspect 检查标签和可用资源 |
| 副本显示 starting 但始终不 Ready | 镜像拉取失败或健康检查失败 | docker service ps --no-trunc 查看任务错误信息 |
| update 卡住不动 | monitor 期间新副本不健康 | docker service inspect 查看 UpdateStatus |
| secret not found | secret 未创建或名称拼错 | docker secret ls 核对名称 |
| 端口冲突导致任务无法启动 | 宿主机端口已被占用 | docker service ps 报错信息里会带具体端口 |
| docker stack deploy 报 network not found | external network 未提前创建 | docker network ls 核对网络名 |
6.5 最后再分享一个实用小技巧
每次执行 docker stack deploy 之前,我会先跑一遍配置渲染检查,确认 YAML 里的变量、镜像标签、secret 引用都没有问题,这个命令是新版 Docker 提供的 docker stack config:
bash复制docker stack config -c docker-stack.yml -c docker-stack.prod.yml
它会输出渲染后的最终配置,但不会直接部署。我在更新前会先看一遍这个输出,重点检查镜像版本号是否如预期、secret 的 external 引用是否正确、网络名是否和生产环境一致。这个习惯帮我挡掉了至少两三回把线上配置改坏的半夜事故。一次 deploy 看起来很轻量,但改坏一个环境变量,或者把 secret 名打错,就会让你在十分钟后发现整条链路在报错。先渲染、再部署、最后确认,这个三步流程花不了几秒钟,但对生产环境的安全感提升非常明显。
我在实际部署中用 docker-stack 发布过的服务已经数不清了,从最初只有两三个 Web 服务的小集群,到后来承载完整业务链路的几十个服务,跌跌撞撞踩过的坑基本都写在这篇里了。如果你刚开始把业务往 Swarm 上迁移,我建议先不要追求复杂的配置,用一个最小化的 Web 服务把 deploy、滚动更新、secrets 这三件事跑通,再逐步把多环境文件、资源限制、节点约束加上去。一套稳定可复现的发布流程,比任何花哨的编排技巧都值钱。
