自己折腾过容器编排的应该都有这种体验:单机 docker run 一把梭还行,几台机器十几个服务一起来,靠手动敲命令或者分发 compose 文件,那基本就是拿命在运维。每次发布要登录每台机器拉镜像、起容器,服务间重新连接还得祈祷网络没配错,半夜扩容更是能把手点麻。直到把项目切到 Docker Swarm 集群,配合docker-stack做企业级部署,我才算是把这套流程整舒坦了。这篇文章聚焦的就是这一套组合拳里最实用的部分:用 docker-stack 文件在 Swarm 集群上搞定标准化的企业部署。
这套玩法适合谁?适合手里已经有 Swarm 集群(不管三台还是一个节点),还在靠手工或者笨办法维护服务的运维和开发同学。stack 文件的价值在于把整个应用的运行状态写进一个声明式配置文件里:集群里跑哪些服务、每个服务跑几个副本、怎么更新、怎么限制资源、怎么自愈,全写明白。改完配置一条命令执行,Swarm 自己会去对齐实际状态和期望状态,扩缩容、滚动更新、故障拉起全自动化。它的上手成本比 Kubernetes 低得多,但没有牺牲掉生产环境最在乎的那几个能力:滚动更新、服务发现、安全加密、资源限制。下面我把这次 enterprise-deployment 的完整思路、具体配置和经验坑位一次说清楚。
1. docker-stack 与企业部署的整体设计思路
1.1 为什么企业环境选 docker-stack 而不是挂着 compose 硬上
单机场景下 docker-compose 确实好用,YAML 一写,up 一下,几个容器就起来了。但企业环境落到集群里,compose 立刻露怯:它默认只在一台机器上生效,没有任何跨节点的服务编排能力。你要是手动在每台机器上各跑一份 compose,服务副本是多了,但它们各自为战,没有统一的服务发现和负载均衡,某个副本挂了也不会在别的节点上被自动拉起来。这本质上还是在做“脚本化部署”,没有做“集群编排”。
docker-stack是 Swarm 模式下的部署单元,它把 compose 文件里描述应用的方式升级成了集群级别的“期望状态声明”。同一个文件,你用docker-compose up是跑一组容器,用docker stack deploy则是让 Swarm 集群的调度器来决定如何把这些服务落到节点上。调度器会持续盯着实际运行状态,副本数对不上就补,节点挂了就把任务迁移到健康节点,更新出问题还能自动回滚。企业部署最怕的不是发布失败,而是发布失败后没人知道、不知道回滚到哪个版本、服务永远在半死不活的状态。stack 这套“声明期望状态 + 持续收敛”的机制,直接把这些运维风险按住了。
1.2 声明式部署带来的运维范式转变:从“执行命令”到“描述状态”
我最初从 compose 转 stack 的时候,最大的思维障碍是把 stack 文件当成 compose 文件的语法升级版。实际上两者的工作方式差别很大。compose 是你告诉 Docker“把这些容器跑起来”,然后命令执行完、容器状态稳定了,任务就结束了。stack 则是你告诉 Swarm“我的应用跑起来之后应该长这样”,Swarm 从收到这份声明的时刻起,就背着这个期望状态一直干活。
举个实际的例子。线上有个服务设计 3 个副本,运行过程中某个节点上的副本因为宿主机内存不足被系统直接杀掉了。用 compose 的方式,你发现服务只剩两个副本,需要登录机器、找到原因、重新 up。用 stack 的方式,Swarm 的调度器在几秒钟之内就会发现期望副本数和实际数量不一致,自动在可用节点上把新副本拉起来。整个过程中你甚至不需要收到告警去做恢复操作。这背后的机制是 Swarm 的reconciler(状态协调器),它一直循环比对“想跑的”和“正在跑的”,有缺口就执行 create/start,多出来了就 stop/remove。
这些机制在企业部署里还有一个隐形收益:环境漂移被遏制了。以前哪台机器上改过什么配置、谁能说得清?共享同一个 stack 文件做发布,线上所有节点上跑的容器都只是这份文件当前状态的一个投影。新加的节点会被调度器自动填充服务,坏节点上的服务会被自动驱逐。机器的具体状态已经不重要了,重要的是这份 stack 文件被集群“收敛”成了什么样子。
1.3 方案选型背后的取舍:为什么这个组合能抗住生产压力
有人会问,既然是“企业级部署”,为什么不直接上 Kubernetes,反而在 Swarm 上折腾?这个问题我实际对比过。Kubernetes 确实更强大,生态更丰富,但它的学习曲线和运维成本也不是一个量级。要是你们的业务规模还没到需要自定义 CRD、服务网格、复杂弹性伸缩策略的程度,Swarm 加 stack 的性价比其实非常高。首先它不需要单独部署控制面组件,Swarm 的 manager 节点就是控制面,worker 节点装上 Docker 加入集群就行,没有一堆 etcd、kubelet、各种 controller 的维护负担。其次 stack 文件可以直接沿用团队熟悉的 compose 语法,原有 docker-compose.yml 改一改就能变成生产部署文件。
如果再往深看一层,Swarm 在内置服务发现和本地 DNS 这方面做得非常顺滑。一个服务api在 stack 里定义之后,任何同网络的服务都可以直接通过api这个服务名访问到它,Swarm 自带的 ingress network 还会在节点间做流量负载均衡。这种开箱即用的体验,配合最小化的运维心智负担,就是它适合企业落地的原因。企业部署要的不是功能最多的平台,而是长期最容易维护、故障时最容易排查的那一套。Swarm 加 stack,恰好这种中庸务实路线的代表。
2. 企业级 stack 文件的核心细节与关键配置项解析
2.1 stack 文件与 compose 文件的关键差异:先避开这三个坑
把一份 compose 文件直接拿来做 stack deploy,大概率会报错,或者跑起来和预期不符。企业环境里写 stack 文件,必须先分清哪些配置是 compose 专用、哪些是为 Swarm 调度的。这里有几个最关键的差异。
第一是version 版本。stack 的version字段需要是 3.0 以上的格式,建议直接写3.8或者更高,取决于 Docker 引擎版本。低版本的 compose 格式虽然在单机模式下跑得欢,但很多集群调度字段(比如deploy.replicas、deploy.placement)根本不被解析。
第二是deploy字段体系。stack 文件里最核心的编排配置全在deploy这个 key 下面。compose 文件也有deploy字段,但在单机模式下纯属摆设,Docker 会忽略它。在 stack 模式里,deploy才是主菜:replicas、placement、update_config、restart_policy、resources,这些字段直接决定了服务在集群里的命运。
第三是网络和卷的生命周期差异。compose 里你写networks:会顺手帮你建一个网络。stack 里如果你指定外部网络,这个网络必须在 deploy 之前已经存在于 Swarm 集群中。卷也一样,driver_opts指定的本地路径、NFS 配置等都需要事先确认在这些宿主机上有效。企业环境我最推荐的模式是把网络创建和应用部署分开:网络用一次docker network create -d overlay建好,标注为 external,stack 文件里引用它。这样的好处是多个 stack 之间可以安全地共享网络,服务跨 stack 通信也不成问题。
2.2 更新策略、重启策略与健康检查:构成无损发布的最小组合
企业部署最怕的是发布过程导致请求失败或服务中断。stack 文件里的deploy.update_config专门解决这个问题。我一般会这样配置:
deploy: update_config: parallelism: 1 delay: 10s failure_action: rollback monitor: 30s order: start-first这里的逻辑是:并行度设为 1,每次只更新一个副本。一个副本更新完成之后,等待 30 秒观察它是否健康,没问题再更新下一个。如果某个副本更新后没有通过健康检查,failure_action: rollback会让整个更新回滚到上一个版本。order: start-first表示先把新副本启动起来,确认可用后再停掉旧副本。这组配置组合起来的效果就是:发布过程中始终有旧版本在对外服务,副本逐个替换,任何一个失败都不会带崩全局。
当然start-first的前提是你能提供“健康检查”这个判断标准。健康检查写在哪?不是写在deploy里,而是写在 service 的healthcheck字段。Swarm 调度器会执行这个健康检查,作为服务是否处于 ready 状态的依据。以 Nginx 为例:
healthcheck: test: ["CMD", "curl", "-f", "http://localhost/healthz"] interval: 30s timeout: 3s retries: 3 start_period: 10sstart_period特别有用,它给新启动的容器一个预热时间,避免因为启动初期依赖还没就绪就被误判为不健康。这个字段加上之后,发布误暂停率肉眼可见下降。注意健康检查命令用到的工具(比如curl)必须存在于镜像里,否则测试永远是失败的。我在生产环境见过因为这个原因导致的“服务明明活着,Swarm 却说 unhealthy”的怪事。
2.3 资源限制与任务放置:让集群资源按你的意愿流动
企业环境里不同节点性能往往不一样,有些机器内存大一些,有些磁盘快一些。如果不对服务做放置约束,Swarm 调度器只按默认策略分配任务,很可能会把一个重数据库服务调度到一台小内存的机器上,夜深人静的时候直接 OOM。我用deploy.placement.constraints做节点角色区隔已经有两年多了,模式很固定:数据库和有状态服务放专用节点,无状态 API 服务放普通工作节点。
deploy: placement: constraints: - "node.labels.role == db"这里node.labels.role是节点的自定义标签,部署前先给节点打上标签:
docker node update --label-add role=db worker-node-01 docker node update --label-add role=app worker-node-02资源限制上也要注重实际效果。resources.limits设置上限是为了不让某个失控的服务吃光母机内存,resources.reservations设置预留是为了让调度器在部署的时候找到符合条件的节点,避免任务被分配到资源不足的机器上。两者的意义不同,生产 stack 里我都会写。比如:
deploy: resources: limits: cpus: "0.50" memory: 512M reservations: cpus: "0.25" memory: 256M有一个经验,reservations不要写得太高,否则集群中只要有一两台节点资源紧张,整个服务扩容就会一直卡在 pending 状态。我见过同事把内存预留写成了 8G,结果集群明明有 80G 空闲,却因为没有任何单台节点有连续 8G 可分配而无法部署——调度器的逻辑是找单台符合条件的节点,不是看集群累计资源。
3. 企业级多服务栈的完整实操部署过程
3.1 先规划设计一个多服务应用栈:包含 Nginx、API、Worker、Redis、PostgreSQL
为了演示得足够具体,我构建一个常见的电商微服务场景:nginx做前端反向代理和静态资源服务,api提供 HTTP 接口,worker处理异步任务队列,redis做缓存和任务队列,postgres做主数据库。五个服务,有状态和无状态混合,需要跨网络访问,还涉及服务间依赖关系。这个规模虽然不豪华,但涵盖的编排特性已经足够拆开分析。
网络规划上,我准备两个 overlay 网络:frontend-net用于暴露给外部的 Nginx 和其他需要被外部访问的服务,backend-net用于内部服务之间的通信。Nginx 在两个网络里都有入口,API 和 Worker 连接backend-net,Redis 和 PostgreSQL 只待在backend-net里,不向外暴露任何端口。这样外部的流量只能到达 Nginx,内部数据层完全不和外部网络打交道,攻击面被有效压缩。
目录规划和单机 compose 不一样的是,stack 部署不依赖当前目录,文件只在 deploy 时被读取。但我强烈建议把 stack 文件放进 Git 仓库,环境变量文件不要进仓库(这个后面说)。项目目录组织成这样:
prod-stack/ ├── stack.yml ├── .env └── nginx/ └── nginx.conf3.2 编写一份可落地的生产级 stack.yml(完整文件拆开讲)
下面这份 stack 文件是我从实际项目里简化抽象出来的,可以作为企业部署的起点模板。
version: "3.8" networks: frontend-net: external: true backend-net: external: true volumes: pg-data: driver: local redis-data: driver: local services: nginx: image: nginx:1.24-alpine ports: - "80:80" - "443:443" networks: - frontend-net - backend-net volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro deploy: replicas: 2 placement: constraints: - "node.role == worker" update_config: parallelism: 1 delay: 5s order: start-first restart_policy: condition: any healthcheck: test: ["CMD", "curl", "-f", "http://localhost/healthz"] interval: 10s timeout: 3s retries: 3 start_period: 10s api: image: registry.example.com/api-server:1.4.2 networks: - backend-net environment: - DB_HOST=postgres - REDIS_HOST=redis - WORKER_QUEUE=default deploy: replicas: 3 placement: constraints: - "node.labels.role == app" update_config: parallelism: 1 delay: 10s failure_action: rollback monitor: 20s order: start-first restart_policy: condition: any delay: 5s resources: limits: cpus: "0.50" memory: 512M reservations: cpus: "0.25" memory: 256M depends_on: - postgres - redis worker: image: registry.example.com/api-server:1.4.2 command: ["python", "worker.py"] networks: - backend-net environment: - DB_HOST=postgres - REDIS_HOST=redis deploy: replicas: 2 placement: constraints: - "node.labels.role == app" restart_policy: condition: any resources: limits: cpus: "0.50" memory: 512M redis: image: redis:7-alpine command: ["redis-server", "--appendonly", "yes"] networks: - backend-net volumes: - redis-data:/data deploy: replicas: 1 placement: constraints: - "node.labels.role == db" restart_policy: condition: any healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 2s retries: 3 postgres: image: postgres:15-alpine networks: - backend-net volumes: - pg-data:/var/lib/postgresql/data environment: POSTGRES_DB: appdb POSTGRES_USER: appuser POSTGRES_PASSWORD_FILE: /run/secrets/pg_password secrets: - pg_password deploy: replicas: 1 placement: constraints: - "node.labels.role == db" restart_policy: condition: any healthcheck: test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"] interval: 10s timeout: 5s retries: 5 start_period: 15s secrets: pg_password: external: true这个文件里有几个企业部署的关键细节,值得单独拎出来说。第一个是environment里的POSTGRES_PASSWORD_FILE而不是POSTGRES_PASSWORD。这是 PostgreSQL 官方镜像支持的环境变量变体,它从文件读取密码。配合 Swarm secrets,真正的密码不会明文出现在 stack 文件和容器环境变量里。第二个是depends_on,stack 模式下它只是控制启动顺序的一个提示,Swarm 调度不会严格等 postgres 完全就绪再启动 api,所以 service 本身的健康检查和重试机制才更值得依赖。第三个是nginx服务的./nginx/nginx.conf相对路径挂载,在 stack 部署下这个路径依赖你执行docker stack deploy时所在目录,企业环境建议写绝对路径,或者干脆把配置也打成镜像。
3.3 网络、卷、Secrets 的预先准备:部署五分钟前的必要动作
stack 文件里external: true的网络和 secrets 不会由 stack 帮你创建。在实际 deploy 之前,需要手动检查这些前置资源。我自己一般写上一个小脚本,把前置准备工作固定下来:
# 检查 overlay 网络 docker network inspect frontend-net >/dev/null 2>&1 || \ docker network create -d overlay frontend-net docker network inspect backend-net >/dev/null 2>&1 || \ docker network create -d overlay backend-net # 创建 secrets(实际项目中密钥来源是内部密钥管理系统,这里是示意) printf 'your-strong-password-here' | docker secret create pg_password - # 检查节点标签 docker node update --label-add role=db worker-node-db-01 docker node update --label-add role=app worker-node-app-01 docker node update --label-add role=app worker-node-app-02这里有个细节:docker secret create是从标准输入读取数据的,管道里不要加echo的那种换行。否则 secrets 里会带一个\n,密码对照的时候永远匹配不上。我用printf就是为了避免这个问题。Secrets 创建之后,它只会被挂载到/run/secrets/<secret_name>,不会以明文形式出现在镜像层或者环境变量里。企业安全审计的时候,这个特性算是一个强需求。
节点标签这个前置动作,顺序很重要。没有标签就执行 deploy,服务会一直卡在 pending 状态,从docker service ps看到的提示是“no suitable node”。这个报错不要慌,基本都是 placement 约束没满足,检查一下标签是否打对、节点是否 active 就行。
3.4 执行部署与验证下发的完整流程:一次成功的 stack deploy 现场记录
前置资源备好之后,执行部署就一句话:
docker stack deploy -c stack.yml prod-stack我这里给 stack 取名为prod-stack,它会作为所有服务名的前缀。部署完成之后,用docker stack services看一眼全局状态:
docker stack services prod-stack正常情况下输出大概这样(简写):
| 服务名 | 副本数 | 镜像 | 状态 |
|---|---|---|---|
| prod-stack_api | 3/3 | registry.example.com/api-server:1.4.2 | Running |
| prod-stack_nginx | 2/2 | nginx:1.24-alpine | Running |
| prod-stack_worker | 2/2 | registry.example.com/api-server:1.4.2 | Running |
| prod-stack_postgres | 1/1 | postgres:15-alpine | Running |
| prod-stack_redis | 1/1 | redis:7-alpine | Running |
3/3的含义是期望副本数 3,当前运行副本数 3。如果显示2/3,说明有一个副本还没有起来或者正在启动,等几秒再看。个别情况下会变成2/3然后卡住,大概率是 placement 约束无法满足,或者镜像在节点上拉取失败。这时候用docker service ps看详细情况:
docker service ps prod-stack_api --no-trunc这个命令会列出这个服务的每个任务和它们的历史状态。我会重点看两个字段,一是CURRENT STATE,二是ERROR。比如显示Assigned但迟迟不变成Running,说明调度器已选好节点但镜像在下载或者启动失败;显示Failed并带Task non-zero exit (1),就要去查容器本身的日志了。
整体验证通过后,再通过 Nginx 的入口地址确认对外访问是否正常。因为 Nginx 有两个副本,并且通过 ingress network 绑定了宿主机 80/443 端口,访问任意 worker 节点的 IP 都应该能通。此时整个栈已经稳定运行在集群上了。
4. 企业级 Stack 的更新、版本管理与故障转移机制
4.1 滚动更新的完整机制与回滚实操记录
企业部署里不可能一直不动配置。代码升级、环境变量调整、镜像 tag 更改,都是常态。我在上面把update_config配置成了parallelism: 1和failure_action: rollback,这意味着更新是逐个替换副本并且坑自动回滚的。实际触发更新很简单,还是同一句话:
docker stack deploy -c stack.yml prod-stack只要你修改了 stack.yml 中某个服务的镜像 tag,重新执行部署命令,Swarm 会检测到期望状态变化,启动滚动更新。这时候注意观察docker service ps输出的变化,你会发现旧任务状态变成Shutdown,新任务依次经历Preparing、Starting、Running。配合docker service logs能看到新旧版本交替期间的日志流。
滚动更新的监控窗口要稍微说清楚。我配置了monitor: 20s,意思是 Swarm 在滚动更新时会持续检查新任务 20 秒,确认它们处于 healthy 状态后才算本轮更新成功。如果这 20 秒里健康检查失败,Swarm 会执行failure_action: rollback,整个服务回滚到之前的版本。回滚过程中同样按 parallelism 逐个替换,不会直接一把梭全部重启。
这里有一个我在生产环境踩过的大坑:别把monitor设得太短。之前我设过 5 秒,结果服务里有一个任务启动较慢,前 5 秒健康检查还没通过,Swarm 认为是发布失败,直接把所有副本全部回滚了。那次发布被误回滚了三次,吓得我看日志才发现只是启动慢。start_period: 10s能缓解一部分问题,但monitor本身也别小于 15 秒,给服务留足预热时间。
4.2 使用配置变更、服务名与服务发现完成平滑过渡
企业服务更新不只是镜像版本更新,配置项和环境变量也会频繁改动。stack 模式支持直接修改 stack.yml 里的environment或挂载配置,然后重新 deploy。这里有一个细节:如果只改了环境变量,Swarm 会创建一个新的 Task 模板,同样走滚动更新流程。但是如果你在 stack.yml 里改了command,这个改动在docker stack deploy时会被检测到,并触发容器的重新创建。
另外一个容易被忽略的点:Swarm 自带的服务发现是“名字即服务”。在同一个 overlay 网络里,服务之间的通信用的是服务名而不是 IP。比如api服务要访问postgres,写jdbc:postgresql://postgres:5432/appdb就行。Swarm 的 DNS 解析会把postgres解析到当前所有这个服务对应的任务 IP 上。如果你的 DB 服务只有一个副本,DNS 解析就直接指向它;如果将来做了主从,主服务的名字一样能用。这就要求团队成员在连接串里别写 IP,一定要用服务名。IP 是动态的,Swarm 重新调度后任务 IP 就会变,写死 IP 等于让服务退化成手动维护。
4.3 多节点故障转移与数据持久化的兜底策略
Swarm 的故障转移只对无状态服务是真正的“无缝”。节点挂了,nginx 和 api 这种无状态服务会被调度器自动在其他健康节点重建。但 redis 和 postgres 这种有状态服务,情况要复杂得多。我在 stack 里给它们设置的副本数是 1,并且通过 placement 约束固定在 db 节点上。一旦这个节点整体宕机,Swarm 能感知到节点进入 down 状态吗?能,但这个有状态服务不会自动在其他节点重新启动,因为 Swarm 不会帮你迁移本地卷数据。除非你把卷交给了分布式存储(比如 NFS、Ceph、云厂商的块存储),否则数据还在宕机节点的磁盘里,强行在其他节点拉起来只会得到一个空数据库实例。
所以企业环境里对有状态服务的故障兜底,我不会依赖 Swarm 的自动重建,而是靠三层设计来保证。第一层:数据库本身做主从复制,故障时做手动或半自动的主从切换,这是真正能保住数据的手段。第二层:卷挂载尽量使用网络存储,保证节点级别的故障不至于数据丢失。第三层:Swarm 的restart_policy解决进程级别的崩溃,比如 redis 进程挂掉了,调度器会在当前节点重启容器。不要指望 Swarm 能帮你处理数据节点的整体故障,它始终是编排工具,不是容灾系统。
5. 实战中的常见故障与排查实录
5.1 服务副本一直调度不起来,卡在 pending 状态
这是 stack 部署中最常见的问题,几乎每个新手都要踩一次。现象是docker stack services里副本数一直是期望值/0或者期望值/2。排查路径我也整理成自己的一套顺序:
- 先跑
docker service ps prod-stack_api --no-trunc,看到CURRENT STATE是Assigned还是Pending。 - 如果
Assigned,说明调度器已经安排了节点,卡在镜像拉取或任务启动阶段。查看节点端docker service ps不会有更多信息,要docker service logs看容器日志,或者到对应节点上docker ps -a找退出的容器看报错。 - 如果一直是
Pending,基本就是 placement constraints 没有满足,检查节点标签:docker node ls docker node inspect <node-id> --format '{{.Spec.Labels}}' - 还有一种隐蔽情况:集群里所有节点都在 Drain 维护模式。
docker node ls里MANAGER STATUS和AVAILABILITY两列看仔细,Drain节点上不会分配新任务。
5.2 服务之间域名解析不了:跨网络通信故障
服务在一个 overlay 网络里互相用服务名访问,关键前提是它们连了同一个 overlay 网络。我的经验是,出问题时 80% 是服务没连同一个网络,而不是 DNS 本身的问题。排查方式直接走进容器的实际网络环境里测试:
docker exec -it <api-container-id> ping redis docker exec -it <api-container-id> getent hosts redisgetent hosts能返回 DNS 解析结果,如果返回空,说明当前容器根本不在redis服务所属的 overlay 网络里。回头检查 stack.yml 里该服务的networks列表。另外,overlay 网络的 DNS 只对“加入该网络的任务”生效,两个服务即便都在集群里,只要没连同一个 overlay,相互之间就是两个世界。跨 stack 的服务通信也一样,必须共享同一个 external overlay 网络。
5.3 滚动更新后服务瞬间不可用,健康检查配置的锅
明明配置了order: start-first,为什么更新瞬间流量还是报错了?我遇到过这个问题。排查到最后,发现原因是新版容器虽然启动了,但旧容器还没完全退出,新旧容器同时存在的一小段时间里,健康检查命令对新容器返回的 “starting”状态过快导致 ingress 负载均衡器认为它不健康,暂时把所有流量都打给了正在退出的旧容器。更本质的问题是我给的健康检查里没有start_period,容器启动初期 Docker 默认把它当成 unhealthy,而 ingress 网络对 unhealthy 任务会进行剔除处理。现象就是那一两秒里新任务还没被标记为健康,旧任务已经处于 shutting down,外部请求直接打到正在停止的容器上。
解决方式:健康检查必须包含start_period,建议设置成应用实际启动耗时的 1.5 到 2 倍。再配合update_config.monitor的长度,给新任务足够的观察期。如果服务本身依赖数据库等外部组件,健康检查脚本里也最好只检查自身存活状态,不要一启动就去连数据库,不然数据库稍微抖一下,发布就会被误判成失败。
5.4 企业实战中容易栽的几个隐藏深坑(配置细节速查表)
落到一张表上,团队新同学照着核对,能省掉我当年到处踩坑的时间。下面列出的是我经过多轮生产事故提炼出的要点。
| 容易出问题的配置项 | 错误示例 | 正确做法 | 核心原因 |
|---|---|---|---|
version字段 | version: "2" | version: "3.8"或更高 | stack 的编排字段需要 3.x 格式解析 |
external: true网络 | 未预先创建网络直接 deploy | 先docker network create -d overlay xxx | Swarm 不会为 external 网络自动建网 |
| secrets 创建 | echo "pass" | docker secret create pg_password - | 用printf去掉换行符 | 换行符会变成密码内容的一部分 |
| placement 标签 | 部署后再想起来打标签 | deploy 之前先docker node update --label-add | 标签缺失导致任务永远 pending |
| healthcheck 定义 | 不写start_period | 设置预热期,覆盖应用启动时间 | 启动慢被误判为 unhealthy 导致回滚 |
| 相对路径挂载 | ./nginx.conf:/etc/nginx/nginx.conf | 使用绝对路径 | 相对路径取决于执行 deploy 的目录,容易不一致 |
| 服务间访问 | 写容器 IP 或者宿主机 IP | 一律用服务名 | Swarm 调度后 IP 会变,服务名由 DNS 接管 |
6. 企业部署的运维实操总结与个人经验
6.1 一套有实际价值的日常运维操作清单
stack 部署稳定跑起来之后,日常运维动作会非常固定。我把自己平时用到的命令整理成了一份操作清单,基本可以覆盖 80% 的日常工作。
查看整体状态,用docker stack ps prod-stack会显示所有服务的任务分布,比docker service ls直观多了。查看某个服务的实时日志,docker service logs -f prod-stack_api会把所有副本的日志混合输出,副本多的时候建议加--tail 200先看尾部。想单看某个容器,先docker service ps拿到节点和任务 ID,再到对应节点上用docker logs查看。
扩缩容有两种路径。临时快速扩容(比如大促前把 api 从 3 个扩到 10 个)可以用:
docker service scale prod-stack_api=10但注意docker service scale改的是运行时的服务配置,不会回写到 stack 文件。下次执行docker stack deploy会把副本数改回 3,所以生产环境我建议用这个命令做临时热调整,但最终要记得把新副本数同步到 stack.yml 里重新 deploy,避免配置漂移。长期扩容直接改 stack.yml 的replicas,再执行 deploy 即可。
6.2 版本标识与发布流程的工程化方法
stack 文件管理的核心问题是如何让发布可追踪。我的做法是在每个服务的镜像 tag 里带上构建号,比如registry.example.com/api-server:1.4.2-build.218。stack.yml 里的 tag 每次发布都更新,git 提交记录里就能精确对应到“什么时间发布了哪个版本”。如果搭配 CI/CD,每次构建镜像后自动改 stack.yml 的 tag,再自动执行 deploy,就是一套很轻量的 GitOps 流程了。不用引入额外的工具,底层全靠 Swarm 的期望状态收敛能力。
另外一个心得是:环境差异不要硬塞进同一个 stack 文件。开发和生产的网络拓扑、资源限制、副本数都不同。我习惯拆成stack-base.yml和按环境区分的覆盖文件,但 docker stack 不像 docker compose 那样原生支持-f多个文件合并 override。所以我更常用的方案是:维护一份模板 stack.yml,用脚本替换环境变量生成最终文件。变量集中放在.env,但.env不进仓库,仓库里只放.env.example。这样环境差异被隔离,也不会把生产密码泄露到代码库。
6.3 从“能跑”到“敢升级”:我对这套方案的实际感受
这套 stack 部署方案在我这边稳定运行之后,最大的改变不是命令敲得少了,而是我的“胆量”变大了。以前每次发布都紧张,担心某台机器状态不对、某个服务忘了更新、流量切过去之后起不来。现在发布就是一版 stack.yml 的变更,diff 越看越安心,deploy 之后 Swarm 自动滚动、自动健康检查、自动回滚,我要做的只是观察监控面板上有没有异常曲线。这让我真正感受到了“平台化”的收益:不是非得搭一个 Kubernetes 才算平台化,关键是所有服务都统一享受同一种调度、同一种部署、同一种自愈逻辑,这就是最普惠的平台能力。
我个人还有一个小建议,如果你还没完全信任 Swarm 的自动更新,可以先把failure_action从rollback改成pause跑一段时间。这样 Swarm 发现问题会暂停更新,而不是自作主张回滚,等你确认没问题再重新设置成rollback。随着发布次数增多、信心建立了,再切换到自动回滚。这套渐进策略比一上来就全自动要稳得多。部署这件事本质上是求稳的,让层层的保障机制替你守住每一次变更,你才能真正把精力花在业务架构上,而不是灭火。