寫在前面:在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 Deployon:# 推送到 main 分支時自動觸發(如需改分支,請改這里)push:branches:- main# 允許在 Actions 頁面手動觸發workflow_dispatch:permissions:contents: readjobs:build-and-deploy:runs-on: ubuntu-latest# Job 級別環境變量:默認生產。構建階段會臨時切到 development 以安裝 dev 依賴env:NODE_ENV: productionsteps:# 1) 拉取代碼- name: Checkout repositoryuses: actions/checkout@v4# 2) 選擇 Node 版本(與服務器一致;可改為 20 等)- name: Use Node.js 18uses: actions/setup-node@v4with:node-version: 18cache: 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" ]; thenprintf "%s" "$ENV_FILE_CONTENTS" > .env.productionfi# 4) 安裝依賴(包含 devDependencies,避免構建缺包)- name: Install dependencies (with fallback, include dev deps)env:NPM_CONFIG_PRODUCTION: "false"NODE_ENV: developmentrun: |npm ci || npm install --legacy-peer-deps# 5) 構建(生產環境)- name: Buildenv:NODE_ENV: productionrun: 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 serveruses: appleboy/scp-action@v0.1.7with: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.3with:host: ${{ secrets.SSH_HOST }}username: ${{ secrets.SSH_USER }}password: ${{ secrets.SSH_PASSWORD }}port: ${{ secrets.SSH_PORT }}script_stop: truescript: |set -ecd ${{ secrets.REMOTE_PATH }}mkdir -p currentmv release.tar.gz current/cd currenttar -xzf release.tar.gzrm -f release.tar.gz# 二次兜底:如提供了 ENV_FILE_CONTENTS,這里也會寫入(與構建前一致)if [ ! -z "${{ secrets.ENV_FILE_CONTENTS }}" ]; thenecho "${{ secrets.ENV_FILE_CONTENTS }}" > .env.productionfi# 服務器僅安裝生產依賴,減小體積npm ci --omit=dev# 使用 PM2 平滑重載;若無進程則創建if command -v pm2 >/dev/null 2>&1; thenpm2 startOrReload ecosystem.config.js --env production || pm2 start ecosystem.config.js --env productionpm2 saveelsenpm i -g pm2pm2 start ecosystem.config.js --env productionpm2 savefi