2026/08/27

當 Next.js 專案開始進入正式環境後,CI/CD 要處理的事情其實不只是:
1npm install
2npm run build
3deploy真正進入團隊開發後,會開始遇到更多問題:
因此我後來把 Next.js 專案的 GitLab CI/CD 拆成幾個階段:
1verify
2 ↓
3security-check
4 ↓
5tag
6 ↓
7build
8 ↓
9deploy目標不是單純做到「自動部署」,而是讓程式在進入正式環境之前,就先經過幾層基本的品質與風險控制。
第一層是最基本的程式碼檢查:
1npx next typegen
2npm run lint
3npm run tsc只要建立 Merge Request,就先跑 lint 與 TypeScript 檢查。
1lint_and_type_check:
2 stage: verify
3
4 before_script:
5 - npm ci --include=dev --prefer-offline --no-audit
6
7 script:
8 - npx next typegen
9 - npm run lint
10 - npm run tsc這一層的目的很單純:
不要讓明明可以由機器發現的錯誤,留到 Code Review 甚至部署後才被人發現。
其中 next typegen 是 Next.js 新版本中特別需要注意的一步。
App Router 部分型別資訊會由 Next.js 產生,因此 CI 環境如果直接執行 TypeScript,可能會缺少必要的 generated types。
所以流程會先執行:
1next typegen再跑:
1tsc讓 CI 使用的型別資訊更接近實際 Next.js build。
另一個我加入的設定是 changes。
只有真的影響程式碼或建置流程的檔案發生變更時,才啟動主要 Pipeline。
例如:
1changes:
2 - src/**/*
3 - public/**/*
4 - scripts/**/*
5 - next.config.ts
6 - package.json
7 - package-lock.json
8 - Dockerfile
9 - .gitlab-ci.yml如果只是修改 README,就沒有必要重新跑一次完整 Next.js Pipeline。
目前主要處理幾種情境:
1Merge Request
2master / develop push
3手動執行
4每日排程其他情況則直接略過。
這看起來只是小設定,但當 CI 次數開始增加後,可以省掉不少沒有意義的 Runner 時間。
Next.js 專案另一個很容易被忽略的問題,是 dependency vulnerability。
因此 Pipeline 另外建立:
1security-check其中會執行:
1npm audit --json --audit-level=high --omit=dev我的策略是只要 Production dependency 出現 High 以上漏洞,就直接讓 CI fail。
概念大概是:
1High / Critical vulnerability
2 ↓
3 CI Fail也就是安全問題不只是顯示 warning,而是真的會阻止後續流程繼續。
除了 npm audit,Pipeline 也會產生 SBOM。
目前包含:
1audit.json
2sbom.xml
3sbom.jsonSBOM 使用 CycloneDX:
1npx @cyclonedx/cyclonedx-npm \
2 --output-format xml \
3 --output-file sbom.xml
4
5npx @cyclonedx/cyclonedx-npm \
6 --output-format json \
7 --output-file sbom.json這樣每次安全掃描,都能留下當下版本的 Software Bill of Materials。
它解決的是另一個問題:
某個套件幾個月後爆出 CVE 時,當時部署的版本到底有沒有使用它?
如果只有 package.json,不一定能準確反映當時實際安裝的 dependency tree。
SBOM 則提供一份更完整的依賴清單,之後需要追查資安問題時會方便很多。
Dependency vulnerability 並不會因為專案沒有新的 commit,就停止出現。
假設某個套件今天被揭露 Critical CVE,但你的網站兩個月都沒有部署:
1沒有新的 commit
2≠
3沒有新的安全風險所以 Security Pipeline 也支援 Scheduled Pipeline。
例如每天固定掃描 Production branch。
1每日排程
2 ↓
3npm audit
4 ↓
5產生 SBOM
6 ↓
7High / Critical?
8 ↓
9通知團隊這樣安全檢查就從:
有人部署時順便檢查
變成:
即使沒有人修改程式,也會持續檢查目前使用中的 dependencies。
安全檢查產生的:
1audit.json
2sbom.xml
3sbom.json都會保留成 GitLab Artifacts。
例如:
1artifacts:
2 when: always
3 paths:
4 - sbom.xml
5 - sbom.json
6 - audit.json
7 expire_in: 1 week即使 Pipeline 因為漏洞而失敗,也還是能下載完整結果進一步確認。
而不是 CI 只丟下一句:
1npm audit failed然後工程師再自己重新跑一次。
這種事情一天做一次叫除錯,做久了就是浪費生命。
Production build 另外加了一層 Release Version 控制。
流程大概是:
1master
2 ↓
3Manual Tag
4 ↓
5Build例如輸入:
11.4.2Pipeline 會先檢查:
x.y.z如果版本合法,就產生新的 Git Tag。
1git tag -a "$RELEASE_VERSION" \
2 -m "Release $RELEASE_VERSION"而 Production build 也會再次確認目前 commit 是否真的存在 Tag。
1VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "")
2
3if [ "$CI_COMMIT_BRANCH" = "master" ] && [ -z "$VERSION" ]; then
4 echo "master build 必須來自 tag_release"
5 exit 1
6fi這麼做最大的好處不是 Git Tag 本身,而是建立:
1Code
2 ↓
3Build
4 ↓
5Deployment
6 ↓
7Release之間的對應關係。
否則專案做久之後,很容易開始出現:
Production 現在到底是哪一版?
然後大家開始翻 Git history。
另一個容易踩雷的地方是 CI/CD Environment Variables。
例如:
1NEXT_PUBLIC_BASE_URL
2NEXT_PUBLIC_API_URL
3NEXTAUTH_URL
4NEXTAUTH_SECRET
5API_URL如果其中一個沒有設定,與其讓 Next.js build 跑到一半才爆掉,不如一開始就直接檢查。
1for v in \
2 NEXT_PUBLIC_BASE_URL \
3 NEXT_PUBLIC_API_URL \
4 NEXTAUTH_URL \
5 NEXTAUTH_SECRET \
6 API_URL
7do
8 if [ -z "${!v:-}" ]; then
9 echo "$v MISSING"
10 exit 1
11 fi
12done而且只檢查變數存在,不直接把內容印出來。
這點很重要。
CI Log 通常很多人都可以看到,把 Secret 印進 log 基本上是在幫未來的資安事件寫序章。
Next.js build 採用 standalone output。
Build 完成後真正需要部署的是:
1.next/standalone
2.next/static
3public因此 CI build:
1npm run build
2
3cp -r public .next/standalone/ || true
4
5mkdir -p .next/standalone/.next
6
7cp -r .next/static .next/standalone/.next/相比直接把整個 repository 丟到 Production Server,再執行:
1npm install
2npm run build這種方式可以讓:
Build 發生在 CI,Production Server 只負責執行 Build Artifact。
也讓 Build Environment 與 Runtime Environment 的責任更清楚。
Deployment 並不會直接覆蓋原本的 Application Directory。
而是建立類似:
1releases/
2 ├── 2026-08-25_xxxxx
3 ├── 2026-08-26_xxxxx
4 └── 2026-08-27_xxxxx
5
6current -> releases/2026-08-27_xxxxx每次 Deployment 都會產生一組 RELEASE_ID:
1RELEASE_TS=$(TZ=Asia/Taipei date +%Y-%m-%d_%H-%M-%S_TW)
2
3RELEASE_ID="${RELEASE_TS}_${CI_COMMIT_SHORT_SHA}"例如:
12026-08-27_14-30-22_TW_a1b2c3d而 Production 實際執行的版本,則由:
1current這個 symbolic link 決定。
切換版本只需要:
1ln -sfn "$RELEASE_DIR" "$DEPLOY_PATH/current"這種做法最大的優點是:
舊版本不會因為新版本部署而直接消失。
因此之後需要 Rollback 時,不需要重新 build 舊版。
當然,也不能讓:
1releases/一路長到伺服器硬碟開始懷疑人生。
所以 Pipeline 另外設定:
1KEEP_RELEASES: '3'Deployment 完成後自動清除更舊的 Release,只保留最近幾版。
概念就是:
1Latest
2Previous
3Previous - 1留下足夠的 Rollback 空間,同時避免 Deployment Artifact 無限累積。
因為每個版本都是獨立 Release,所以可以另外建立一個:
1rollback_applicationJob。
使用時可以指定:
1TARGET_RELEASE例如:
12026-08-26_15-22-31_TW_f4e5d6Pipeline 會先確認該 Release 是否存在,而且 .next/standalone/server.js 是否完整。
確認後:
1指定 TARGET_RELEASE
2 ↓
3切換 current symlink
4 ↓
5重新啟動 PM2如果沒有指定 TARGET_RELEASE,則預設使用上一個版本。
這樣如果 Production 發現功能問題,不需要:
1git revert
2↓
3重新 Build
4↓
5重新 Deploy而是直接重新使用已經存在的 Build Artifact。
Rollback 的成本會低很多。
最後是通知。
Deployment 完成後,通知內容會包含:
1專案
2環境
3網址
4Commit
5Release ID
6最近可回滾版本
7Pipeline
8觸發者Security Pipeline 則包含:
1High vulnerabilities
2Critical vulnerabilities
3Branch
4Pipeline
5觸發者這些資訊本身看起來很普通,但實際出問題時非常重要。
至少可以快速回答:
1什麼時候部署?
2誰部署?
3部署哪個 Commit?
4目前是哪個 Release?
5可以切回哪些版本?
6Security Scan 為什麼失敗?而不是等到事情發生後,再到 GitLab 裡考古。
整理之後,整套流程大概會變成:
1Merge Request
2 │
3 ▼
4Lint + Type Check
5 │
6 ▼
7Merge develop / master
8 │
9 ▼
10Security Check
11 npm audit + SBOM
12 │
13 ▼
14Production Tag
15 │
16 ▼
17Next.js Build
18 standalone artifact
19 │
20 ▼
21Manual Deploy
22 │
23 ▼
24Create Release
25 │
26 ▼
27Switch current
28 │
29 ▼
30PM2 Start
31 │
32 ▼
33Deployment Notification如果 Production 後續發現問題:
1Manual Rollback
2 │
3 ▼
4指定 TARGET_RELEASE
5 │
6 ▼
7切換 current symlink
8 │
9 ▼
10Restart PM2一開始我對 CI/CD 的想像其實很單純:
幫我把程式部署上去就好。
但實際把流程補完整後,Deployment 反而只是其中一部分。
整套流程真正處理的是:
1程式品質
2↓
3Dependency Security
4↓
5Release Traceability
6↓
7Build Artifact
8↓
9Deployment
10↓
11Manual Rollback
12↓
13Notification好的 CI/CD 並不能保證程式永遠不會出錯。
它真正能做到的是:
讓可以自動發現的問題更早失敗,並且讓每一次部署都有清楚的版本、紀錄與回復方式。
對我來說,這才是 CI/CD 最重要的價值。
你的意見會幫助我們持續改善內容。