本文讨论的是少量服务、少量 VPS 和小团队的生产交付,不把 Docker Compose 包装成集群调度器,也不把大型公司的平台原样缩小。重点是提取可以落到小系统里的原则:同一制品、状态外置、分批发布、业务验证、显式回滚和可恢复的数据。
一、先给结论:Compose 的上限不由服务数量单独决定#
Compose 解决的是一台 Docker 主机上多个容器的声明、网络、卷、依赖和生命周期管理。它不原生负责跨主机调度、节点故障迁移、全局滚动发布或控制面高可用。
因此,判断 Compose 是否仍然合适,应该连续问五个问题:
- 单台主机故障后,业务允许多长时间恢复?
- 应用能否在任意节点重建,还是依赖本地会话、上传和数据库?
- 发布失败时,能否在几分钟内切回一个已经验证的镜像?
- 多台 VPS 的配置、版本和密钥是否仍然可以清楚审计?
- 团队真正缺的是跨主机调度,还是尚未做好备份、监控和发布纪律?
如果前四个问题有明确答案,而机器数量仍少,Compose 可以是成熟选择。相反,即使只有三台 VPS,只要经常发生版本漂移、跨节点状态耦合和恢复失控,问题也已经超出“再写一个部署脚本”的范围。
代码提交 ↓测试并构建一次 ↓带 commit SHA 的不可变镜像 ↓按节点部署同一个 digest ↓业务探针和指标验证 ├─ 通过:继续下一节点 └─ 失败:停止并切回旧 digest二、把生产系统拆成四个边界#
1. 制品边界:构建一次,而不是每台机器各自构建#
生产服务器不应执行 git pull && docker compose build。这样做会让构建结果受到服务器缓存、网络、基础镜像更新和本地文件影响,同一个 Git 提交可能得到不同镜像。
更稳妥的路径是:
- CI 在受控环境中完成测试和镜像构建。
- 镜像同时标记 commit SHA,并记录不可变 digest。
- 预发布和生产拉取同一个 digest,不重新构建。
- 保留前一个已经验证的 digest,作为回滚目标。
- SBOM、漏洞扫描结果和签名与该制品关联,而不是与一个可移动的
latest标签关联。
latest 可以作为方便人阅读的提示,但不能成为生产系统唯一的版本身份。镜像 digest 才能回答“现在实际运行的是哪一组字节”。
2. 运行边界:容器可替换,数据不可假装无状态#
下面这些内容一旦只存在某台应用 VPS,本质上就把应用节点变成了状态节点:
- 登录 Session。
- 用户上传文件。
- SQLite 或容器内数据库。
- 本地任务队列。
- 只写在容器文件系统里的日志。
- 只保存在某台机器上的加密密钥。
多 VPS 的前提不是“把同一份 Compose 文件复制三次”,而是应用实例能够被替换:Session 放到共享存储或使用可验证的无状态令牌,上传进入对象存储,数据库有独立生命周期,日志离开短生命周期容器。
CDN / 负载均衡器 ├─ VPS A:反向代理 + App ├─ VPS B:反向代理 + App └─ VPS C:反向代理 + App
独立状态层 ├─ PostgreSQL / MySQL ├─ Redis(缓存或会话,不自动等于主数据) └─ 对象存储三台 VPS 各跑一个 MySQL,并不会自动形成共享数据库或高可用系统。复制拓扑、写入入口、故障选主、脑裂防护、备份和恢复仍需单独设计。
3. 流量边界:进程存活不等于可以接流量#
健康检查至少分三层:
| 层次 | 要回答的问题 | 典型检查 |
|---|---|---|
| 进程 | 容器内进程是否仍在运行 | Docker healthcheck、进程退出码 |
| 依赖 | 应用是否能访问关键依赖 | 数据库轻查询、必要配置是否加载 |
| 业务 | 用户关键路径是否正确 | 登录、读写、下单或内容页的 smoke test |
健康端点不要执行昂贵的全表查询,也不要把所有非关键依赖都设为硬失败。否则监控本身会放大故障,或因一个次要服务抖动把全部应用节点摘除。
外部负载均衡器的检查还有一个重要作用:只有它确认新节点健康后,才把真实流量导入。Cloudflare Load Balancing、AWS Elastic Load Balancing、HAProxy 和 Nginx 都可以承担这一层,但“DNS/CDN 正常”不能证明源站业务正常。
4. 控制边界:CI 有发布权,不代表它应持有永久 root 密钥#
CI/CD 是生产控制面,泄露后的影响通常比单个应用漏洞更大。最低限度应做到:
- Pull Request 检查与生产部署权限分离。
- 生产环境有审批、分支限制和部署并发锁。
- 云平台优先使用 OIDC 换取短期凭据。
- 必须使用 SSH 时,部署账号只获得必要命令和目录权限。
- 不把私钥写进镜像、仓库、构建日志或 Compose 文件。
- fork PR 和不可信贡献不能读取生产 Secret。
- 记录谁、何时、把哪个 digest 部署到了哪些节点。
GitHub Actions 的 OIDC 模式体现的是“工作流身份换短期令牌”;GitLab 的 Protected Environments 体现的是“谁可以把什么部署到受保护环境”。具体产品可以不同,信任边界不能省略。
三、一个可落地的单 VPS 方案#
单机并不意味着可以忽略生产纪律。一个适合个人产品或小团队的基线如下:
Internet ↓Caddy / Nginx(TLS、限流、访问日志) ↓ Docker 内部网络App + Worker ↓独立数据库 / 对象存储Compose 文件只描述运行所需内容,环境差异通过受控变量或密钥注入。示意配置如下:
services: app: image: ghcr.io/example/app@sha256:${APP_DIGEST} restart: unless-stopped env_file: .env.production networks: [private] healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health/ready"] interval: 15s timeout: 3s retries: 5 start_period: 30s read_only: true tmpfs: [/tmp] security_opt: [no-new-privileges:true] logging: options: max-size: "20m" max-file: "5"
networks: private: internal: true这段配置不是通用模板。镜像里未必有 wget,只读根文件系统也可能与现有应用不兼容。成熟做法是根据应用实际行为验证约束,而不是复制完就认为安全已经完成。
发布状态机#
一次发布应有确定的前态、动作、验证和失败路径:
记录 CURRENT_DIGEST ↓拉取 TARGET_DIGEST ↓更新 APP_DIGEST 并启动 ↓等待容器健康(有上限) ↓执行外部 smoke test ├─ 成功:写入 LAST_KNOWN_GOOD └─ 失败:恢复 CURRENT_DIGEST,重新启动并报警关键不在脚本写得多复杂,而在于:失败不能默默继续,等待必须有超时,回滚引用的是旧制品而不是重新构建旧代码,回滚后还要再次验证。
数据迁移不能跟着容器盲目回滚#
应用镜像通常可以回滚,数据库变更未必可以。生产发布更适合使用 expand/contract:
- 先新增兼容字段或表,不删除旧结构。
- 发布同时兼容新旧结构的应用。
- 回填并核对数据。
- 切换读写路径并观察。
- 确认旧版本不再需要后,再删除旧结构。
如果一次发布包含不可逆 DDL,就必须在发布前写清恢复方案、维护窗口和最大可接受数据损失,而不是把 docker compose down 当成回滚。
四、多 VPS 的关键是逐节点,而不是同时执行#
假设有三台无状态应用 VPS,最简单可靠的策略通常是串行滚动:
- 从负载均衡器摘除 VPS A。
- 等待既有连接排空。
- 在 A 部署目标 digest。
- 运行本机健康检查和外部 smoke test。
- 把 A 加回流量池,观察错误率、延迟和业务成功率。
- 达到观察窗口后,再处理 B 和 C。
任何一步失败都停止后续节点。这样即使新版本有问题,B、C 仍保留旧版本。对三台小机器而言,这个串行过程往往比引入一套复杂编排平台更容易理解和恢复。
发布期间必须防止并发#
两个生产工作流同时运行会造成节点版本交错。GitHub Actions 的 concurrency、GitLab 的 resource_group,或部署端的一把排他锁,都可以确保同一环境同一时间只有一个发布者。
Canary 看什么#
Canary 不是“先更新一台”这一个动作,还要定义比较信号:
- HTTP 5xx 和业务错误码。
- p50、p95、p99 延迟,而不只看平均值。
- 登录、支付、写入等关键业务成功率。
- 数据库连接池耗尽、慢查询和锁等待。
- CPU、内存、文件描述符和重启次数。
- 与旧版本相比是否出现显著偏离。
没有判定窗口和阈值的 Canary,只是延迟了全量发布。
五、海外公司的公开实践,应该借什么#
GitHub:工作流身份与环境保护#
GitHub Actions、Environments、GHCR 和 OIDC 可以组成一条完整交付链:PR 执行验证,主分支构建镜像,受保护环境控制生产审批,OIDC 向云平台换取短期令牌。值得借鉴的是减少永久凭据和限定发布环境,不是照抄一份 Actions YAML。
对于 VPS,OIDC 未必能直接替代 SSH;此时仍可以使用受限部署账号、短期证书或由服务器主动拉取已批准制品,缩小 CI 到生产的权限。
GitLab:把环境当成可审计对象#
GitLab CI/CD 的 environments、protected environments、deployment approvals 和 deployment history 强调:流水线结束不是交付结束,环境中的版本、操作者和结果才是审计对象。小团队即使不用 GitLab,也应保留同样的发布账本。
Netflix:Kayenta 让指标参与发布决策#
Netflix 与 Google 公开的 Kayenta 自动化 Canary 分析,会比较基线与 Canary 的多组指标并给出判定。它说明两个边界:第一,编排器报告 Ready 只代表探针通过;第二,真正的发布判断需要服务指标和业务指标。
小系统不需要部署完整 Kayenta。固定观察窗口、明确阈值、旧新版本对照和自动停止,已经是在迁移其核心方法。
AWS:计算层可替换,状态服务独立治理#
AWS 常见的 ECS/EKS、RDS、ElastiCache、S3 分层不是产品采购清单,而是在表达生命周期差异:容器可以频繁替换,数据库、缓存和对象存储有不同的复制、备份与恢复策略。即使全部自建,也应保持这条边界。
Cloudflare:边缘健康不代表源站健康#
Cloudflare 的负载均衡健康监控可以探测源站并调整流量,但 CDN 命中、DNS 响应和 TLS 成功都不能证明应用写入路径正常。外部 synthetic check 仍需覆盖一条真实用户路径。
六、安全供应链:先覆盖高收益环节#
工具可以很多,但一条小团队流水线的优先级通常是:
- 固定基础镜像版本并自动接收依赖更新提醒。
- 使用 BuildKit/Buildx 构建,减少不确定的本地差异。
- 用 Trivy 扫描镜像和依赖,定义阻断级别与例外期限。
- 用 Syft 等工具生成 SBOM,跟随制品保存。
- 用 Cosign 或平台原生 attestation 记录制品来源。
- 部署端验证仓库、digest 和必要签名。
扫描结果不是“零漏洞证明”。真正重要的是知道哪些漏洞可达、谁负责修复、例外何时到期,以及当前生产到底运行哪个制品。
七、可观测性与故障演练#
至少建立四类信号:
| 类型 | 最小信号 | 主要用途 |
|---|---|---|
| 流量 | 请求量、状态码、关键业务成功率 | 判断用户是否受影响 |
| 延迟 | p50/p95/p99 | 发现尾延迟与依赖退化 |
| 饱和度 | CPU、内存、磁盘、连接池、队列积压 | 判断资源是否接近边界 |
| 变更 | commit、digest、发布节点、操作者 | 把异常与发布关联 |
Prometheus、Grafana、Loki 和 OpenTelemetry 是成熟开源选择,但不必一次全部引入。只要指标能够支持发布判定、日志能关联请求和版本、告警能到达实际维护者,系统就已经具备可操作性。
上线前至少演练:
- 镜像仓库不可用时,旧版本能否继续运行。
- 新镜像无法启动时,是否自动停止后续节点。
- 单台 VPS 突然离线时,流量是否被摘除。
- 数据库恢复是否在目标 RTO 内完成,恢复点是否满足 RPO。
- Secret 轮换后,旧凭据是否真正失效。
- 回滚后,数据库 schema 是否仍兼容。
备份任务显示成功不等于可以恢复。只有把备份恢复到隔离环境、核对数据并记录耗时,RTO/RPO 才不是纸面数字。
八、何时继续 Compose,何时升级平台#
继续使用 Compose#
- 主机数量少,逐节点发布仍然清晰。
- 应用已经无状态,数据层独立并可恢复。
- 发布频率和团队规模不需要统一调度控制面。
- 故障主要来自应用和数据,而不是跨主机资源调度。
- 现有脚本可以可靠回答版本、健康、停止与回滚状态。
评估 ECS、Nomad 或 Kubernetes#
- 服务和环境增长使主机放置与容量管理持续出错。
- 多团队需要统一的 RBAC、配额、策略和服务发现。
- 需要自动重新调度、弹性扩缩容和标准化发布控制器。
- 配置漂移与人工登录已经难以审计。
- 团队能够长期承担控制面、网络、存储、升级和安全成本。
迁移之前,先做好不可变制品、探针、资源边界、备份恢复和指标。Kubernetes 可以接管调度,却不会自动修复状态耦合、错误探针或不可逆数据库迁移。
九、生产验收清单#
- 生产只部署 commit SHA 对应的不可变 digest。
- 每次发布保存当前和上一个已验证版本。
- 同一环境有并发锁,失败会停止后续节点。
- 健康检查覆盖进程、依赖和至少一条业务路径。
- 应用节点不保存唯一 Session、上传或主数据库。
- 数据迁移向前、向后兼容,并有独立恢复方案。
- Secret 不进入 Git、镜像、日志或普通 Artifact。
- 负载均衡器能摘除故障节点,且演练过单节点离线。
- 备份做过隔离恢复,RPO/RTO 有实测结果。
- 监控能把错误率和延迟变化关联到具体 digest。
参考资料#
- Docker Compose Specification
- Docker:Compose production considerations
- GitHub Actions:使用 OIDC 强化部署安全
- GitHub Actions:管理部署环境
- GitLab:Protected environments
- Netflix:Automated Canary Analysis at Netflix with Kayenta
- Spinnaker:Canary overview
- AWS Well-Architected Framework
- Cloudflare Load Balancing:Health monitors
- Trivy 文档
- Sigstore Cosign 文档
- SLSA:Supply-chain Levels for Software Artifacts
