切换语言
切换主题

GitHub Actions 部署策略:从 VPS 到云平台的 CD 流水线

导语

凌晨三点,我盯着 GitHub Actions 的日志界面,红色错误一行行往上滚。“Host key verification failed”。又是 SSH 问题。

这已经是第五次部署失败了。明明本地测试全通过,推到 GitHub 后就炸。那一刻我特别想骂人——但也意识到一件事:部署策略的选择,远比我想的复杂。

不管是自己管理的 VPS,还是 Vercel、Cloudflare Pages 这类托管平台,每种方案都有坑。选错了,深夜调试的次数只会越来越多。

这篇文章聊聊 GitHub Actions 的几种部署策略,帮你找到适合自己的那条路。


VPS SSH 部署:老派但可靠

说实话,我一开始特别抗拒 VPS 部署。觉得太麻烦——SSH 密钥、known_hosts、rsync 参数……一堆东西要搞。

但踩过几次坑后,我发现这套”老派”方案反而最可控。

SSH 密钥配置:别硬编码

最常见的问题就是 SSH 密钥放哪儿。

新手往往直接把私钥写进 workflow 文件。大错特错。GitHub Secrets 才是正确位置。

去仓库的 Settings → Secrets → Actions,添加 SSH_PRIVATE_KEY。然后在 workflow 里这样用:

- name: Setup SSH
  uses: webfactory/[email protected]
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

这个 action 会自动启动 ssh-agent,加载你的密钥。省心。

known_hosts:避开 “Host key verification failed”

SSH 第一次连接服务器时,会问你是否信任这个 host。交互式问答在 CI 环境里没法处理,所以需要提前把服务器指纹加到 known_hosts。

两种方式:

方式一:用 action 自动添加

- name: Add server to known hosts
  uses: webfactory/[email protected]
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
    known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}

SSH_KNOWN_HOSTS 的内容可以这样获取:

ssh-keyscan -H your-server.com >> known_hosts.txt
# 把文件内容复制到 GitHub Secrets

方式二:手动配置

- name: Add server to known hosts
  run: |
    mkdir -p ~/.ssh
    ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts

方式一更干净,方式二适合快速调试。

rsync 还是 scp?

部署文件传输,我用 rsync。原因很简单:

  • 只传改动的文件,节省时间
  • 可以排除特定目录(比如 node_modules)
  • 支持增量同步

一个典型的 rsync 命令:

- name: Deploy to server
  run: |
    rsync -avz --delete \
      --exclude 'node_modules' \
      --exclude '.git' \
      ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/var/www/html/

--delete 参数会删除目标目录里源目录没有的文件。小心用——搞错了可能删掉不该删的东西。

部署后命令:重启服务

静态网站传完就完事了。但如果部署的是 Node.js 应用,还得重启服务。

我喜欢用 PM2 管理 Node 进程。部署后执行:

- name: Restart application
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "cd /var/www/app && pm2 restart all"

或者更保险的方式——重启特定应用:

- name: Restart application
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "pm2 restart my-app --update-env"

--update-env 会重新加载环境变量,适合配置有变化的情况。


云平台部署:托管服务的便利

VPS 部署的问题是——你得自己管服务器。安全补丁、SSL 证书续期、防火墙规则……琐事一堆。

托管平台就省心多了。推送代码,自动构建,自动部署。你只管写代码。

Vercel:前端项目的首选

Vercel 对前端项目的支持几乎是完美的。Next.js、Astro、React——一键部署,零配置。

但如果你的项目需要后端 API,就要注意了。Vercel 的 Serverless Functions 有运行时间限制(免费版 10 秒,Pro 版 60 秒)。超过就会 timeout。

对于纯静态站点或者简单的 API,Vercel 很够用。复杂后端服务还是得靠自己。

GitHub Actions 部署到 Vercel 的配置:

name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Vercel CLI
        run: npm i -g vercel@latest

      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build Project Artifacts
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

VERCEL_TOKEN 从 Vercel 控制台生成,存到 GitHub Secrets。

Cloudflare Pages:免费额度大方

Cloudflare Pages 的免费额度比 Vercel 大方得多。带宽不限,构建次数每月 500 次——对个人项目绰绰有余。

而且 Cloudflare 的全球 CDN 真的快。我自己测过,亚洲访问速度比 Vercel 稳定。

部署配置:

name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: npm run build

      - name: Deploy
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-project
          directory: dist

Cloudflare 还有个好处——R2 存储免费额度也大。静态资源可以放 R2,配合 Pages 的 CDN,加载速度能快不少。

Netlify:老牌稳定选手

Netlify 我用得不如前两个多,但它是老牌托管平台,生态成熟。

部署配置类似:

- name: Deploy to Netlify
  uses: netlify/actions/cli@master
  with:
    args: deploy --prod
  env:
    NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
    NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Netlify 的 Form handling 功能挺实用——表单提交自动处理,适合简单的营销页面。

托管平台的限制

话说回来,托管平台也不是万能的。

几个常见限制:

  1. 构建环境受限:内存、CPU 都有上限,大型项目可能构建失败
  2. 自定义程度低:想改 nginx 配置?没门
  3. 依赖平台存活:平台倒闭或者改政策,你得迁移
  4. 国内访问问题:部分平台在国内访问不稳定(虽然 Cloudflare 有改善)

如果你的项目需要完全控制,还是得回到 VPS。


混合策略:灵活与控制并存

很多项目不是”纯静态”也不是”纯后端”。前端是 Next.js,后端还要接数据库、跑定时任务……

这种情况下,混合部署可能是最优解。

静态页面托管 + API 部署到 VPS

一个典型架构:

  • 静态页面(HTML/CSS/JS)部署到 Cloudflare Pages 或 Vercel
  • Node.js API 服务部署到自己的 VPS
  • 数据库也在 VPS 上(或者用 Supabase/PlanetScale 托管)

好处是各取所长:

  • 前端享受 CDN 加速和自动 HTTPS
  • 后端有完全控制权,不受托管平台限制
  • 数据库访问延迟低(API 和数据库在同一台机器)

GitHub Actions 的多阶段部署

用一个 workflow 同时部署到两个地方:

name: Hybrid Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      artifact-path: ./dist
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist

  deploy-frontend:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist
      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-frontend
          directory: dist

  deploy-backend:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup SSH
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      - name: Deploy API to VPS
        run: |
          rsync -avz --delete \
            --exclude 'node_modules' \
            ./api/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/var/www/api/
      - name: Restart API service
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
            "cd /var/www/api && npm install && pm2 restart api"

这个 workflow 有三个 job:

  1. build:构建项目,产出静态文件
  2. deploy-frontend:把静态文件部署到 Cloudflare Pages
  3. deploy-backend:把 API 部署到 VPS 并重启服务

needs: build 确保部署 job 在构建完成后才执行。upload-artifactdownload-artifact 在 job 之间传递构建产物。

环境变量分离

混合部署的一个挑战:前端和后端的环境变量不一样。

前端需要知道 API 地址,后端需要知道数据库密码。

我的做法:

# 前端 job
- name: Set frontend env
  run: |
    echo "API_URL=https://api.mydomain.com" >> $GITHUB_ENV

# 后端 job
- name: Deploy with env
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "cd /var/www/api && pm2 restart api --update-env DATABASE_URL=${{ secrets.DATABASE_URL }}"

敏感信息(数据库密码、API token)永远走 GitHub Secrets。非敏感信息(API 地址)可以写在 workflow 里。


实战配置示例

下面是一个完整的 VPS 部署 workflow,覆盖了前面提到的所有要点。

完整 workflow 文件

name: Deploy to VPS

on:
  push:
    branches: [main]
  workflow_dispatch:  # 手动触发部署

env:
  NODE_VERSION: '20'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist
          retention-days: 1

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist

      - name: Setup SSH
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Add server to known hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy files
        run: |
          rsync -avz --delete \
            --exclude '.htaccess' \
            ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.DEPLOY_PATH }}

      - name: Verify deployment
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} \
            "ls -la ${{ secrets.DEPLOY_PATH }}"

      - name: Send deployment notification
        if: always()
        run: |
          curl -X POST "${{ secrets.NOTIFICATION_WEBHOOK }}" \
            -H "Content-Type: application/json" \
            -d '{"text": "Deployment completed: ${GITHUB_SHA}"}'

需要配置的 Secrets

Secret 名称说明获取方式
SSH_PRIVATE_KEYSSH 私钥内容本地生成,公钥放服务器
SERVER_HOST服务器 IP 或域名你的 VPS 信息
SERVER_USERSSH 登录用户名通常是 rootubuntu
DEPLOY_PATH部署目标路径/var/www/html
NOTIFICATION_WEBHOOK部署通知地址Slack/Telegram webhook

常见问题排查

部署失败时,看日志很容易晕——信息太多。

我的排查顺序:

  1. SSH 连接问题:看 “Setup SSH” 和 “Add server to known hosts” 步骤
    • 如果失败,检查密钥格式、known_hosts 内容
  2. rsync 传输问题:看 “Deploy files” 步骤
    • 如果失败,检查路径是否存在、权限是否正确
  3. 服务重启问题:看 “Verify deployment” 步骤
    • 如果失败,检查目标路径是否有文件

一个技巧:在失败的步骤后面加调试输出。

- name: Debug SSH connection
  if: failure()
  run: |
    echo "SSH config:"
    cat ~/.ssh/config || echo "No config file"
    echo "Known hosts:"
    cat ~/.ssh/known_hosts || echo "No known_hosts file"
    ssh -v ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} echo "Connection test"

ssh -v 会输出详细日志,能看到问题在哪。


总结

写了这么多,其实就一句话:没有完美的部署方案,只有最适合你项目的方案。

选型建议

  • 纯静态站点(博客、文档):Cloudflare Pages 或 Vercel,省心
  • 简单 API + 前端:托管平台够了,别折腾 VPS
  • 复杂后端 + 数据库:VPS 或云服务器,控制权重要
  • 混合架构:前端托管 + 后端 VPS,各取所长

不管选哪种,GitHub Actions 的配置模式都差不多:构建 → 传输 → 重启。把这三步拆清楚,调试时就不会乱了方向。

还有一点:部署失败时别慌。日志分段看,先定位是 SSH 连接问题还是命令执行问题。加个调试步骤,问题很快就暴露出来了。

下次凌晨三点部署失败,希望你能更快找到原因。

配置 GitHub Actions VPS 部署

完整配置 GitHub Actions 通过 SSH 部署到 VPS 的流程

⏱️ 预计耗时: 30 分钟

  1. 1

    步骤1: 生成 SSH 密钥对

    在本地生成专用于部署的 SSH 密钥:

    • ssh-keygen -t ed25519 -C "deploy@github" -f deploy_key
    • 公钥(deploy_key.pub)添加到服务器 ~/.ssh/authorized_keys
    • 私钥(deploy_key)内容存入 GitHub Secrets 的 SSH_PRIVATE_KEY
  2. 2

    步骤2: 配置 GitHub Secrets

    在仓库 Settings → Secrets → Actions 添加:

    • SSH_PRIVATE_KEY:私钥完整内容
    • SERVER_HOST:服务器 IP 或域名
    • SERVER_USER:SSH 用户名(如 root 或 ubuntu)
    • DEPLOY_PATH:目标部署路径
  3. 3

    步骤3: 创建 Workflow 文件

    在 .github/workflows/deploy.yml 创建部署配置:

    • 添加 SSH 密钥配置步骤(webfactory/ssh-agent-action)
    • 配置 known_hosts 避免 host verification 失败
    • 使用 rsync 传输构建产物
    • 部署后执行服务重启命令
  4. 4

    步骤4: 测试部署流程

    推送代码触发自动部署,或手动触发:

    • 观察每个步骤的日志输出
    • SSH 失败时检查密钥格式和 known_hosts
    • rsync 失败时检查路径和权限
    • 添加调试步骤排查问题

常见问题

GitHub Actions 部署时出现 'Host key verification failed' 怎么解决?
这是 SSH 首次连接服务器时缺少 known_hosts 配置导致的。两种解决方案:

• 方案一:使用 ssh-keyscan 获取服务器指纹,存入 SSH_KNOWN_HOSTS Secret
• 方案二:在 workflow 中手动执行 ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts

推荐方案一,更干净安全。
SSH 密钥应该放在哪里?直接写在 workflow 文件里行吗?
绝对不行。私钥必须存在 GitHub Secrets 里,workflow 通过 `${{ secrets.SSH_PRIVATE_KEY }}` 引用。硬编码私钥会被推到代码库,任何人都能看到,严重安全隐患。
Vercel、Cloudflare Pages、Netlify 哪个更适合个人项目?
Cloudflare Pages 免费额度最大方(不限带宽,500 次/月构建),亚洲访问速度也更稳定。Vercel 对 Next.js 项目体验最好,但免费版 Serverless Functions 有 10 秒限制。Netlify 生态成熟,Form handling 功能实用。

纯静态站点优先选 Cloudflare Pages。
混合部署架构有什么优势?
前端部署到托管平台享受 CDN 加速和自动 HTTPS,后端部署到 VPS 有完全控制权不受平台限制。适合需要数据库、定时任务等复杂后端服务的项目。

用 GitHub Actions 的多 job workflow 可以同时部署到两个地方。
部署失败时日志太多看不清楚,怎么快速定位问题?
分段看日志,按顺序排查:

1. SSH 连接问题 → 检查 Setup SSH 和 known_hosts 步骤
2. rsync 传输问题 → 检查路径存在性和权限
3. 服务重启问题 → 检查目标路径文件列表

在失败步骤后加调试输出(ssh -v 详细日志)能快速暴露问题。
rsync 的 --delete 参数有什么风险?
--delete 会删除目标目录中源目录没有的文件,保证两边完全同步。但如果配置错误路径,可能删掉不该删的东西。

建议首次部署不加 --delete,确认正确后再启用。或者用 --delete-excluded 只删除被排除的文件。

8 分钟阅读 · 发布于: 2026年4月7日 · 修改于: 2026年4月8日

评论

使用 GitHub 账号登录后即可评论

相关文章