写在前面:在github上使用CI/CD部署Nextjs项目,具体配置可以按照自己的实际的修改
这是我的项目配置,仅供参考
后端项目可以参考:使用CI/CD部署后端项目
正文开始
项目名(PROJECT_NAME)- CI/CD 部署指南(GitHub Actions + SSH + PM2)
本项目已内置基于 GitHub Actions 的 CI/CD。它会在 main
分支有变更或手动触发时:
- 安装依赖并构建 Next.js 产物
- 通过 SCP 将构建产物上传至服务器指定目录
- 通过 SSH 调用 PM2 平滑重载运行中的服务
1. 文件位置
- 工作流文件(部署文件示例已经放到文末):
.github/workflows/deploy.yml
2. 前置条件
- 服务器已安装 Node.js 18.x 与 npm(与工作流一致)
- 服务器已安装 PM2:
npm i -g pm2
- 服务器部署目录存在且对 SSH 用户可写,例如:
/var/www/PROJECT_NAME
3. 仓库 Secrets 配置
在 GitHub → 仓库 → Settings → Secrets and variables → Actions 中添加:
必填
SSH_HOST
:服务器 IP/域名SSH_USER
:SSH 用户名SSH_PORT
:SSH 端口(如 22)SSH_PASSWORD
:SSH 登录密码(如改用密钥见文末)REMOTE_PATH
:服务器部署根目录(例如/var/www/PROJECT_NAME
)
可选
ENV_FILE_CONTENTS
:用于生成.env.production
的完整文本。例如:NEXT_PUBLIC_API_URL=https://api.your-domain.com NEXT_PUBLIC_LANGUAGE=en NEXT_PUBLIC_WALLETCONNECT_ID=xxxxxxx
说明
- 工作流会在“构建前”把
ENV_FILE_CONTENTS
写为.env.production
,确保NEXT_PUBLIC_*
变量参与 Next.js 打包。
4. 服务器目录结构(默认)
工作流会将 release.tar.gz
上传至 REMOTE_PATH
并解压到 REMOTE_PATH/current
下:
REMOTE_PATH/
└── current/
├── .next/
├── public/
├── ecosystem.config.js
├── package.json
├── package-lock.json
├── .env.production (可选)
└── ...
5. 触发部署
- 自动:向
main
分支推送代码会自动触发 - 手动:GitHub → Actions → 选择
CI/CD Deploy
→Run workflow
→ 选择main
6. 运行流程概览
- Checkout 代码
- 使用 Node 18 安装依赖(包含 devDependencies)并构建
- 压缩构建产物与必要文件为
release.tar.gz
- 通过 SCP 上传到服务器
REMOTE_PATH
- 通过 SSH:
- 解压到
REMOTE_PATH/current
- 写入
.env.production
(如提供) npm ci --omit=dev
pm2 startOrReload ecosystem.config.js --env production
- 解压到
7. PM2 常用命令
pm2 ls # 查看进程
pm2 logs --lines 100 # 查看日志
pm2 restart <name|id> # 重启
pm2 stop <name|id> # 停止
pm2 delete <name|id> # 删除
8. 回滚思路(简易)
当前流程将产物解压到 current/
。若需要回滚,推荐:
- 在服务器保留历史版本目录(可扩展工作流增加
releases/
与符号链接),或 - 临时将上一份稳定包重新上传并覆盖
current/
后pm2 reload
。
9. 常见问题与排查
- 构建期报
Cannot find module 'xxx'
:确保安装步骤包含 devDependencies(本工作流已处理)。 - SCP/SSH 失败:检查
SSH_HOST/USER/PORT/PASSWORD
是否正确,服务器防火墙、安全组、端口开放情况。 - 权限问题:确保
REMOTE_PATH
对SSH_USER
可写,如需:sudo chown -R <user>:<user> /var/www/PROJECT_NAME
。 - 环境变量不生效:确认
ENV_FILE_CONTENTS
已填写,变量名与代码中一致(例如NEXT_PUBLIC_API_URL
)。
10. 切换为 SSH 密钥登录(可选,更安全)
- 本地生成密钥:
ssh-keygen -t ed25519 -C "deploy" -N "" -f ~/.ssh/PROJECT_NAME_deploy
- 将
~/.ssh/PROJECT_NAME_deploy.pub
追加到服务器~/.ssh/authorized_keys
- 在仓库 Secrets 新增:
SSH_KEY
(粘贴私钥全文),并把工作流中password: ${{ secrets.SSH_PASSWORD }}
改为key: ${{ secrets.SSH_KEY }}
(scp/ssh 两处)
11. 调整 Node 版本
- 服务器与工作流默认使用 Node 18。如需升级:同时升级服务器 Node 与工作流的
actions/setup-node
版本号,保持一致。
如需灰度、分环境(staging/prod)或保留多版本回滚,请联系维护者扩展工作流(增加 environments
与 releases
目录策略)。
附:示例工作流(脱敏,含注释与可改项)
# 工作流名称,会显示在 Actions 列表中
name: CI/CD Deploy
on:
# 推送到 main 分支时自动触发(如需改分支,请改这里)
push:
branches:
- main
# 允许在 Actions 页面手动触发
workflow_dispatch:
permissions:
contents: read
jobs:
build-and-deploy:
runs-on: ubuntu-latest
# Job 级别环境变量:默认生产。构建阶段会临时切到 development 以安装 dev 依赖
env:
NODE_ENV: production
steps:
# 1) 拉取代码
- name: Checkout repository
uses: actions/checkout@v4
# 2) 选择 Node 版本(与服务器一致;可改为 20 等)
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
# 3) 写入 .env.production(可选)
# 值来自仓库 Secret: ENV_FILE_CONTENTS(整段文本,包含多行 KEY=VALUE)
- name: Create .env.production from secrets (if provided)
env:
ENV_FILE_CONTENTS: ${{ secrets.ENV_FILE_CONTENTS }}
run: |
if [ -n "$ENV_FILE_CONTENTS" ]; then
printf "%s" "$ENV_FILE_CONTENTS" > .env.production
fi
# 4) 安装依赖(包含 devDependencies,避免构建缺包)
- name: Install dependencies (with fallback, include dev deps)
env:
NPM_CONFIG_PRODUCTION: "false"
NODE_ENV: development
run: |
npm ci || npm install --legacy-peer-deps
# 5) 构建(生产环境)
- name: Build
env:
NODE_ENV: production
run: npm run build
# 6) 仅打包需要的文件(如需额外文件,按需在此补充)
- name: Prepare artifact (ship only what is needed)
run: |
tar -czf release.tar.gz \
.next \
public \
package.json \
package-lock.json \
next.config.js \
ecosystem.config.js \
tsconfig.json \
postcss.config.js \
tailwind.config.js
# 7) 上传产物到服务器(以下 5 个值均来自仓库 Secrets)
# - SSH_HOST:服务器 IP/域名(需改为你的)
# - SSH_USER:SSH 用户名(需改为你的)
# - SSH_PASSWORD:SSH 密码(如改用密钥见文档)
# - SSH_PORT:SSH 端口(默认 22,可按需修改)
# - REMOTE_PATH:部署目录(需改为你的,例如 /var/www/your-app)
- name: Upload artifact to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
source: "release.tar.gz"
target: ${{ secrets.REMOTE_PATH }}
# 8) 服务器上解压、装产线依赖并用 PM2 启动/热重载
- name: Deploy on server (extract, install prod deps, reload pm2)
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
script_stop: true
script: |
set -e
cd ${{ secrets.REMOTE_PATH }}
mkdir -p current
mv release.tar.gz current/
cd current
tar -xzf release.tar.gz
rm -f release.tar.gz
# 二次兜底:如提供了 ENV_FILE_CONTENTS,这里也会写入(与构建前一致)
if [ ! -z "${{ secrets.ENV_FILE_CONTENTS }}" ]; then
echo "${{ secrets.ENV_FILE_CONTENTS }}" > .env.production
fi
# 服务器仅安装生产依赖,减小体积
npm ci --omit=dev
# 使用 PM2 平滑重载;若无进程则创建
if command -v pm2 >/dev/null 2>&1; then
pm2 startOrReload ecosystem.config.js --env production || pm2 start ecosystem.config.js --env production
pm2 save
else
npm i -g pm2
pm2 start ecosystem.config.js --env production
pm2 save
fi