Appearance
Nuxt.js 项目 GitHub Actions 自动化 CI/CD 实践(二):远程部署
前言
承接第一篇文章,我们已经实现了创建 Release 时自动构建并打包。本文将讲解如何将构建产物自动部署到远程服务器,实现从代码提交到服务上线的全自动化。
本系列文章回顾:
| 部分 | 目标 | 状态 |
|---|---|---|
| 第一部分 | 构建、打包并上传到 GitHub Release | ✅ 已完成 |
| 第二部分(本文) | 自动部署到远程服务器,PM2 重启服务 | 📍 当前 |
| 第三部分 | CI/CD 邮箱通知 | 待发布 |
达成效果:
当你创建一个 Release 后,第一部分自动完成构建打包,第二部分将自动:
- 下载构建产物
- 解压并验证文件完整性
- SCP 传输到目标服务器
- 备份当前版本
- 部署新文件
- PM2 重启服务
- 清理临时文件
整个过程无需人工干预,真正做到一键发布、自动上线。
NOTICE
本文为系列文章的第二部分,聚焦于远程部署环节。建议先完成第一部分的配置再继续本文。
第二部分目标
当第一部分构建完成后,自动完成:
- 下载构建产物(artifact)
- 解压并验证文件完整性
- SCP 传输到目标服务器
- 备份当前运行版本
- 部署新文件到正式目录
- PM2 重启服务
- 清理临时文件
目录结构(延续第一部分)
.github/
└── workflows/
└── release-bump.yaml # 添加 remote-deploy job完整配置文件
在第一部分的 release-bump.yaml 基础上,添加 remote-deploy Job:
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.100 或 example.com |
DEPLOY_USER | SSH 登录用户名 | root 或 ubuntu |
DEPLOY_SSH_KEY | SSH 私钥内容 | -----BEGIN OPENSSH PRIVATE KEY----- |
DEPLOY_PORT | SSH 端口(可选) | 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 # 允许下载 artifactsneeds: 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
运行流程
- 在 GitHub 仓库中创建新的 Release(如
v0.4.0-alpha11) - 第一部分 自动触发:构建 → 打包 → 上传 Release → 上传 artifact
- 第二部分 自动触发(等待第一部分完成):
- 下载 artifact → 解压 → 验证
- SCP 传输到服务器临时目录
- SSH 执行备份 → 部署 → PM2 重启 → 清理
- 部署完成,服务上线
常见问题
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。