全栈独立产品 CI/CD 复盘:从手动部署到自动化流水线
全栈独立产品 CI/CD 复盘从手动部署到自动化流水线一、独立产品的部署之痛当一行命令变成十步操作独立产品在 MVP 阶段部署通常是这样的本地npm run build→ scp 上传到服务器 → ssh 登录 →pm2 restart。整个过程不超过 3 分钟看起来很高效。但当产品开始增长——前端从单页变成多模块、后端从单服务变成微服务、同时需要对接多个第三方 API——这 3 分钟的部署变成了 30 分钟的心智负担前端构建还记得切换 node 版本吗后端构建Docker 镜像还是裸进程数据库迁移这次 migration 是幂等的吗环境变量更新新加了一个 API Key服务器的.env改了吗Nginx 配置更新新增了一个子路由CDN 缓存刷新改了 CSS 文件名CDN 上旧文件删了吗检查生产环境是否正常万一刚才的 migration 把表锁了呢步骤越多遗漏的可能越大。独立产品的 CI/CD 不是为了自动化而自动化而是为了将部署操作从依赖人的记忆力转变为依赖流程的确定性。二、阶段一GitHub Actions 的最小可行流水线2.1 单文件搞定前端 后端的自动化独立产品的 CI/CD 不需要 Kubernetes、Terraform、Helm Charts 这些重型基础设施。一条 GitHub Actions Workflow 文件可以覆盖 90% 的需求# .github/workflows/deploy.yml # 全栈独立产品的 CI/CD 流水线 name: Deploy on: push: branches: [main] pull_request: branches: [main] env: NODE_VERSION: 20 PNPM_VERSION: 9 DOCKER_REGISTRY: ghcr.io jobs: # ───────────────────────────────────────────── # Job 1: 代码质量检查Lint Type Check Test # ───────────────────────────────────────────── quality: runs-on: ubuntu-latest outputs: frontend-changed: ${{ steps.changes.outputs.frontend }} backend-changed: ${{ steps.changes.outputs.backend }} steps: - uses: actions/checkoutv4 - name: Detect changes id: changes uses: dorny/paths-filterv3 with: filters: | frontend: - packages/frontend/** backend: - packages/backend/** - uses: pnpm/action-setupv4 with: version: ${{ env.PNPM_VERSION }} - uses: actions/setup-nodev4 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - run: pnpm install --frozen-lockfile # 前端ESLint TypeScript 类型检查 单元测试 - name: Frontend quality if: steps.changes.outputs.frontend true run: | pnpm --filter frontend lint pnpm --filter frontend type-check pnpm --filter frontend test --coverage # 后端ESLint 单元测试 - name: Backend quality if: steps.changes.outputs.backend true run: | pnpm --filter backend lint pnpm --filter backend test --coverage # ───────────────────────────────────────────── # Job 2: 构建 Docker 镜像 # ───────────────────────────────────────────── build: needs: quality if: github.ref refs/heads/main runs-on: ubuntu-latest strategy: matrix: service: [frontend, backend] outputs: image-tag: ${{ steps.meta.outputs.version }} steps: - uses: actions/checkoutv4 - name: Docker meta id: meta uses: docker/metadata-actionv5 with: images: ${{ env.DOCKER_REGISTRY }}/${{ github.repository }}/${{ matrix.service }} tags: | typesha,prefix,formatshort typeref,eventbranch typesemver,pattern{{version}} - name: Login to GitHub Container Registry uses: docker/login-actionv3 with: registry: ${{ env.DOCKER_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-actionv6 with: context: . file: ./packages/${{ matrix.service }}/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: typegha cache-to: typegha,modemax build-args: | BUILDKIT_INLINE_CACHE1 # ───────────────────────────────────────────── # Job 3: 数据库迁移仅限后端变更时 # ───────────────────────────────────────────── migrate: needs: build if: needs.quality.outputs.backend-changed true runs-on: ubuntu-latest environment: production steps: - name: Run database migration uses: appleboy/ssh-actionv1 with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /opt/app docker compose run --rm backend pnpm prisma migrate deploy echo Migration completed # ───────────────────────────────────────────── # Job 4: 部署到生产环境 # ───────────────────────────────────────────── deploy: needs: [build, migrate] if: needs.migrate.result success || needs.migrate.result skipped runs-on: ubuntu-latest environment: name: production url: https://app.example.com steps: - name: Deploy via SSH uses: appleboy/ssh-actionv1 with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /opt/app # 拉取最新 compose 配置 git pull origin main # 拉取最新镜像 docker compose pull # 滚动更新启动新容器等待健康检查通过再停旧容器 docker compose up -d --remove-orphans # 清理旧镜像 docker image prune -f --filter until24h # ───────────────────────────────────────────── # Job 5: 部署后健康检查 # ───────────────────────────────────────────── health-check: needs: deploy runs-on: ubuntu-latest steps: - name: Wait for services run: sleep 10 - name: Health check frontend run: | STATUS$(curl -s -o /dev/null -w %{http_code} https://app.example.com/api/health) if [ $STATUS ! 200 ]; then echo Frontend health check failed: HTTP $STATUS exit 1 fi - name: Health check backend run: | STATUS$(curl -s -o /dev/null -w %{http_code} https://api.example.com/health) if [ $STATUS ! 200 ]; then echo Backend health check failed: HTTP $STATUS exit 1 fi - name: Notify on failure if: failure() uses: slackapi/slack-github-actionv2 with: webhook: ${{ secrets.SLACK_WEBHOOK }} webhook-type: incoming-webhook payload: | { text: :x: 部署失败\n仓库: ${{ github.repository }}\n提交: ${{ github.sha }}\n查看: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} }2.2 流水线设计的核心决策这条流水线有几个关键设计决策值得展开条件构建Conditional Build通过dorny/paths-filter检测变更文件路径只有变更涉及的服务才触发对应的构建和部署。避免每次提交都全量重新构建所有服务。matrix strategy 并行构建前后端镜像在同一个 Job 中使用strategy.matrix并行构建缩短整体构建时间。数据库迁移独立 Job将 migration 从部署流程中剥离为独立 Job。原因migration 如果失败如 PostgreSQL 不可用部署应被阻断。如果 migration 和部署混在一个 SSH 脚本中执行可能迁移失败但服务已启动。Docker 镜像缓存cache-from: typegha和cache-to: typegha,modemax利用 GitHub Actions 的 Actions Cache 缓存 Docker 构建层第二次及以后的构建时间缩短 60%~80%。健康检查独立 Job部署完成不是终点服务真正可用才算成功。部署后等待 10 秒让容器启动然后分别验证前后端健康检查端点。如果失败通过 Slack 推送告警。三、阶段二多环境管理——Staging 与 Production 的分流3.1 为什么需要 Staging 环境当独立产品有了第一批付费用户后直接推到 production 的风险急剧增加。Staging 环境的价值在于在推送真实用户之前用一个与生产环境完全相同配置的环境来验证部署的完整性。Staging 和 Production 的差异应该仅在于数据库连接独立的 staging 数据库API Keystaging 使用沙箱密钥域名staging.example.comvsapp.example.com日志级别staging 可以更详细其余配置——Docker 镜像版本、环境变量结构、Nginx 配置——应当完全一致。如果 staging 和生产不一致那 staging 的验证就失去了意义。3.2 环境配置管理使用 GitHub Actions 的 Environment 机制管理多套配置# Staging 自动部署main 分支合并后自动触发 deploy-staging: needs: build runs-on: ubuntu-latest environment: name: staging url: https://staging.example.com steps: - name: Deploy to staging # ... SSH 到 staging 服务器 # Production 手动审批后部署 deploy-production: needs: deploy-staging runs-on: ubuntu-latest environment: name: production url: https://app.example.com steps: - name: Deploy to production # ... SSH 到 production 服务器GitHub Actions 的 Environment 提供了三项关键保护审批门Approval Gateproduction 环境可以配置为需要人工审批才能继续。环境专属密钥同一个密钥名称如SSH_PRIVATE_KEY在 staging 和 production 中对应不同的值。部署历史每次 deployment 都有记录包括谁审批的、什么时候部署的、关联哪个 commit。四、阶段三灰度发布与自动回滚4.1 灰度发布的成本权衡对于独立产品完整的金丝雀发布10% → 30% → 50% → 100% 逐步切流量往往过于复杂。一个实用的替代方案是先切 10%观察 5 分钟再全量# .github/workflows/deploy.yml 中的灰度部署 deploy-canary: needs: build runs-on: ubuntu-latest steps: - name: Deploy canary (10% traffic) uses: appleboy/ssh-actionv1 with: host: ${{ secrets.SSH_HOST }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /opt/app # 启动新版本容器不同端口Nginx 配置 10% 流量指向新版本 docker compose -f docker-compose.canary.yml up -d # 更新 Nginx 分流规则 cp nginx/canary.conf /etc/nginx/conf.d/app.conf nginx -s reload - name: Wait and monitor canary run: | sleep 300 # 等待 5 分钟 # 检查 error rate ERROR_RATE$(curl -s https://app.example.com/api/metrics/error-rate?window5m | jq .rate) if (( $(echo $ERROR_RATE 0.01 | bc -l) )); then echo Error rate too high: $ERROR_RATE, rolling back... exit 1 fi - name: Rollback canary on failure if: failure() uses: appleboy/ssh-actionv1 with: host: ${{ secrets.SSH_HOST }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /opt/app cp nginx/main.conf /etc/nginx/conf.d/app.conf nginx -s reload docker compose -f docker-compose.canary.yml down deploy-full: needs: deploy-canary runs-on: ubuntu-latest steps: - name: Full deployment uses: appleboy/ssh-actionv1 with: host: ${{ secrets.SSH_HOST }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /opt/app docker compose up -d cp nginx/main.conf /etc/nginx/conf.d/app.conf nginx -s reload docker compose -f docker-compose.canary.yml down4.2 自动回滚的条件回滚的条件需要是可自动判定的而非依赖人工观察。可用的自动判断指标健康检查端点返回非 200。错误率5xx 占比超过 1%。请求延时 P95 超过 500ms相对于基线的 2x 偏离。在 GitHub Actions 中如果灰度部署后的监控 Job 返回非零退出码流水线自动进入回滚步骤。不需要人工判断——在凌晨 3 点也没有人能做出可靠的判断。五、总结独立产品的 CI/CD 不需要过度工程化。核心思路是用一条 GitHub Actions Workflow 解决 90% 的需求其余 10% 的复杂场景多地域部署、蓝绿发布等真正需要时再引入。关键决策条件构建减少不必要的构建和部署耗时。数据库迁移独立 Job失败时阻断后续部署。Staging 环境审批门是保护生产环境的最低成本方案。灰度 自动回滚把靠人盯着上线变成靠指标自动判定。流水线设计的最终目标不是看起来很先进而是你可以在周六下午 3 点放心地推一个版本到生产环境然后关掉电脑去喝咖啡。