GitHub Actions 生产部署流水线:零停机滚动更新、自动回滚与实时告警实战

在独立开发早期,很多技术人习惯了一种极其原始、危险的部署方式:
本地跑完 npm run build,打开 FileZilla 或者敲一行 scp -r ./dist user@myserver:/var/www 把文件暴力覆盖上去;后端更新了代码,就 SSH 登入服务器,敲 git pull && pm2 restart all。
这种“肉身运维”在刚开始看似省事,但只要团队稍有业务量,就会引发一系列令人崩溃的生产惨案:
- 服务瞬时 502 宕机:在旧服务进程杀掉、新服务重新启动编译的 5~15 秒真空期内,正好在付款或提交发票的用户全部收到接口 502 Bad Gateway 报错;
- 构建坏包直接杀死生产:由于本地与服务器 Node.js 版本微小差异、或者某个服务端环境变量未设置,新版本一启动就直接 Crash 退出,整个系统瘫痪,线上没有备份版本可供秒级回滚;
- 完全没有审计与发布通知:出了问题不知道是哪一个 Git Commit 引发的,团队成员对生产状态两眼一抹黑。
作为追求极致人效的独立开发者,我们必须打造一套完全托管在 GitHub Actions 上的全自动化生产级发布流水线:代码推送到 main 分支,CI 自动跑测试、基于 Docker Compose 与健康检查实现零停机滚动切换(Zero-downtime Rolling Update)、若启动失败自动安全回滚,并通过飞书/Slack 机器人毫秒级推送美观的发布卡片。
零停机生产部署架构设计
我们利用 Docker 镜像版本化与单机双容器健康探测机制,实现零停机发布:
[开发者推送到 GitHub main 分支]
│
▼
[GitHub Actions CI: 自动化类型检查 + E2E 核心回归]
│
▼
[多架构 Docker 镜像构建并推送到 GitHub Container Registry (ghcr.io)]
- 打上精确 Commit SHA 标签与 latest 标签
│
▼
[SSH 接入生产主机: 执行零停机平滑切换脚本]
- 启动新版本容器 (绑定临时端口并执行健康检查 /healthz)
│
┌────────┴──────────────────────────┐
▼ ▼
[健康检查通过 (HTTP 200)] [健康检查失败 (连续3次超时/报错)]
- 平滑切换 Caddy/Nginx 上游 - 自动销毁损坏的新容器
- 优雅终止 (graceful stop) 旧容器 - 保持老版本容器继续稳定服务
- 向飞书推送【部署成功】卡片 - 向飞书推送【严重告警: 已自动回滚】
第一步:编写自愈与健康检查的 Dockerfile 与服务
后端服务必须暴露一个无副作用的深层健康检查端点 /healthz,不仅检查服务本身是否在线,还一并核验数据库与 Redis 连接是否通畅:
// src/api/health.ts
import { Request, Response } from 'express';
import { db } from '../db';
import { sql } from 'drizzle-orm';
export async function healthCheckHandler(req: Request, res: Response) {
try {
// 快速探测数据库连接是否存活
await db.execute(sql`SELECT 1`);
return res.status(200).json({
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || 'unknown',
});
} catch (error: any) {
return res.status(503).json({
status: 'unhealthy',
reason: error.message,
});
}
}
配套的多阶段构建 Dockerfile:
# 生产多阶段极轻量构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --prod --frozen-lockfile
COPY --from=builder /app/dist ./dist
EXPOSE 3000
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/healthz || exit 1
CMD ["node", "dist/index.js"]
第二步:生产服务器零停机滚动切换脚本
在宿主机上,我们通过一段简洁高效的 Bash 脚本管理容器切换。新旧容器并存,只有当新容器的 /healthz 返回 200 并且通过探测后,才通过软链接与 Docker 网络无缝切流:
#!/usr/bin/env bash
# /var/www/mysaas/deploy.sh
set -euo pipefail
IMAGE_TAG="${1:-latest}"
CONTAINER_NAME="mysaas-backend"
NEW_CONTAINER="${CONTAINER_NAME}-next"
IMAGE="ghcr.io/myteam/mysaas:${IMAGE_TAG}"
echo "🚀 [Deploy] 正在拉取生产镜像: ${IMAGE}"
docker pull "${IMAGE}"
echo "🔄 [Deploy] 正在启动新版本容器用于预热与健康探测..."
# 先在备用端口 3001 启动新容器
docker run -d \
--name "${NEW_CONTAINER}" \
--env-file /var/www/mysaas/.env.production \
-p 3001:3000 \
"${IMAGE}"
# 等待健康检查,最多重试 10 次 (每次 3 秒)
HEALTHY=false
for i in {1..10}; do
if curl -sf http://127.0.0.1:3001/healthz > /dev/null; then
HEALTHY=true
break
fi
echo "⏳ 等待新版本健康状态... ($i/10)"
sleep 3
done
if [ "$HEALTHY" = true ]; then
echo "✅ 新容器健康探测通过!执行原子平滑替换..."
# 停止并移除老容器
docker stop "${CONTAINER_NAME}" || true
docker rm "${CONTAINER_NAME}" || true
# 将新容器重命名为正式服务并切换端口
docker stop "${NEW_CONTAINER}"
docker rm "${NEW_CONTAINER}"
# 正式启动
docker run -d \
--name "${CONTAINER_NAME}" \
--restart always \
--env-file /var/www/mysaas/.env.production \
-p 3000:3000 \
"${IMAGE}"
echo "🎉 零停机平滑升级完成!"
else
echo "🚨 [Critical Error] 新容器健康探测失败!启动自动化回滚..."
docker stop "${NEW_CONTAINER}" || true
docker rm "${NEW_CONTAINER}" || true
echo "🛡️ 老版本容器未受任何影响,生产环境保持安全。"
exit 1
fi
第三步:GitHub Actions 生产流水线与飞书/Slack 卡片告警
在 .github/workflows/production-deploy.yml 中,我们将代码检测、镜像构建、自动化部署与即时告警串联为完整的闭环:
name: Production CI/CD Pipeline
on:
push:
branches:
- main
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Run Type Check & Tests
run: |
pnpm typecheck
pnpm test:unit
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
deploy-production:
needs: test-and-build
runs-on: ubuntu-latest
steps:
- name: SSH Deploy to Cloud Host
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PROD_SERVER_IP }}
username: root
key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
script: |
/var/www/mysaas/deploy.sh ${{ github.sha }}
- name: Send Notification on Success
if: success()
run: |
curl -X POST -H "Content-Type: application/json" \
-d '{
"msg_type": "interactive",
"card": {
"header": { "title": { "tag": "plain_text", "content": "🟢 生产环境发布成功" }, "template": "green" },
"elements": [{
"tag": "div",
"text": { "tag": "lark_md", "content": "**提交分支**: `main`\n**Commit**: `${{ github.sha }}`\n**提交者**: `${{ github.actor }}`\n**状态**: 零停机滚动更新已完成,全量监控正常。" }
}]
}
}' ${{ secrets.FEISHU_WEBHOOK_URL }}
- name: Send Notification on Failure
if: failure()
run: |
curl -X POST -H "Content-Type: application/json" \
-d '{
"msg_type": "interactive",
"card": {
"header": { "title": { "tag": "plain_text", "content": "🔴 生产部署失败(已自动回滚)" }, "template": "red" },
"elements": [{
"tag": "div",
"text": { "tag": "lark_md", "content": "**提交**: `${{ github.sha }}`\n**严重等级**: `CRITICAL`\n**状态**: 新版本未通过健康探测,已自动触发保护回滚,生产环境未受损伤,请立即排查。" }
}]
}
}' ${{ secrets.FEISHU_WEBHOOK_URL }}
落地成效
上线这套现代 CI/CD 部署流水线后:
- 发布频率大幅提升:从以前心惊肉跳地“一周选个半夜挑时间发一次”,变成现在“每天随时发布 3~5 次”,任何小改动点击 Merge 之后 3 分钟内自动上线;
- 彻底消灭 502 宕机窗口:得益于 Docker 备用端口健康探测,老版本在完全接棒之前持续服务,用户端零感知;
- 安全自愈兜底:即使有新引入的数据库配置遗漏,自愈脚本在 30 秒内直接扼杀坏包,绝不波及线上正常运转的业务。
免运维不是放任自流,而是用严密的自动化工程脚本,把一切意外阻绝在系统边界之外。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)