发布与回滚
一条可重复的发布流程,以及在出问题时三分钟内退回上一个可用版本的准备工作。
发布不难,难的是出问题时能快速退回去。
大部分个人项目的发布流程是"拉代码、重新构建、重启"。这个流程能用,但它有一个隐藏问题:重新构建会覆盖上一个版本,你没有可以退回的东西。
这一页给一条可重复的发布流程,重点是回滚的准备工作。
快速版
- 发布前记下当前正在运行的版本,这是你的回滚目标。
- 给镜像打带版本的标签,不要只用
latest——否则重新构建就覆盖了上一版。 - 发布前备份数据库,且备份要在迁移之前。
- 数据库迁移按"扩展—迁移—收缩"做,让应用和数据库能分别回滚。
- 发布后按固定清单验证,不只看首页。
- 回滚不是失败,是把影响时间压到最短。
- 出问题先回滚,再查原因。不要在生产上边查边改。
发布前
四件事,都不能跳:
# 1. 记下当前运行的版本(这是回滚目标)
git rev-parse --short HEAD
# 2. 备份数据库(必须在迁移之前)
docker compose exec -T db pg_dump -U postgres mvp01 | gzip > ~/backups/db-$(date +%F-%H%M).sql.gz
# 3. 备份当前应用目录和配置
tar czf ~/backups/app-pre-$(git rev-parse --short HEAD).tgz .env docker-compose.yml
# 4. 确认没有正在进行的危险操作(数据导入、批量任务)
docker compose logs --tail 50 app第 1 步最容易被跳过,也最要紧。出问题时你需要知道"退回到哪",而不是在慌乱中翻历史记录。
pg_dump 那条命令里的 -T 不能省,它阻止 Docker 分配伪终端,否则管道里的数据会被破坏。
让自己有东西可以退回
这是我们自己配置里的一个真实缺口,值得单独说。
常见的发布命令是这样:
git pull && docker compose up -d --build它能工作,但 --build 会就地重建镜像。构建完成后,上一个版本的镜像就没有标签指向它了。这时候要回滚,只能重新用旧代码再构建一次——在故障时多花几分钟构建,这几分钟全是用户在受影响。
改进做法是发布前给当前镜像打个标签:
# 发布前:给当前镜像打上版本标签
docker tag <当前镜像> myapp:$(git rev-parse --short HEAD)
# 之后回滚就是改一下用哪个标签,不需要重新构建或者更简单的思路:构建产物和运行分开。 先构建并打标签,确认构建成功,再切换运行的版本。这样"构建失败"和"新版本有问题"是两个独立的阶段,不会互相拖累。
如果你现在的发布方式是 up -d --build,至少要知道你的回滚时间等于一次完整构建时间。测一下这个时间有多长,然后判断能不能接受。接受不了就改成打标签的方式。
数据库迁移的特殊性
代码可以随便回滚,数据库不行。
删掉一列的迁移执行完,旧数据就没了。这时候即使把代码退回上一版,应用也跑不起来——它要读那一列。
所以迁移要按三步走,让每一步都可以单独回滚:
| 步骤 | 做什么 | 这一步能回滚吗 |
|---|---|---|
| 扩展 | 加新列/新表,允许为空,旧代码不受影响 | 能,直接删掉新增的 |
| 迁移 | 新代码同时写新旧两处,回填历史数据 | 能,退回旧代码即可 |
| 收缩 | 确认稳定后再删旧列 | 不能 |
关键在于**"收缩"这一步要单独发布,而且要等前面稳定运行一段时间之后**。把三步压在一次发布里,就失去了分别回滚的能力。
详见数据库迁移、备份与恢复。
发布
# 拉取目标版本
git fetch && git checkout <目标版本>
# 构建并启动
docker compose up -d --build
# 看日志确认启动成功,不要看到"启动中"就走
docker compose logs -f app日志要看到应用真的开始监听端口,而不是只看到构建成功。构建成功和运行成功是两件事。
发布后验证
固定清单,每次都跑同样几项:
- 首页能打开
- 一个需要读数据库的页面能打开
- 登录流程能走通
- 一个受保护的页面在未登录时正确跳转
- 第三方回调地址返回预期状态
- 日志里没有新出现的报错
第二项是重点。首页是静态页的话,它返回 200 几乎说明不了什么。 数据库连不上、迁移没跑完、连接池配错了,首页照样正常。
第五项容易漏。支付回调、OAuth 回调这些路径平时没人访问,出问题要等到真实用户触发才会发现——那时候是订单已经付款但没发货。
回滚
判断标准要提前定,不要在故障时临时讨论。
出现下面任一情况,直接回滚:
- 核心流程(登录、支付、主要功能)不可用
- 出现数据写错的风险
- 日志里有持续增长的报错
- 你在五分钟内找不到原因
最后一条是最重要的一条。"再看五分钟就能查出来"这个判断通常是错的,而这五分钟里用户一直在受影响。
回滚步骤:
# 1. 切回上一个版本(发布前记下的那个)
git checkout <发布前记下的版本>
# 2. 启动
docker compose up -d --build
# 3. 验证核心流程恢复
# 4. 通知受影响的用户如果这次发布包含了不可逆的迁移(删列、改类型),代码回滚不够,需要从备份恢复数据库。这就是为什么备份必须在迁移之前做。
回滚之后
回滚不是终点,是止损。之后还要做:
- 在本地或测试环境复现问题,不要在生产上试
- 找到根因,不是找到"哪一行改了就好了"
- 修好之后重新走完整发布流程
- 把这次的教训加进发布清单
不要把"以后更加小心"当成整改措施。它不可执行、不可验证,下次一定会再犯。有效的整改是往清单里加一项具体检查,或者加一个自动化校验。
什么时候不该发布
- 你接下来两小时不在电脑前
- 数据库备份没做或没验证过
- 这次改动你自己没在生产构建下跑过
- 正在进行数据导入、批量任务这类不能中断的操作
- 你已经很累了
最后一条不是玩笑。发布本身很快,处理发布出的问题很慢。 在没有精力排障的时候发布,等于把小问题变成长时间故障。
常见卡点
| 卡在哪 | 可能原因 | 试试这样 |
|---|---|---|
| 要回滚但没有旧镜像 | --build 就地覆盖了 | 发布前给镜像打版本标签 |
| 代码回滚了但应用还是起不来 | 迁移不可逆 | 从迁移前的备份恢复数据库 |
| 不知道该退回哪个版本 | 发布前没记 | 发布前第一件事就是记下当前版本 |
| 首页正常但功能全挂 | 只验证了静态页 | 验证一个读数据库的页面 |
| 支付回调坏了很久才发现 | 回调路径没纳入验证 | 加进发布后清单 |
| 在生产上边查边改 | 没有先止损 | 先回滚,再在本地复现 |
| 同样的问题反复出现 | 整改措施不可执行 | 往清单里加具体检查项 |
| 备份是在迁移之后做的 | 顺序错了 | 备份必须在迁移之前 |
验收标准
- 有一条写下来的发布流程,每次照着走
- 发布前会记录当前运行的版本
- 知道自己的回滚需要多长时间,并且能接受
- 备份在迁移之前,且验证过能恢复
- 迁移按扩展—迁移—收缩拆开,收缩单独发布
- 发布后验证清单包含一个读数据库的页面和一个回调地址
- 回滚判断标准提前写好了,不在故障时讨论
- 至少完整演练过一次回滚
本章动作
演练一次回滚,在没有故障的时候。
计时:从"决定回滚"到"核心流程恢复"用了多久。如果超过十分钟,说明缺少准备——通常是缺一个可以直接切换的旧版本镜像。
下一步
- 数据库迁移、备份与恢复——迁移和备份的完整做法
- 监控、日志与故障处理——怎么发现该回滚了
- 生产环境验收——第一次上线前的完整检查
- 正式发布计划——对外发布的节奏安排