FDE Times 数据在招 FDE 职位 838▼55 近 7 天在招职位数的变化(同口径比较):不计入本周新开始追踪的公司的职位近 7 天新增 66在招公司 169支持远程 24%美国薪资中位数 $200k招聘最多 Palantir 95
ENVI

前线部署工程师(FDE)的专业媒体

技能指南 · 指南

没有 K8s,Docker Compose 也能自动回滚坏版本

健康检查、--wait 参数,加上 deploy.sh 里一个 else 分支,新版本未能按时变健康时就自动换回旧镜像。

分享微博

要点速览

  • --wait 会阻塞命令,直到服务变为健康;--wait-timeout 让坏版本导致命令失败,而不是一直挂起。
  • 健康检查返回 0 表示健康,返回 1 表示不健康;2 是保留值,不要使用。
  • 要做到零停机,需要一个反向代理,并且服务中不能声明 container_name 和 ports。
时间线从 0 到 90 秒:启动耗时 20 秒,之后每隔 10 秒做一次健康检查,共三次,因此坏版本在第 50 秒被标记为 unhealthy。30 秒超时会在检查进行到一半时截断,触发一次本不必要的回滚。90 秒超时以橙色阴影标出,位于 50 秒标记之后,留有安全余量。
假设启动耗时 20 秒、每 10 秒检查一次、默认重试三次,坏版本约在 50 秒后就会暴露,deploy.sh 中的 90 秒超时留足了安全余量。来源:示意性计算及文中的 deploy.sh 脚本。

很多客户并不运行 Kubernetes。他们只有一台虚拟机、一个 compose.yaml 文件,以及一位只会敲 docker compose up 的运维人员。晚上 6 点新版本出了故障,能救场的不是什么平台,而是一条懂得说“不”、能自己退回旧版本的部署命令。

本指南要做的就是这样一套东西。最终你会得到:一个带健康检查的 app 服务、一个先行运行的迁移任务,以及一个接收两个版本号(新版本和旧版本)的 deploy.sh 脚本。如果新版本在时限内没有变为健康状态,脚本会自动把旧版本重新拉起来。

你需要安装了 Compose 插件的 Docker、一个带 /health 端点的小型 Web 应用,以及该应用的两个不同镜像标签,比如 v1 和 v2。下面的每条命令都可以先在笔记本上跑通,再带到客户的机器上。

健康检查必须说 Docker 听得懂的话

Docker 读取检查命令的退出码:0 表示容器健康、可以提供服务,1 表示不健康,2 是保留值。--retries 默认是 3,也就是说,连续失败三次后容器才会被标记为不健康。

第 1 步。 在应用的 Dockerfile 中加入:

# 假设镜像中有 curl,且应用监听 8080 端口
HEALTHCHECK --retries=3 CMD curl -f http://localhost:8080/health || exit 1

|| exit 1 不是摆设。很多工具失败时会返回任意错误码;强制改成 1,可以确保永远不会意外返回 2,而 Docker 并不把 2 当作有效状态。

检查: 构建镜像、运行容器,然后查看 docker ps 的 STATUS 列。接着关掉 /health 端点并等待:三次检查失败后,容器应变为 unhealthy。

有一点常被忽略:/health 端点应当检查应用真正依赖的东西,比如数据库连接。一个永远返回 200 的 /health,会让坏版本悄无声息地通过关卡。

启动顺序也是部署的一部分

借助 depends_on 加 condition: service_healthy,Compose 可以让一个服务等到它所依赖服务的健康检查通过后再启动。对于迁移这类一次性任务,service_completed_successfully 条件要求该任务成功结束后,下一个服务才能启动。

第 2 步。 一个精简版的 compose.yaml:

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD", "pg_isready"]   # 已简化,请按你的数据库调整
  migrate:
    image: myapp:${APP_VERSION}
    command: ["./migrate"]
    depends_on:
      db:
        condition: service_healthy
  app:
    image: myapp:${APP_VERSION}
    depends_on:
      migrate:
        condition: service_completed_successfully

${APP_VERSION} 是一个简化假设:镜像标签通过环境变量传入。./migrate 命令同样只是示意,请换成你真实的迁移命令。

检查: 运行 APP_VERSION=v1 docker compose up 并观察日志。migrate 应在 db 变为健康之后才运行,app 应在 migrate 成功退出之后才启动。如果让 migrate 以错误退出,app 就不会被启动。

部署关卡就在两个参数里

--wait 参数让 docker compose up 阻塞,直到服务处于运行或健康状态;它隐含了后台模式,因此不必再加 -d。--wait-timeout 参数设定最长等待秒数,这样坏版本会让命令失败,而不是永远挂起。

两个参数合起来,就把一条启动命令变成了一道是非题,shell 通过退出码就能读懂答案。再加上 --no-deps,只更新 app,不动 db 等关联服务。

第 3 步。 创建 deploy.sh:

#!/usr/bin/env bash
set -u
NEW="$1"   # 例如 v2
OLD="$2"   # 例如 v1

if APP_VERSION="$NEW" docker compose up --wait --wait-timeout 90 --no-deps app; then
  echo "已部署 $NEW"
else
  echo "$NEW 不健康,正在回滚到 $OLD"
  APP_VERSION="$OLD" docker compose up --wait --wait-timeout 90 --no-deps app
fi

现在回滚真的只是一条命令:./deploy.sh v2 v1。半夜不需要记住任何步骤,只要知道哪个标签之前运行良好。

检查: 构建一个 /health 永远失败的 v2-broken 标签,然后运行 ./deploy.sh v2-broken v1。第一条命令应在 90 秒内失败,随后打印回滚提示;docker ps 应显示 app 重新运行 v1 镜像,并处于健康状态。

90 秒不要随手定

算一笔账。假设应用启动约需 20 秒,检查间隔 10 秒。按默认的三次重试,坏版本大约需要 20 + 3 × 10 = 50 秒才会被标记为不健康。

如果设成 --wait-timeout 30,就连一个启动稍慢的正常版本也可能被判为失败,白白回滚。如果设成 600,每次部署失败后,客户都要等十分钟旧版本才回来。先在客户机器上测出真实的启动时间,再把超时设在这个总时长之上,并留出安全余量。

--no-deps 还藏着一个坑:它不碰关联服务,所以这个脚本里 migrate 不会运行。请把迁移作为部署前的独立步骤执行,并且写成向后兼容的形式,因为回滚镜像并不会回滚表结构。v1 必须能在 v2 迁移过的数据库上运行。

零停机需要两个版本同时运行

上面的脚本只回答“新版本是否健康”,并不保证新版本启动期间旧版本仍在提供服务。

Compose 的 deploy 规范区分了两种更新顺序:stop-first 先停掉旧版本;start-first 先拉起新版本,让两者重叠运行一段时间。

如果要让用户完全感受不到中断,就需要后者:交接期间有两个容器同时存活。

--scale 参数会覆盖 Compose 文件中的 scale 设置,让一个服务的多个实例并行运行。社区工具 docker-rollout 是一个 Docker CLI 插件,正是把这个思路自动化:先把实例数翻倍,等新实例起来,再移除旧实例。

但它有限制:服务不能在 Compose 文件中声明 container_name 或 ports,而且需要一个反向代理在新旧容器之间分配流量。

这个限制有其原因:两个容器不能占用主机上同一个固定端口,也不能共用同一个名字。所以由代理持有对外端口,背后的容器创建或移除时,用户毫无察觉。

Compose 规范也有声明式回滚

Compose 的 deploy 规范以声明式方式描述了这种行为。rollback_config 块定义更新失败时服务如何回滚。在 update_config 中,failure_action 取 continue、rollback 或 pause 三个值之一,order 则在前文提到的 stop-first 和 start-first 之间选择。

services:
  app:
    deploy:
      update_config:
        failure_action: rollback
        order: start-first

这是一份精简配置。依赖它之前,先确认客户机器上运行 Compose 文件的工具是否真的遵循这些 deploy 块。如果拿不准,上面的 deploy.sh 脚本更透明,因为它的每一步你都看得见。

把这套流程带到客户的机器上

前线部署工程师(FDE)的典型场景是:一台本地部署的服务器,或客户云上的一台虚拟机,IT 团队不允许再装一套编排系统。你交付的是一个仓库,里面有 compose.yaml、deploy.sh,以及一份 README,写清楚当前运行的是哪个标签、如何部署、回滚命令是什么。客户的运维人员必须不打电话给你也能完成操作。

到了客户现场,第一件事是问清应用的 /health 检查了什么,然后在他们的机器上测出真实启动时间。这两个事实决定了坏版本是被拦下,还是溜过关卡。

如果你在求职 FDE 岗位,简历上不要写“熟悉 Docker”。写得具体些,比如:为没有 Kubernetes 的环境搭建了一套 Compose 部署流程,包含健康检查、超时和自动回滚。再附上一个带 v2-broken 场景的小仓库,让招聘方可以亲自运行,看着回滚生效。

客户很少记得那些顺利的部署。他们记得的是:某次坏版本上了生产环境,三分钟后系统自己把旧版本换了回来。

这篇文章对你有帮助吗?

5 个信源
路线图上的下一篇 · 阶段 5: 部署上线客户问你交付了什么,让 SBOM 来回答软件清单要真正有用,就得列出你从没选过的库,以及每个库是怎么引入的。