FE Day 41 部署實戰
執行需求:需外部服務帳號。今天是「前端開發實戰:React 與 Next.js 全套」系列的第四十一天,貫穿專案「預約管理系統前端」進入上線階段。前 40 天我們把 11 個頁面、角色導向、資料層、測試與效能都做完,今天把整套部署流程跑完:建立 Vercel 專案、設定三層環境變數、串接 GitHub Actions 做持續整合、啟用 Speed Insights 與 Sentry、設定自訂網域與 HTTPS,並補上一條不依賴 Vercel 的自架伺服器路線。本文所有設定都以 2026 年 3 月的主流版本為準,環境變數與金鑰一律放在平台的環境設定裡,不寫進程式碼。
引言
部署是前端最容易被低估的工作。看起來「把程式碼推上去就好」,但真正上線後才會遇到:環境變數少設一個就整頁壞、預覽環境不小心連到正式資料庫、原始碼對照檔(source map)沒上傳所以錯誤堆疊看不懂、持續整合沒接好所以合併前不知道有沒有壞。這些都不是「寫程式的問題」,而是「部署工程的問題」。
貫穿專案的「預約管理系統」是個小型服務業線上預約平台(情境涵蓋攝影棚、健身教練、語言家教、諮詢工作室,所有資料皆為虛構示範)。前 40 天我們完成了 11 個頁面、admin 與 customer 兩種角色的分流、Day 36 的資料層、Day 37 的錯誤處理、Day 39 的效能調整與 Day 40 的測試套件。今天要把這些成果推上正式環境:前端部署到 Vercel,後端維持 Web 系列的 FastAPI(自架或放在託管平台),並把 CORS、cookie 與環境變數對齊。Day 41 是「可上線的完整系統」這個里程碑的兌現日。
今天的內容分五段:第一段把部署選項與環境分離講清楚;第二段走 Vercel 的完整流程(環境變數、設定檔、持續整合、Sentry、自訂網域);第三段補一條自架伺服器路線(standalone 輸出、systemd、Caddy);第四段整理常見錯誤;第五段說明部署後的驗收與巡檢。讀完之後你會拿到完整的部署設定、一份 CI workflow、一份 Runbook,以及一份部署後的驗收清單。
貫穿專案共用設定(Day 31–44 沿用)
今天所有部署相關設定都建立在 Day 31–40 的技術堆疊上:
- 框架:Next.js 15(App Router、Server Components 預設)、React 19.x、TypeScript 5.9
- 樣式:Tailwind CSS 4.x、CSS 變數主題
- 後端:FastAPI 預約管理系統(Web 系列 Day 35–44 定義的 REST API),前端部署到 Vercel、後端維持獨立主機
- 認證:access token 與 refresh token 走 cookie,沿用 Day 38 的 AuthProvider
- 監控:Day 30 的
lib/monitor.ts(雲端與本地 noop 雙軌) - 圖片:Day 39 的
next/image設定,遠端網域cdn.hao-code.com - 測試:Day 40 的 Vitest 與 Playwright(CI 必跑)
這份設定從 Day 31 到 Day 44 不變。今天的部署是把這套堆疊推到正式環境;任何「部署時才發現」的問題(環境變數沒設、原始碼對照檔沒上傳、CORS 沒開)都是沒在 Day 31–40 先把設定想清楚的後果。
原理:部署選項與環境分離
部署的本質是「把程式碼變成使用者可以存取的系統」。對 Next.js 加 FastAPI 的中型應用來說,流程可以拆成五段:開發者提交程式碼、持續整合跑檢查、平台建置並部署到預覽網址、測試人員在預覽環境驗證、合併到主分支後自動部署到正式環境。每一段的失敗代價不同:CI 失敗只影響這一個 PR、預覽失敗只影響這個分支、正式環境失敗則影響所有使用者。所以「在哪一段抓到錯」是部署工程的核心設計。
環境分離是另一個核心觀念。平台通常內建三個環境:正式環境(給真實使用者)、預覽環境(給每個 PR 一個獨立網址)、開發環境(本機的 dev server)。三者要用不同的環境變數:正式環境指向正式 API 與正式的錯誤追蹤專案,預覽環境指向測試後端,開發環境走 mock 模式。把三層分清楚,就能避免「預覽環境連到正式後端,把測試預約寫進真實資料」的災難。
環境變數也要依「誰讀得到」分類。前端環境變數分兩種:以 NEXT_PUBLIC_ 開頭的會被打包進瀏覽器端的程式碼(任何使用者都看得到,因此絕對不能放秘密);沒有這個前綴的只在伺服器端可讀(例如 API 路由、Server Component),適合放金鑰。例如錯誤追蹤的 DSN 可以公開(它只負責送出事件),但上傳原始碼對照檔用的授權權杖是秘密,只在建置階段使用,必須留在伺服器端。
| 比較面向 | Vercel(本篇主要路線) | 自架伺服器(替代路線) |
|---|---|---|
| 建置與部署 | 推上 Git 自動建置,零維運 | 自己跑建置指令並重啟服務 |
| 預覽環境 | 每個 PR 自動一個網址 | 要自己搭,通常省略 |
| HTTPS | 自動申請與續簽 | 用 Caddy 或 Let's Encrypt 自動化 |
| 成本與掌控 | 免費方案即可起步,掌控度低 | 固定月費,設定完全自控 |
| 適合情境 | 想快速上線、不想維運主機 | 有資安或合規限制、想完全自控 |
還有一個值得先講清楚的特性是冷啟動。無伺服器函式長時間沒被呼叫會進入「冷」狀態,下一次呼叫需要幾百毫秒準備執行環境。對小型應用影響不大,但對高頻的 API 端點會被使用者察覺。對應做法有三種:升級方案的常駐執行環境、把低流量端點放進啟動更快的邊緣函式、或用快取避免每次都打到冷啟動的函式。今天的部署先不處理,Day 43 的技術手冊與交接會把「已知限制」寫進 Runbook。
完整實作:Vercel 部署流程
第一步是把專案推上版本控制並連結平台。以下的網域與主機名稱都是示範,請換成你自己的:
# 第一次部署前的準備
git init
git remote add origin git@github.com:hao-code/booking-frontend.git
git add .
git commit -m "feat: 預約管理系統前端(Day 41 部署版)"
git push -u origin main
# 在 Vercel Dashboard 選 Import Project -> 選 GitHub repo -> 選 Next.js 框架
# 預設 build 指令:next build
# 預設 output 目錄:.next
# 預設 install 指令:npm install(我們改成 pnpm install)
建立專案後,平台會給一個預設網域(例如 booking-frontend-hao.vercel.app),這是正式環境的入口。每個 PR 也會自動建立獨立的預覽網址,方便測試人員在合併前驗證。
第二步是把環境變數的驗證寫進程式,讓「少設一個變數」在建置階段就失敗,而不是等到使用者回報。這份驗證會在啟動時執行,缺值就立即丟出錯誤:
// lib/env.ts
// 在 build 與啟動時早期驗證環境變數,缺一個就 fail-fast。
type EnvShape = {
NEXT_PUBLIC_API_BASE_URL: string;
NEXT_PUBLIC_API_MODE: "mock" | "live";
NEXT_PUBLIC_SENTRY_DSN?: string;
SENTRY_AUTH_TOKEN?: string;
SENTRY_ORG?: string;
SENTRY_PROJECT?: string;
};
function readEnv(): EnvShape {
const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
const apiMode = process.env.NEXT_PUBLIC_API_MODE;
if (!apiBaseUrl) throw new Error("NEXT_PUBLIC_API_BASE_URL 未設定");
if (apiMode !== "mock" && apiMode !== "live") {
throw new Error(`NEXT_PUBLIC_API_MODE 必須是 mock 或 live,目前是 ${apiMode}`);
}
// 錯誤追蹤的四個變數要同進同退,避免只設了一半。
const sentry = [
process.env.NEXT_PUBLIC_SENTRY_DSN,
process.env.SENTRY_AUTH_TOKEN,
process.env.SENTRY_ORG,
process.env.SENTRY_PROJECT,
];
const filled = sentry.filter(Boolean).length;
if (filled !== 0 && filled !== 4) {
throw new Error("錯誤追蹤的四個環境變數必須同時設定或同時留空");
}
return {
NEXT_PUBLIC_API_BASE_URL: apiBaseUrl,
NEXT_PUBLIC_API_MODE: apiMode,
NEXT_PUBLIC_SENTRY_DSN: sentry[0],
SENTRY_AUTH_TOKEN: sentry[1],
SENTRY_ORG: sentry[2],
SENTRY_PROJECT: sentry[3],
};
}
export const env = readEnv();
這份驗證把「什麼時候才知道環境變數少設」從「使用者回報」提前到「建置失敗」,省下大量除錯時間。接著在平台的環境設定裡分三層填入。公開的值可以直接寫;祕密值(例如上傳原始碼對照檔的授權權杖)只在平台後台設定,程式碼裡只讀取變數名稱:
# 正式環境(給真實使用者;網域為示範,請換成自己的)
NEXT_PUBLIC_API_BASE_URL=https://api.hao-code.com
NEXT_PUBLIC_API_MODE=live
NEXT_PUBLIC_APP_URL=https://booking.hao-code.com
# 預覽環境(給 PR 預覽;指向測試後端,避免寫入正式資料)
NEXT_PUBLIC_API_BASE_URL=https://api-staging.hao-code.com
NEXT_PUBLIC_API_MODE=live
# 開發環境(本機 dev server,不需要真實後端)
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
NEXT_PUBLIC_API_MODE=mock
# 以下為祕密值,只在平台後台設定,不會出現在程式碼或版控:
# NEXT_PUBLIC_SENTRY_DSN、SENTRY_AUTH_TOKEN、SENTRY_ORG、SENTRY_PROJECT
三層的差異很關鍵。正式環境用正式後端與正式的錯誤追蹤專案;預覽環境用測試後端與獨立的追蹤專案,避免測試流量觸發正式告警;開發環境走 mock,不需要後端也能跑。錯誤追蹤刻意分兩個專案是常見做法:預覽環境的錯誤不該跟正式環境混在一起,否則每次測試都會跳出假的告警。
第三步是設定檔。Next.js 的設定放在 next.config.ts,平台專屬設定放 vercel.json。我們在設定裡加入安全標頭,並讓同一份設定也能支援自架(稍後說明):
// next.config.ts
import type { NextConfig } from "next";
const isSelfHost = process.env.SELF_HOST === "true";
const nextConfig: NextConfig = {
reactStrictMode: true,
// 自架時輸出 standalone,讓 .next/standalone 內含精簡的 node server。
output: isSelfHost ? "standalone" : undefined,
images: {
formats: ["image/avif", "image/webp"],
remotePatterns: [{ protocol: "https", hostname: "cdn.hao-code.com" }],
},
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
],
},
];
},
};
export default nextConfig;
// vercel.json
// 只放平台專屬設定;建置指令與環境變數在 Dashboard 設定。
{
"framework": "nextjs",
"buildCommand": "pnpm build",
"installCommand": "pnpm install --frozen-lockfile",
"regions": ["hnd1"],
"headers": [
{
"source": "/(.*)",
"headers": [{ "key": "X-Robots-Tag", "value": "index, follow" }]
}
]
}
next.config.ts 有三個重點。第一,output 依環境切換:部署到平台時留空(平台自己處理),自架時輸出 standalone。第二,images.remotePatterns 明確列出允許的最佳化來源網域,沒有列出的網域會被拒絕,這是防止圖片被盜連的第一道防線。第三,安全標頭(nosniff、Referrer-Policy、HSTS)統一在這裡加上,所有路由都繼承,不必逐頁設定。
第四步是持續整合。我們用 GitHub Actions 在每個 PR 與合併到主分支時跑檢查,通過才允許合併。NEXT_PUBLIC_API_MODE 在 CI 設為 mock,讓測試不需要真實後端:
// .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
NEXT_PUBLIC_API_MODE: mock
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm test:e2e
矩陣測試同時跑 Node 22 與 24 兩個長期支援版本,確保程式碼在兩個版本都能跑,避免「某天升級 Node 就壞」。整個流程(lint、型別檢查、單元測試、端對端測試)大約 3 到 5 分鐘,對小型專案足夠;若之後變慢,再考慮快取或自架執行器。
第五步是啟用真實使用者效能量測與錯誤追蹤。前者只要在根 layout 放一個元件就會自動收集 Core Web Vitals;後者在建置階段上傳原始碼對照檔,讓線上錯誤的堆疊能對回原始碼:
// app/layout.tsx(節錄)
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import type { ReactNode } from "react";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="zh-Hant">
<body>
{children}
<SpeedInsights />
<Analytics />
</body>
</html>
);
}
SpeedInsights 會自動量測 LCP、CLS、INP 等指標並送到平台後台,Analytics 負責頁面瀏覽與自訂事件,兩者都不需要金鑰、在第一次部署後就會生效。我們不需要自己寫 web-vitals 的蒐集程式,Day 39 的 beacon 機制可以保留作為自訂事件的補充。
第六步是自訂網域。在平台的專案設定裡輸入網域(示範為 booking.hao-code.com),平台會給出 DNS 設定,去網域服務商設定後等幾分鐘讓憑證自動核發。平台用自動化的憑證服務簽發與續簽 HTTPS,不需要手動維護。若使用自架路線,這一步改用 Caddy 自動處理(見下一段)。
第七步是加一個健康檢查端點,讓負載平衡與外部監控可以確認服務活著:
// app/api/health/route.ts
// 給負載平衡與監控用的健康檢查端點。
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
return NextResponse.json({ status: "ok", at: new Date().toISOString() });
}
部署完成後,用一支小腳本做煙霧測試,確認關鍵頁面都回 200。這支腳本可以在流程末端自動執行,也可以由值班者手動跑:
// scripts/smoke.mjs
// 部署後的最小驗收:確認首頁、服務清單、登入頁與建立預約頁都回 200。
const base = process.env.SMOKE_BASE_URL ?? "http://localhost:3000";
const paths = ["/", "/services", "/login", "/bookings/new"];
let failed = 0;
for (const path of paths) {
const res = await fetch(`${base}${path}`);
const ok = res.status === 200;
if (!ok) failed += 1;
console.log(`${ok ? "通過" : "失敗"} ${path} -> ${res.status}`);
}
if (failed > 0) process.exit(1);
console.log("煙霧測試全部通過");
替代路線:自架伺服器
不是每個團隊都能把前端放在第三方平台。有資安或合規限制時,可以在自己的虛擬主機上跑。Next.js 支援 output: "standalone",建置後會產生一個包含精簡 node server 的目錄,只需要 Node.js 執行環境與一個反向代理即可,不必額外安裝容器。以下用 systemd 管理服務、用 Caddy 做反向代理並自動處理 HTTPS:
# /etc/systemd/system/booking-frontend.service
[Unit]
Description=booking-frontend
After=network.target
[Service]
Type=simple
WorkingDirectory=/srv/booking-frontend
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/srv/booking-frontend/.env.production
ExecStart=/usr/bin/node .next/standalone/server.js
Restart=always
User=deploy
[Install]
WantedBy=multi-user.target
# /etc/caddy/Caddyfile:自動申請與續簽 TLS 憑證
booking.hao-code.com {
encode gzip
reverse_proxy 127.0.0.1:3000
}
更新版本的流程也很單純:拉取最新程式碼、安裝相依、以自架模式建置、把靜態資源複製進 standalone 目錄、重啟服務,最後用健康檢查確認:
# 在自架伺服器上更新版本
ssh deploy@booking-server
cd /srv/booking-frontend
git pull --ff-only
pnpm install --frozen-lockfile
SELF_HOST=true NEXT_PUBLIC_API_MODE=live pnpm build
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static
sudo systemctl restart booking-frontend
curl -fsS https://booking.hao-code.com/api/health && echo "更新完成"
自架路線有三個要自己負責的地方。第一,HTTPS 憑證:Caddy 會自動申請與續簽,但網域的 DNS 必須先指到這台主機。第二,靜態資源:public 目錄與 .next/static 必須手動複製進 standalone 目錄,否則頁面會缺圖片與樣式。第三,回滾:平台有「把舊版重新設為正式」的一鍵回滾,自架則要自己保留上一版的目錄,出事時切回去再重啟。若團隊熟悉容器,也可以用容器映像打包,但本系列以不依賴容器為前提,先示範最少的系統需求。
Runbook 是自架路線的必備說明。把常見狀況與處理步驟寫成一份清單放在專案裡,值班者照著做即可,不必臨場翻找指令:
# RUNBOOK:值班者處理線上問題的步驟
# 一、部署失敗
# 1. 平台:Deployments -> 點最新一筆看建置紀錄
# 2. 自架:看 systemctl status booking-frontend 與 journalctl 輸出
# 3. 常見原因:lockfile 不同步、型別錯誤、找不到模組
# 4. 在本機先跑 pnpm typecheck 與 pnpm build 重現,修正後再上版
# 二、線上 5xx 暴增
# 1. 錯誤追蹤後台:找最熱門的錯誤
# 2. 平台:Logs -> Functions,找 5xx 最多的路徑
# 3. 緊急處置:把上一個成功的版本重新設為正式(或切回上一版目錄)
# 三、登入流程異常
# 1. 錯誤追蹤後台篩選 auth 標籤,看 /auth/refresh 是否大量 401
# 2. curl -fsS https://api.hao-code.com/health 確認後端健康
# 3. 後端問題轉後端值班;前端問題先回滾再查
常見錯誤與踩雷
第一個踩雷是「把環境變數或金鑰寫進程式碼」。把授權權杖直接寫在設定檔並推上版控,會被秘密掃描工具抓到、金鑰被撤銷、所有部署一起壞。修法:所有敏感值放平台的環境設定或版本控制平台的 Secrets,程式碼只讀 process.env。如果不小心推上去了,立刻撤銷並重新簽發,不要只是刪掉那次提交。
第二個踩雷是「預覽環境連到正式後端」。如果預覽環境也用正式的 API 網址,測試人員在預覽網址按送出就會真的寫入正式資料。修法:預覽環境的 API 網址指向測試後端,今天的環境變數設定就是這樣區分的。
第三個踩雷是「錯誤追蹤沒收到事件」。常見原因有四個:DSN 貼錯、初始化沒被載入、上傳事件的過濾函式把所有事件都濾掉、以及原始碼對照檔沒上傳導致堆疊看不懂。排查方式:打開瀏覽器主控台確認追蹤物件存在、手動送一則測試訊息看後台是否出現、確認過濾函式沒有回傳空值、以及建置紀錄裡有「上傳原始碼對照檔」的訊息。
第四個踩雷是「部署成功但頁面 500」。這通常是 Server Component 在建置時正常、執行時卻壞了,例如請求的網址少了 base URL、依賴一個正式環境沒有的變數、或中介層把所有請求都擋掉。排查方式:到平台的函式紀錄找堆疊、確認正式環境的變數都有值、暫時停用中介層看是不是它造成的。
第五個踩雷是「自訂網域的憑證沒生效」。通常是 DNS 還沒傳播,或記錄設定不完整。排查方式:用 dig 查詢網域解析結果、到平台或反向代理看網域狀態是否顯示設定正確。DNS 一般幾分鐘內生效,跨地區傳播最久可能要一天。
效能與實務提醒
部署頻率是團隊健康的指標。建議每個 PR 都觸發預覽環境、每天至少合併一到三個 PR 到主分支,讓每次正式變更都小、回滾容易、問題好定位。相對地,累積一個月才合併一次,每次上線都是高風險事件,出事時也不知道是哪個變更造成的。
環境變數的管理要有紀律。把「環境變數清單」寫成一份說明放在專案裡(不是程式碼),列出每個變數的用途、適用環境、以及新成員該去哪裡取得,每次新增變數都順手更新,程式碼審查時一併檢查。這條慣例在 Day 43 的技術手冊與交接會再強調。
部署後的巡檢要持續。真實使用者效能數據、錯誤事件、伺服器端紀錄三者合在一起,才能完整看見系統狀態。建議每週看一次後台:確認效能指標沒有突然變差、5xx 比率低於百分之一、錯誤追蹤沒有新的嚴重問題。回滾演練也要做過一次,確認「把上一版設為正式」真的能在幾分鐘內完成,而不是出事當天才第一次操作。
最後提醒「部署不等於結束」。上線後要驗收:打開正式網址確認頁面正常、用 admin 帳號登入確認流程通、送出一筆測試預約確認後端運作、看效能後台確認資料開始收集。把這些步驟寫成清單,逐項確認後才算部署完成。
小結
今天把預約管理系統前端正式推上線。我們建立了 Vercel 專案、設定三層環境變數、把環境變數驗證寫進程式、加了安全標頭、串好 GitHub Actions 的持續整合、啟用效能量測與錯誤追蹤、設定自訂網域與健康檢查端點,並補上一條不依賴平台的自架路線(standalone 輸出、systemd 服務、Caddy 反向代理)與一份 Runbook。整套設定沿用 Day 31–40 的技術堆疊,沒有引入新的設計概念,只增加營運相關的設定。預期效益是:合併 PR 就有獨立預覽可驗證、錯誤自動被收集、效能自動被量測、出事能在幾分鐘內回滾。
結語
部署是把「寫好的程式」變成「別人用得上的系統」的最後一哩路,也是 side project 與正式產品之間最明顯的分界。今天我們把 Vercel 與自架兩條路線都走過一次,你可以依團隊的資安與維運條件選擇。明天 Day 42 我們會在正式環境上做無障礙與響應式的總檢:把 11 個頁面用自動化工具掃描、走一遍鍵盤導航、用多種螢幕寬度截圖確認版面、並確認系統尊重減少動態的偏好設定。讀完這篇你應該能回答:環境變數為什麼要分三層?哪些變數不能放前端?預覽環境為什麼要指向測試後端?自架路線要自己負責哪三件事?
延伸資源
- Vercel 部署官方文件:
https://vercel.com/docs,環境變數、預覽部署與自訂網域的標準做法。 - Next.js 自架與 standalone 輸出:
https://nextjs.org/docs/app/getting-started/deploying,自架部署的官方說明。 - GitHub Actions 官方文件:
https://docs.github.com/en/actions,workflow 語法、矩陣測試與 Secrets 管理。 - Sentry Next.js 整合說明:
https://docs.sentry.io/platforms/javascript/guides/nextjs/,原始碼對照檔上傳與效能監控。 - Caddy 反向代理與自動 HTTPS:
https://caddyserver.com/docs/,自架路線的 TLS 自動化。
留言
張貼留言