6 步用 GitHub Actions 搭建生产级 CI/CD 流水线,附 3 个常踩的坑


目录:

还在手动 SSH 上服务器拉代码、重启服务?GitHub Actions 免费额度就够中小团队用了。本文从真实项目出发,手把手搭建一条从代码推送到自动部署的完整流水线。


痛点:手动部署的代价

运维老哥都经历过这种场景:开发喊一声"代码合了",你 SSH 上去 git pulldocker builddocker-compose up -d,顺便祈祷别忘了跑数据库迁移。

问题不在于操作本身,而是——

  • 人会忘事:漏跑 migration、忘记清缓存、配置没同步
  • 无法审计:谁在什么时间部署了什么版本?翻 history 猜吧
  • 回滚靠运气:上个版本的镜像还在不在?Tag 打了没?

CI/CD 不是锦上添花,是消灭人为失误的基础设施

方案:GitHub Actions + Docker + 自托管 Runner

架构一句话说清:Push 代码 → Actions 自动构建镜像 → 推到 Registry → SSH 到目标机部署

Developer → Push/Merge → GitHub Actions
                             ↓
                     Build Docker Image
                             ↓
                     Push to ghcr.io
                             ↓
                  SSH Deploy to Production

为什么选 GitHub Actions?三个理由:免费额度够用(公开仓库无限,私有仓库每月 2000 分钟)、和代码仓库零距离Marketplace 有现成的 Action 直接拿来用

实操步骤

第 1 步:定义 Workflow 文件

在仓库根目录创建 .github/workflows/deploy.yml

name: Build & Deploy

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

第 2 步:镜像打双标签

注意上面的 tags 写了两个——github.sha 是精确版本号,latest 是方便快速部署。回滚时靠 SHA 标签,日常部署靠 latest

第 3 步:添加部署 Job

在同一个文件里追加部署步骤:

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            cd /opt/myapp
            export IMAGE_TAG=${{ github.sha }}
            docker compose up -d --no-build
            docker image prune -f

第 4 步:配置 Secrets

在仓库 Settings → Secrets and variables → Actions 中添加:

Secret 名 内容
DEPLOY_HOST 目标服务器 IP
DEPLOY_USER SSH 用户名
DEPLOY_KEY SSH 私钥

第 5 步:增加健康检查

部署后别急着下班,加一步验证:

      - name: Health Check
        run: |
          sleep 15
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://your-domain.com/healthz)
          if [ "$STATUS" != "200" ]; then
            echo "❌ Health check failed: HTTP $STATUS"
            exit 1
          fi
          echo "✅ Health check passed"

第 6 步:通知到群聊

部署结果推到 Slack 或钉钉,全团队可见:

      - name: Notify
        if: always()
        run: |
          STATUS=${{ job.status }}
          curl -X POST "${{ secrets.WEBHOOK_URL }}" \
            -H "Content-Type: application/json" \
            -d "{\"text\":\"Deploy ${STATUS}: ${{ github.repository }}@${{ github.sha }}\"}"

避坑指南

坑 1:Secret 泄漏——别在日志里 echo 变量

GitHub Actions 会自动遮蔽 Secrets 的值,但如果你把 Secret 拼到 URL 或文件名里,遮蔽可能失效。安全铁律:Secret 只通过环境变量传递,永远不拼进字符串

# ❌ 危险写法
run: echo "Deploying to ${{ secrets.DEPLOY_HOST }}"

# ✅ 安全写法
env:
  HOST: ${{ secrets.DEPLOY_HOST }}
run: echo "Deploying to $HOST"

坑 2:并发部署互相踩——需要 concurrency 控制

两个人几乎同时 push 到 main,两条流水线同时跑部署——服务器上 docker compose up 被调了两次,端口冲突或数据库迁移冲突。

加一行就解决:

concurrency:
  group: deploy-production
  cancel-in-progress: false  # 排队而非取消

坑 3:磁盘被撑爆——旧镜像没清理

每次部署都 docker pull 新镜像,旧的不删,一个月后磁盘就满了。上面脚本已经加了 docker image prune -f,但更稳的做法是加个 cron:

# /etc/cron.daily/docker-cleanup
#!/bin/bash
docker image prune -a --filter "until=168h" -f
docker volume prune -f

保留最近 7 天的镜像,其余全清。

总结

步骤 作用
Workflow 文件 定义触发条件和任务
双标签 SHA 精确回滚 + latest 快速部署
SSH 部署 拉镜像、重启服务
Secrets 安全存储敏感信息
健康检查 部署后自动验证
群聊通知 全团队可见,出问题秒响应

一条流水线跑通之后,你会发现:回滚就是重跑某次 Action,审计就是看 Actions 历史,协作就是 PR 合并后自动生效。运维的安全感,从自动化开始。