Skip to content

Nuxt.js 项目 GitHub Actions 自动化 CI/CD 实践(二):远程部署

前言

承接第一篇文章,我们已经实现了创建 Release 时自动构建并打包。本文将讲解如何将构建产物自动部署到远程服务器,实现从代码提交到服务上线的全自动化。

本系列文章回顾:

部分目标状态
第一部分构建、打包并上传到 GitHub Release✅ 已完成
第二部分(本文)自动部署到远程服务器,PM2 重启服务📍 当前
第三部分CI/CD 邮箱通知待发布

达成效果:

当你创建一个 Release 后,第一部分自动完成构建打包,第二部分将自动:

  1. 下载构建产物
  2. 解压并验证文件完整性
  3. SCP 传输到目标服务器
  4. 备份当前版本
  5. 部署新文件
  6. PM2 重启服务
  7. 清理临时文件

整个过程无需人工干预,真正做到一键发布、自动上线

NOTICE

本文为系列文章的第二部分,聚焦于远程部署环节。建议先完成第一部分的配置再继续本文。

第二部分目标

当第一部分构建完成后,自动完成:

  • 下载构建产物(artifact)
  • 解压并验证文件完整性
  • SCP 传输到目标服务器
  • 备份当前运行版本
  • 部署新文件到正式目录
  • PM2 重启服务
  • 清理临时文件

目录结构(延续第一部分)

.github/
└── workflows/
    └── release-bump.yaml   # 添加 remote-deploy job

完整配置文件

在第一部分的 release-bump.yaml 基础上,添加 remote-deploy Job:

.github/workflows/release-bump.yaml
yaml
name: Auto Bump Version on Release

on:
  release:
    types: [created]

jobs:
  # Job 1: 构建和发布(详见第一部分)
  build-and-release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    outputs:
      version: ${{ steps.extract-version.outputs.version }}
      project_name: ${{ steps.project-name.outputs.project_name }}

    steps:
      # ... 第一部分的所有步骤 ...
      # 包含:检出、安装依赖、构建、打包、上传 Release、上传 artifact

  # Job 2: 远程部署
  remote-deploy:
    runs-on: ubuntu-latest
    needs: build-and-release  # 等待构建完成
    if: github.event_name == 'release' && success()
    permissions:
      actions: read  # 允许下载 artifacts

    steps:
      # 1. 下载构建产物
      - name: Download Artifact
        uses: actions/download-artifact@v8
        with:
          name: build-output
          path: ./

      # 2. 解压 artifact
      - name: Extract Artifact
        run: |
          echo "Contents before extract:"
          ls -la
          echo ""
          echo "Extracting tar.gz..."
          mkdir -p .output
          tar -xzf *.tar.gz -C .output
          echo "Extracted contents:"
          ls -la .output/
          echo "Total files: $(find .output -type f | wc -l)"

      # 3. 验证提取的内容
      - name: Verify Extracted Content
        run: |
          if [ ! -f ".output/nitro.json" ]; then
            echo "ERROR: nitro.json not found after extraction!"
            exit 1
          fi
          echo "✅ Artifact extracted successfully"

      # 4. SCP 传输到服务器
      - name: Deploy to Server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          port: ${{ secrets.DEPLOY_PORT || 22 }}
          source: ".output/"
          target: "/tmp/${{ needs.build-and-release.outputs.project_name }}-build-${{ needs.build-and-release.outputs.version }}/"

      # 5. SSH 执行远程部署
      - name: Execute Remote Deployment
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          port: ${{ secrets.DEPLOY_PORT || 22 }}
          script: |
            set -e  # 遇到错误立即退出

            PROJECT_NAME="${{ needs.build-and-release.outputs.project_name }}"
            VERSION="${{ needs.build-and-release.outputs.version }}"
            DEPLOY_DIR="/var/www/${PROJECT_NAME}"
            BUILD_DIR="/tmp/${PROJECT_NAME}-build-${VERSION}"

            echo "=========================================="
            echo "Deploying ${PROJECT_NAME} version ${VERSION}"
            echo "=========================================="

            # 1. 备份当前版本
            if [ -d "$DEPLOY_DIR" ]; then
              BACKUP_DIR="/var/www/${PROJECT_NAME}-backup-$(date +%Y%m%d_%H%M%S)"
              echo "📦 Creating backup: ${BACKUP_DIR}"
              cp -r "$DEPLOY_DIR" "$BACKUP_DIR"
              # 保留最近 5 个备份
              ls -t /var/www/${PROJECT_NAME}-backup-* 2>/dev/null | tail -n +6 | xargs rm -rf || true
            else
              echo "📁 Creating deployment directory: ${DEPLOY_DIR}"
              mkdir -p "$DEPLOY_DIR"
            fi

            # 2. 部署新文件
            # 注意:由于打包时使用了 -C .output .,解压后文件直接在 BUILD_DIR 下,没有 .output 层
            echo "📤 Deploying files..."
            rm -rf ${DEPLOY_DIR}/*
            cp -r ${BUILD_DIR}/* ${DEPLOY_DIR}/

            # 3. 设置权限
            chown -R www-data:www-data ${DEPLOY_DIR}
            chmod -R 755 ${DEPLOY_DIR}

            # 4. 重启 PM2 服务
            echo "🔄 Restarting PM2 service..."
            pm2 restart ${PROJECT_NAME}

            # 5. 清理临时文件
            echo "🧹 Cleaning up..."
            rm -rf ${BUILD_DIR}

            # 6. 显示部署结果
            echo "=========================================="
            echo "✅ Deployment completed successfully!"
            echo "📍 Deployed to: ${DEPLOY_DIR}"
            echo "📌 Version: ${VERSION}"
            echo "=========================================="
            pm2 status ${PROJECT_NAME}

      # 6. 输出部署概览
      - name: Build Summary
        run: |
          echo "=========================================="
          echo "🎉 DEPLOYMENT SUMMARY"
          echo "=========================================="
          echo "Project: ${{ needs.build-and-release.outputs.project_name }}"
          echo "Version: ${{ needs.build-and-release.outputs.version }}"
          echo "Status: ${{ job.status }}"
          echo "Files: $(find .output -type f | wc -l)"
          echo "Time: $(date)"
          echo "=========================================="

必需的 Secrets 配置

在第一部分的 ACCESS_TOKEN 基础上,新增以下 Secrets:

在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中添加:

Secret 名称说明示例
DEPLOY_HOST服务器 IP 或域名192.168.1.100example.com
DEPLOY_USERSSH 登录用户名rootubuntu
DEPLOY_SSH_KEYSSH 私钥内容-----BEGIN OPENSSH PRIVATE KEY-----
DEPLOY_PORTSSH 端口(可选)22

如何生成 SSH 密钥对

bash
# 在本地生成 SSH 密钥对(如果还没有)
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_deploy_key

# 将公钥添加到服务器的 authorized_keys
ssh-copy-id -i ~/.ssh/github_deploy_key.pub user@your-server

# 将私钥内容复制到 GitHub Secret
cat ~/.ssh/github_deploy_key

💡 注意

  • 私钥内容需要完整复制,包括 -----BEGIN OPENSSH PRIVATE KEY----------END OPENSSH PRIVATE KEY-----
  • 确保服务器用户有权限写入部署目录和重启 PM2 服务

关键步骤详解

1. Job 依赖与权限

yaml
remote-deploy:
  runs-on: ubuntu-latest
  needs: build-and-release  # 等待构建完成
  if: github.event_name == 'release' && success()
  permissions:
    actions: read  # 允许下载 artifacts
  • needs: build-and-release:确保构建 Job 成功后才执行部署
  • if: github.event_name == 'release' && success():仅在 Release 触发且构建成功时执行
  • permissions: actions: read:允许下载 artifact(必需)

2. 下载构建产物

yaml
- name: Download Artifact
  uses: actions/download-artifact@v8
  with:
    name: build-output
    path: ./

从第一部分上传的 artifact 中下载 tar.gz 文件。使用 path: ./ 表示下载到当前工作目录。

3. 解压与验证

yaml
- name: Extract Artifact
  run: |
    mkdir -p .output
    tar -xzf *.tar.gz -C .output
    ls -la .output/

- name: Verify Extracted Content
  run: |
    if [ ! -f ".output/nitro.json" ]; then
      echo "ERROR: nitro.json not found!"
      exit 1
    fi
  • 解压 tar.gz 文件到 .output 目录
  • 验证 nitro.json 是否存在(Nuxt 构建产物的标志文件)

4. SCP 传输

yaml
- name: Deploy to Server
  uses: appleboy/scp-action@v0.1.7
  with:
    host: ${{ secrets.DEPLOY_HOST }}
    username: ${{ secrets.DEPLOY_USER }}
    key: ${{ secrets.DEPLOY_SSH_KEY }}
    port: ${{ secrets.DEPLOY_PORT || 22 }}
    source: ".output/"
    target: "/tmp/${{ needs.build-and-release.outputs.project_name }}-build-${{ needs.build-and-release.outputs.version }}/"

使用 appleboy/scp-action.output 目录传输到服务器的临时目录。

💡 路径说明:传输到 /tmp/项目名-build-版本号/ 临时目录,避免直接覆盖线上目录。部署脚本会从中复制文件到正式目录。

5. SSH 远程部署

yaml
- name: Execute Remote Deployment
  uses: appleboy/ssh-action@v1.2.0
  with:
    script: |
      set -e
      PROJECT_NAME="${{ needs.build-and-release.outputs.project_name }}"
      VERSION="${{ needs.build-and-release.outputs.version }}"
      DEPLOY_DIR="/var/www/${PROJECT_NAME}"
      BUILD_DIR="/tmp/${PROJECT_NAME}-build-${VERSION}"

      # 备份
      if [ -d "$DEPLOY_DIR" ]; then
        BACKUP_DIR="/var/www/${PROJECT_NAME}-backup-$(date +%Y%m%d_%H%M%S)"
        cp -r "$DEPLOY_DIR" "$BACKUP_DIR"
      fi

      # 部署(注意:解压后文件直接在 BUILD_DIR 下,没有 .output 层)
      rm -rf ${DEPLOY_DIR}/*
      cp -r ${BUILD_DIR}/* ${DEPLOY_DIR}/

      # 设置权限
      chown -R www-data:www-data ${DEPLOY_DIR}
      chmod -R 755 ${DEPLOY_DIR}

      # 重启 PM2
      pm2 restart ${PROJECT_NAME}

      # 清理
      rm -rf ${BUILD_DIR}

脚本说明:

步骤命令说明
备份cp -r "$DEPLOY_DIR" "$BACKUP_DIR"保存当前版本,方便回滚
部署cp -r ${BUILD_DIR}/* ${DEPLOY_DIR}/复制新文件到正式目录(注意:没有 .output 层)
重启pm2 restart ${PROJECT_NAME}重启 PM2 服务
清理rm -rf ${BUILD_DIR}删除临时文件

⚠️ 关键细节:由于第一部分打包时使用了 tar -czf ${PROJECT_NAME}-${VERSION}.tar.gz -C .output .,解压后文件直接位于 BUILD_DIR 目录下,没有额外的 .output。因此复制时应使用 ${BUILD_DIR}/* 而不是 ${BUILD_DIR}/.output/*

打包与解压的目录结构对比:

阶段目录结构说明
打包前.output/nitro.json, public/, server/构建产物
打包后${PROJECT_NAME}-${VERSION}.tar.gz压缩包内直接是文件
解压后${BUILD_DIR}/nitro.json, public/, server/没有 .output
部署后${DEPLOY_DIR}/nitro.json, public/, server/文件直接在根目录

💡 权限说明chown 将文件所有者设置为 www-data,确保 Nginx 有权限读取静态文件;chmod 755 设置目录权限为所有者可读写执行,组用户和其他用户可读执行。

6. PM2 服务管理

bash
pm2 restart ${PROJECT_NAME}

前提条件:

  • 服务器已安装 PM2:npm install -g pm2
  • 服务已注册:pm2 start /var/www/项目名/server/index.mjs --name 项目名
  • PM2 开机自启:pm2 save && pm2 startup

运行流程

  1. 在 GitHub 仓库中创建新的 Release(如 v0.4.0-alpha11
  2. 第一部分 自动触发:构建 → 打包 → 上传 Release → 上传 artifact
  3. 第二部分 自动触发(等待第一部分完成):
  • 下载 artifact → 解压 → 验证
  • SCP 传输到服务器临时目录
  • SSH 执行备份 → 部署 → PM2 重启 → 清理
  1. 部署完成,服务上线

常见问题

Q: download-artifact 报错 "Artifact not found"?

A: 确保第一部分正确上传了 artifact,且 name: build-output 与上传时一致。

Q: SCP 传输失败,提示 "Permission denied"?

A: 检查以下几点:

  • SSH 私钥是否正确配置
  • 服务器用户是否有写入目标目录的权限
  • 目标目录是否存在(可先 SSH 登录手动创建)

Q: PM2 重启失败,提示 "Process not found"?

A: 首次部署需要先启动服务:

bash
# SSH 登录服务器手动执行一次
pm2 start /var/www/项目名/server/index.mjs --name 项目名 --node-args="--env-file=.env"
pm2 save

提醒

关于 Nuxt.js 项目完整的部署过程可以参考 《从零开始部署 Nuxt 应用到 Ubuntu 22.04 服务器》 这篇博文!

Q: 部署后文件路径不对,服务无法启动?

A: 检查部署脚本中的复制路径。由于打包时使用了 -C .output .,解压后文件直接在 BUILD_DIR 下,应该使用:

bash
cp -r ${BUILD_DIR}/* ${DEPLOY_DIR}/

而不是:

bash
cp -r ${BUILD_DIR}/.output/* ${DEPLOY_DIR}/

Q: 如何回滚到上一个版本?

A: 服务器上保留了备份目录:

bash
# 查看备份
ls -la /var/www/项目名-backup-*
# 恢复备份
rm -rf /var/www/项目名
cp -r /var/www/项目名-backup-20250101_120000 /var/www/项目名
pm2 restart 项目名

总结

本文完成了系列文章的第二部分——远程部署自动化。通过配置 GitHub Actions,我们实现了:

  • 自动下载并解压构建产物
  • SCP 传输到目标服务器
  • SSH 执行远程部署命令
  • 自动备份当前版本
  • PM2 重启服务实现无缝上线

至此,完整的 CI/CD 流水线已打通:

创建 Release → 自动构建 → 自动打包 → 自动上传 → 自动部署 → PM2 重启 → 上线完成

下一篇预告:

第三部分将讲解如何配置邮箱通知,让团队在 CI/CD 执行成功或失败时及时收到邮件提醒,实现全流程可感知。

敬请期待!

💡 确认点

运行此 workflow 后,服务器上的 /var/www/项目名/ 目录应更新为最新版本,PM2 服务状态为 online