Abstract background

使用 GitHub Actions 部署 Next.js 项目到 VPS

/static/image-20240514011402733-4983f74b.png

流程

配置服务器

首先需要配置 SSH 允许 GitHub Actions 通过 SSH 连接到你的服务器:

  1. 为 GitHub Actions 配置一个系统用户。
  2. 生成 SSH 密钥,将公钥添加到服务器的~/.ssh/authorized_keys文件。
  3. 添加私钥到 GitHub 仓库。

添加用户:

1sudo adduser github 2sudo usermod -aG sudo github # 授予 sudo 权限,可选

生成 SSH 密钥:

1ssh-keygen -t rsa -b 4096 -C "github-actions-node" 2ssh-copy-id -i ~/.ssh/keyname.pub username@remote_host # 假设生成的公钥文件是 keyname.pub。Windows 系统没有 ssh-copy-id,需要手动添加。

添加私钥到 GitHub 仓库:

使用 pbcopy < ~/.ssh/github_actions_node 将私钥复制到剪切板(也可手动复制)。

访问 <你的仓库 URL>/settings/secrets/actions (Settings -> Secrets and Variables -> Actions),点击 New repository secret 添加新的 secret,名称 SSH_KEY(与下面 yaml 文件中对应),内容为私钥内容。

设置 GitHub Actions Workflow:

  1. 配置仓库 Secrets。
  2. 在 GitHub 仓库根目录下创建一个新的.github/workflows目录。
  3. 在这个目录中创建一个 YAML 文件(如 deploy.yml),并定义 CI/CD 流程。

配置 Secrets:

访问 <你的仓库 URL>/settings/secrets/actions (Settings -> Secrets and Variables -> Actions),点击 New repository secret 新增。共需添加以下几个 secret:

  • PROJECT_DIRECTORY: 项目部署路径,可以是相对或绝对路径。相对路径相对于登录用户的~路径,绝对路径注意是否有权限。
  • SSH_KEY:SSH 私钥。[前文](#添加私钥到 GitHub 仓库)已添加。
  • SSH_HOST:SSH 登录服务器的 IP 地址或域名。
  • SSH_USERNAME:SSH 登录用户名。
  • SSH_PORT:SSH 端口,默认为 22。

image-20240514011103747

创建 CI/CD 流程:

在 GitHub 仓库根目录创建一个 .github/workflows/deploy.yml 如下:

1name: Deploy Next.js Project 2 3on: 4 push: 5 branches: 6 - master 7 8jobs: 9 build: 10 runs-on: ubuntu-latest 11 outputs: 12 cache-key: ${{ steps.cache-key.outputs.key }} 13 steps: 14 - name: Checkout Repository 15 uses: actions/checkout@v4 16 17 - name: Setup Node.js 18 uses: actions/setup-node@v4 19 with: 20 node-version: '18' 21 cache: 'npm' 22 23 - name: Install Dependencies 24 run: npm install 25 26 - name: Build Project 27 env: 28 DATABASE_URL: ${{ secrets.DATABASE_URL }} 29 API_KEY: ${{ secrets.API_KEY }} 30 run: | 31 npx prisma migrate dev --name init 32 npm run build 33 34 - name: Generate Cache Key 35 id: cache-key 36 run: echo "cache_key=$(date +%s)" >> $GITHUB_OUTPUT 37 38 - name: Archive Artifacts 39 uses: actions/upload-artifact@v4 40 with: 41 name: built-app 42 path: | 43 .next 44 .velite 45 public 46 package.json 47 prisma 48 node_modules/.prisma 49 next.config.mjs 50 51 deploy: 52 needs: build 53 runs-on: ubuntu-latest 54 timeout-minutes: 30 55 steps: 56 - name: Download Artifacts 57 uses: actions/download-artifact@v4 58 with: 59 name: built-app 60 61 - name: Clean Previous Build 62 uses: appleboy/ssh-action@master 63 with: 64 host: ${{ secrets.SSH_HOST }} 65 username: ${{ secrets.SSH_USERNAME }} 66 key: ${{ secrets.SSH_KEY }} 67 port: ${{ secrets.SSH_PORT }} 68 script: | 69 cd ${{ secrets.PROJECT_DIRECTORY }} 70 rm -rf .next .velite public package.json prisma/migrations prisma/schema.prisma node_modules/.prisma next.config.mjs 71 find prisma -type f ! -name 'prod.db' -delete 72 73 - name: Deploy to Server 74 uses: appleboy/scp-action@master 75 with: 76 host: ${{ secrets.SSH_HOST }} 77 username: ${{ secrets.SSH_USERNAME }} 78 key: ${{ secrets.SSH_KEY }} 79 port: ${{ secrets.SSH_PORT }} 80 source: ".next, .velite, public, package.json, prisma/migrations, prisma/schema.prisma, node_modules/.prisma, next.config.mjs" 81 target: ${{ secrets.PROJECT_DIRECTORY }} 82 83 - name: Install Dependencies on Server 84 uses: appleboy/ssh-action@master 85 with: 86 host: ${{ secrets.SSH_HOST }} 87 username: ${{ secrets.SSH_USERNAME }} 88 key: ${{ secrets.SSH_KEY }} 89 port: ${{ secrets.SSH_PORT }} 90 script: | 91 cd ${{ secrets.PROJECT_DIRECTORY }} 92 export NVM_DIR=~/.nvm 93 source ~/.nvm/nvm.sh 94 NODE_OPTIONS="--max-old-space-size=1536" yarn install 95 NODE_OPTIONS="--max-old-space-size=1536" yarn add sharp --ignore-engines 96 97 - name: Run Prisma Migrations on Server 98 uses: appleboy/ssh-action@master 99 env: 100 DATABASE_URL: ${{ secrets.DATABASE_URL }} 101 API_KEY: ${{ secrets.API_KEY }} 102 with: 103 host: ${{ secrets.SSH_HOST }} 104 username: ${{ secrets.SSH_USERNAME }} 105 key: ${{ secrets.SSH_KEY }} 106 port: ${{ secrets.SSH_PORT }} 107 envs: DATABASE_URL,API_KEY 108 script: | 109 cd ${{ secrets.PROJECT_DIRECTORY }} 110 export NVM_DIR=~/.nvm 111 source ~/.nvm/nvm.sh 112 npx prisma migrate deploy 113 114 - name: Restart Server 115 uses: appleboy/ssh-action@master 116 env: 117 DATABASE_URL: ${{ secrets.DATABASE_URL }} 118 API_KEY: ${{ secrets.API_KEY }} 119 with: 120 host: ${{ secrets.SSH_HOST }} 121 username: ${{ secrets.SSH_USERNAME }} 122 key: ${{ secrets.SSH_KEY }} 123 port: ${{ secrets.SSH_PORT }} 124 envs: DATABASE_URL,API_KEY 125 script: | 126 cd ${{ secrets.PROJECT_DIRECTORY }} 127 export NVM_DIR=~/.nvm 128 source ~/.nvm/nvm.sh 129 pm2 list | grep "blog-velite" && pm2 restart "blog-velite" || pm2 start yarn --name "blog-velite" -- start 130 pm2 save

Restart Server这一步骤中,使用了 yarn 作为部署 VPS 的包管理器。这是因为 yarn 消耗系统资源更少,适合弱鸡 VPS。

NODE_OPTIONS="--max-old-space-size=1536" 增加默认内存限制,否则可能出现类似 FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory 的 OOM 报错。

GPT-4o 如是说:

这个 GitHub Actions 配置文件是用于自动化部署 Next.js 项目的流程。具体配置的解释如下:

这个配置文件是一个 GitHub Actions 工作流,旨在自动化部署一个 Next.js 项目到服务器。以下是对每个部分的详细解释:

文件结构

1name: Deploy Next.js Project
  • name: 定义工作流的名称,这里是 "Deploy Next.js Project"。

触发条件

1on: 2 push: 3 branches: 4 - master
  • on: 指定工作流的触发条件。这里是当代码被推送到 master 分支时触发。

工作任务

构建任务

1jobs: 2 build: 3 runs-on: ubuntu-latest 4 outputs: 5 cache-key: ${{ steps.cache-key.outputs.key }} 6 steps:
  • jobs: 定义工作流中的任务。
  • build: 定义构建任务。
  • runs-on: 指定运行环境,这里是 ubuntu-latest
  • outputs: 定义输出变量,这里是 cache-key
步骤1:检出代码
1 - name: Checkout Repository 2 uses: actions/checkout@v4
  • actions/checkout@v4: 使用 GitHub 官方动作来检出代码库。
步骤2:设置 Node.js 环境
1 - name: Setup Node.js 2 uses: actions/setup-node@v4 3 with: 4 node-version: '18' 5 cache: 'npm'
  • actions/setup-node@v4: 设置 Node.js 环境,指定使用版本 18,并启用 npm 缓存。
步骤3:安装依赖
1 - name: Install Dependencies 2 run: npm install
  • npm install: 安装项目依赖。
步骤4:构建项目
1 - name: Build Project 2 env: 3 DATABASE_URL: ${{ secrets.DATABASE_URL }} 4 API_KEY: ${{ secrets.API_KEY }} 5 run: | 6 npx prisma migrate dev --name init 7 npm run build
  • env: 设置环境变量,包括 DATABASE_URLAPI_KEY
  • npx prisma migrate dev --name init: 运行 Prisma 迁移。
  • npm run build: 构建项目。
步骤5:生成缓存键
1 - name: Generate Cache Key 2 id: cache-key 3 run: echo "cache_key=$(date +%s)" >> $GITHUB_OUTPUT
  • 生成一个基于当前时间的缓存键并输出。
步骤6:存档构建产物
1 - name: Archive Artifacts 2 uses: actions/upload-artifact@v4 3 with: 4 name: built-app 5 path: | 6 .next 7 .velite 8 public 9 package.json 10 prisma 11 node_modules/.prisma 12 next.config.mjs
  • actions/upload-artifact@v4: 上传构建产物作为工件。

部署任务

1 deploy: 2 needs: build 3 runs-on: ubuntu-latest 4 timeout-minutes: 30 5 steps:
  • deploy: 定义部署任务。
  • needs: build: 表示此任务依赖于 build 任务的完成。
  • timeout-minutes: 设置任务超时时间为 30 分钟。
步骤1:下载构建产物
1 - name: Download Artifacts 2 uses: actions/download-artifact@v4 3 with: 4 name: built-app
  • actions/download-artifact@v4: 下载之前上传的构建产物。
步骤2:清理之前的构建
1 - name: Clean Previous Build 2 uses: appleboy/ssh-action@master 3 with: 4 host: ${{ secrets.SSH_HOST }} 5 username: ${{ secrets.SSH_USERNAME }} 6 key: ${{ secrets.SSH_KEY }} 7 port: ${{ secrets.SSH_PORT }} 8 script: | 9 cd ${{ secrets.PROJECT_DIRECTORY }} 10 rm -rf .next .velite public package.json prisma/migrations prisma/schema.prisma node_modules/.prisma next.config.mjs 11 find prisma -type f ! -name 'prod.db' -delete
  • 使用 appleboy/ssh-action@master 通过 SSH 连接到服务器并清理之前的构建文件。
步骤3:部署到服务器
1 - name: Deploy to Server 2 uses: appleboy/scp-action@master 3 with: 4 host: ${{ secrets.SSH_HOST }} 5 username: ${{ secrets.SSH_USERNAME }} 6 key: ${{ secrets.SSH_KEY }} 7 port: ${{ secrets.SSH_PORT }} 8 source: ".next, .velite, public, package.json, prisma/migrations, prisma/schema.prisma, node_modules/.prisma, next.config.mjs" 9 target: ${{ secrets.PROJECT_DIRECTORY }}
  • 使用 appleboy/scp-action@master 通过 SCP 将新的构建文件上传到服务器。
步骤4:在服务器上安装依赖
1 - name: Install Dependencies on Server 2 uses: appleboy/ssh-action@master 3 with: 4 host: ${{ secrets.SSH_HOST }} 5 username: ${{ secrets.SSH_USERNAME }} 6 key: ${{ secrets.SSH_KEY }} 7 port: ${{ secrets.SSH_PORT }} 8 script: | 9 cd ${{ secrets.PROJECT_DIRECTORY }} 10 export NVM_DIR=~/.nvm 11 source ~/.nvm/nvm.sh 12 NODE_OPTIONS="--max-old-space-size=1536" yarn install 13 NODE_OPTIONS="--max-old-space-size=1536" yarn add sharp --ignore-engines
  • 在服务器上安装项目依赖,并添加 sharp 依赖。
步骤5:在服务器上运行 Prisma 迁移
1 - name: Run Prisma Migrations on Server 2 uses: appleboy/ssh-action@master 3 env: 4 DATABASE_URL: ${{ secrets.DATABASE_URL }} 5 API_KEY: ${{ secrets.API_KEY }} 6 with: 7 host: ${{ secrets.SSH_HOST }} 8 username: ${{ secrets.SSH_USERNAME }} 9 key: ${{ secrets.SSH_KEY }} 10 port: ${{ secrets.SSH_PORT }} 11 envs: DATABASE_URL,API_KEY 12 script: | 13 cd ${{ secrets.PROJECT_DIRECTORY }} 14 export NVM_DIR=~/.nvm 15 source ~/.nvm/nvm.sh 16 npx prisma migrate deploy
  • 运行 Prisma 的数据库迁移。
步骤6:重启服务器上的应用
1 - name: Restart Server 2 uses: appleboy/ssh-action@master 3 env: 4 DATABASE_URL: ${{ secrets.DATABASE_URL }} 5 API_KEY: ${{ secrets.API_KEY }} 6 with: 7 host: ${{ secrets.SSH_HOST }} 8 username: ${{ secrets.SSH_USERNAME }} 9 key: ${{ secrets.SSH_KEY }} 10 port: ${{ secrets.SSH_PORT }} 11 envs: DATABASE_URL,API_KEY 12 script: | 13 cd ${{ secrets.PROJECT_DIRECTORY }} 14 export NVM_DIR=~/.nvm 15 source ~/.nvm/nvm.sh 16 pm2 list | grep "blog-velite" && pm2 restart "blog-velite" || pm2 start yarn --name "blog-velite" -- start 17 pm2 save
  • 使用 pm2 进程管理器重启或启动应用。

这个配置文件涵盖了从代码检出、环境设置、依赖安装、项目构建、上传构建产物、清理旧构建、部署新构建、安装服务器依赖、运行数据库迁移到重启应用的完整流程。这确保了项目在每次代码推送到 master 分支时都能自动完成构建和部署。

配置 VPS 服务器

在首次触发 Actions 部署之前,先安装好依赖:

1cd /path/to/deployment # 与 secrets.PROJECT_DIRECTORY 相同 2# 使用 pm2 管理应用 3yarn global add pm2

随后向仓库提交更改,检查 Actions 是否运行。

效果

image-20240514011402733

image-20240514011427738

配置文件使用 yarn startnext start,默认运行在 3000 端口:

image-20240514011631894