在独立开发早期,很多技术人习惯了一种极其原始、危险的部署方式:
本地跑完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 秒内直接扼杀坏包,绝不波及线上正常运转的业务。
免运维不是放任自流,而是用严密的自动化工程脚本,把一切意外阻绝在系统边界之外。