目錄
- 部署模型總覽
- Edge vs Node Runtime
- 環境變數規則
- Vercel 部署
- 自架 Docker(standalone)
- Reverse Proxy 設定
- Logging 與監控
- Instrumentation Hook
- 錯誤追蹤(Sentry / OpenTelemetry)
- 實戰範例
- 常見問題
- 總結
- 參考資源
部署模型總覽
Next.js 應用可以部署成四種形式:
| 模型 | 特性 | 適合 |
|---|---|---|
| 完全靜態(SSG) | output: "export" 產出 HTML/JS,可放 S3 / GitHub Pages |
純內容站、文件站 |
| Vercel(managed) | 自動 Edge + ISR + Image Optimizer,zero-config | 99% 使用情境 |
| Docker standalone | output: "standalone",獨立可執行包 |
私有雲、Kubernetes |
| 傳統 Node server | next start 直接跑在 Node 上 |
簡單伺服器、客製 server |
Edge vs Node Runtime
兩種 Runtime 對比
| 面向 | Edge Runtime | Node.js Runtime |
|---|---|---|
| 執行環境 | V8 isolate(類似 Cloudflare Workers) | 完整 Node.js |
| 冷啟動 | 幾乎沒有 | 較慢(serverless 場景) |
| API 集合 | Web Standard API only | 完整 Node API |
| Bundle 限制 | 通常 < 1MB | 無嚴格限制 |
| 執行時間 | 較短(10-30s) | 較長 |
| 部署位置 | 全球邊緣節點 | 區域資料中心 |
何時用 Edge
✅ 適合 Edge:
- 輕量的 API(< 1MB bundle)
- 個人化(讀 cookies、地理位置)
- A/B test
- 動態 OG image(next/og)
- Middleware
❌ 不適合 Edge:
- 連傳統 DB(PostgreSQL TCP)
- 需要 Node 套件(如
fs、Sharp) - 重運算或長時間任務
設定 Runtime
Page / Route Handler
// app/api/hello/route.ts
export const runtime = "edge"; // 或 "nodejs"(預設)
export async function GET() {
return new Response("Hello");
}
Middleware
middleware.ts 目前只能在 Edge,無法切換。
全域預設
// next.config.ts
const config = {
// experimental.runtime 已移除,請在各檔案個別設定
};
Edge 上常見問題
// ❌ Edge 不支援
import fs from "fs"; // Node 專屬
import path from "path";
import sharp from "sharp"; // 含 native binding
// ✅ Edge 支援
const data = await fetch("https://api.example.com").then((r) => r.json());
const encrypted = await globalThis.crypto.subtle.encrypt(...); // Web Crypto
環境變數規則
三種環境變數來源
| 來源 | 用途 |
|---|---|
.env |
預設值(commit 進 repo) |
.env.local |
本地覆寫(不要 commit) |
.env.development / .env.production |
環境特化 |
NEXT_PUBLIC_ 規則
關鍵:只有以 NEXT_PUBLIC_ 為前綴的環境變數會被打包進 client bundle,其他只有 server 拿得到。
# .env.local
DATABASE_URL=postgres://... # 只在 server 可用
JWT_SECRET=xxx # 只在 server 可用
NEXT_PUBLIC_SITE_URL=https://example.com # client 與 server 都可用
NEXT_PUBLIC_GA_ID=G-XXXXXX # client 與 server 都可用
// Server Component / Server Action / Route Handler
const dbUrl = process.env.DATABASE_URL; // ✅
const jwtSecret = process.env.JWT_SECRET; // ✅
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL; // ✅
// Client Component
"use client";
const dbUrl = process.env.DATABASE_URL; // ❌ undefined
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL; // ✅
Build 時 vs Runtime
NEXT_PUBLIC_* 在 build 時就被靜態替換進 bundle。意思是:
- ❌ 不能在 build 後改
NEXT_PUBLIC_API_URL就期望生效 - ✅ 改了得重新 build
如果需要 runtime 才決定的值,用以下做法:
- 把值放 cookie / headers(從 server 端注入)
- 開個
/api/configroute handler 由 client fetch
環境變數驗證
// src/env.ts — 啟動時驗證所有環境變數
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NEXT_PUBLIC_SITE_URL: z.string().url(),
});
export const env = envSchema.parse(process.env);
// 啟動時若缺值會立刻噴錯(早發現比 runtime 噴錯好)
Vercel / 平台環境變數
各平台都有環境變數 UI:
- Vercel:Project Settings → Environment Variables,支援 Production / Preview / Development 三組
- Docker:透過
docker run -e KEY=VALUE或 docker-composeenvironment區塊 - Kubernetes:用 Secret + ConfigMap
Vercel 部署
Zero-config 部署
# 1. 推到 GitHub / GitLab / Bitbucket
git push origin main
# 2. 到 Vercel 連接 repo
# https://vercel.com/new
# 3. Vercel 自動偵測 Next.js → 自動 build + deploy
Vercel 內建特性
| 特性 | 自動支援 |
|---|---|
| Edge Functions | Middleware + runtime: "edge" 自動部署到邊緣 |
| Serverless Functions | Route Handlers / SSR pages 自動轉 |
| Image Optimization | next/image 自動用 Vercel CDN 處理 |
| ISR | revalidate 自動配 Vercel KV |
| Preview Deployment | 每個 PR / branch 自動有獨立 URL |
| Edge Config | runtime 可讀的 KV,毫秒級全球同步 |
vercel.json 客製化
{
"framework": "nextjs",
"regions": ["sin1", "hnd1"],
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store" }
]
}
],
"rewrites": [
{ "source": "/old-blog/:slug", "destination": "/blog/:slug" }
]
}
自訂 Domain
Vercel UI → Domains → 輸入域名 → 加 DNS A / CNAME。
自架 Docker(standalone)
啟用 standalone 模式
// next.config.ts
const config = {
output: "standalone",
};
export default config;
build 後產生 .next/standalone/ 包含最小可執行的 server(含必要 node_modules)。
Dockerfile 範本
# 階段 1:依賴
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 階段 2:build
FROM node:20-alpine AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 階段 3:執行
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# 建立非 root user 增加安全
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
Build & Run
docker build -t my-next-app .
docker run -p 3000:3000 -e DATABASE_URL=... my-next-app
重要:static 與 public 必須複製
output: "standalone" 產出的 server.js 不包含:
public/(靜態檔案).next/static/(hashed bundle)
Dockerfile 必須手動複製這兩個資料夾,否則 client 抓不到 CSS / 圖片。
Reverse Proxy 設定
為什麼需要
正式環境通常會把 Next.js 放在 Nginx / Caddy / Traefik 後面:
- TLS 終端
- HTTP/2 / HTTP/3
- 多服務統一入口
- WAF / DDoS 防護
Nginx 範本
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Next.js static 可在 nginx 層快取
location /_next/static/ {
proxy_pass http://localhost:3000;
proxy_cache_valid 200 60m;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
注意 X-Forwarded-* headers
Next.js 用這些 header 推斷真實 client IP 與 protocol:
X-Forwarded-For→ 客戶端 IPX-Forwarded-Proto→http/httpsX-Forwarded-Host→ 原始 host
確保你的 reverse proxy 有設定這些,否則 request.headers 拿到的會是 proxy 的位址。
Logging 與監控
Built-in console
Next.js 預設 logging 走 console.log / console.error:
// Server Component / Route Handler / Server Action
console.log("[server]", data);
console.error("[server]", error);
各平台收集方式:
- Vercel:自動進 Functions → Runtime Logs(保留 1 小時)
- Docker:寫到 stdout / stderr,搭配
docker logs或kubectl logs
結構化 logging
// lib/logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
formatters: {
level: (label) => ({ level: label }),
},
// 環境差異
transport:
process.env.NODE_ENV === "development"
? { target: "pino-pretty" }
: undefined,
});
// 使用
import { logger } from "@/lib/logger";
logger.info({ userId: "123" }, "user logged in");
logger.error({ err }, "failed to fetch posts");
Edge Runtime 的 logging
Edge 環境某些 log 套件不能用(依賴 Node API)。常見替代:
- 用
console.logJSON 字串 - 用 platform-native logger(Vercel
@vercel/edge-config也記事件)
Instrumentation Hook
Next.js 提供 instrumentation.ts 在伺服器啟動時執行 hooks,常用於初始化 telemetry。
啟用
// next.config.ts
const config = {
experimental: {
instrumentationHook: true, // Next.js 15 之前需要
},
};
Next.js 15+ 預設啟用,不需要 flag。
基本結構
// instrumentation.ts (專案根目錄)
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
// 只在 Node runtime 跑(避免在 Edge 跑出問題)
await import("./instrumentation.node");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./instrumentation.edge");
}
}
// 處理未捕捉的 request 錯誤
export async function onRequestError(
err: Error,
request: {
path: string;
method: string;
headers: Record<string, string>;
}
) {
// 上報到錯誤追蹤平台
console.error("[request error]", err, request.path);
}
錯誤追蹤(Sentry / OpenTelemetry)
Sentry
npm install @sentry/nextjs
npx @sentry/wizard@latest -i nextjs
Wizard 會自動建立:
sentry.client.config.tssentry.server.config.tssentry.edge.config.tsnext.config.ts包裝
手動上報
import * as Sentry from "@sentry/nextjs";
try {
await riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: { feature: "checkout" },
user: { id: userId },
});
throw error;
}
OpenTelemetry
npm install @vercel/otel @opentelemetry/api
// instrumentation.ts
import { registerOTel } from "@vercel/otel";
export function register() {
registerOTel({ serviceName: "my-app" });
}
之後可送到任何 OTel-compatible backend(Datadog、Honeycomb、Grafana Tempo)。
實戰範例
範例 1:完整 Docker 部署套組
# Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S nodejs -g 1001 && adduser -S nextjs -u 1001
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV HOSTNAME=0.0.0.0 PORT=3000
CMD ["node", "server.js"]
# docker-compose.yml
services:
app:
build:
context: .
args:
NEXT_PUBLIC_SITE_URL: https://example.com
ports:
- "3000:3000"
environment:
DATABASE_URL: ${DATABASE_URL}
JWT_SECRET: ${JWT_SECRET}
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:3000/api/health"]
interval: 30s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/letsencrypt:ro
depends_on:
- app
// app/api/health/route.ts
export async function GET() {
return new Response("ok", { status: 200 });
}
範例 2:Sentry 整合 + Instrumentation
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}
export async function onRequestError(err: Error, request: { path: string }) {
const Sentry = await import("@sentry/nextjs");
Sentry.captureException(err, { extra: { path: request.path } });
}
// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
environment: process.env.NODE_ENV,
});
範例 3:環境變數驗證
// src/env.ts
import { z } from "zod";
const envSchema = z.object({
// Server-only
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
SENTRY_DSN: z.string().url().optional(),
// Public
NEXT_PUBLIC_SITE_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
});
export const env = envSchema.parse(process.env);
// 在 app/layout.tsx 開頭 import 強制驗證
import "@/env";
// 任何頁面就能型別安全地用
import { env } from "@/env";
console.log(env.NEXT_PUBLIC_SITE_URL); // TypeScript 知道是 string
常見問題
問題 1:Docker build 後啟動報 Cannot find module
原因:output: "standalone" 沒設定,或 public/ / .next/static/ 沒複製
解決方案:
next.config.ts確認有output: "standalone"- Dockerfile 確認有複製
public/與.next/static/
問題 2:NEXT_PUBLIC_API_URL 改了但 client 還是舊值
原因:NEXT_PUBLIC_* 是 build-time 注入,不是 runtime
解決方案:
- 重新 build
- 或改用 runtime 注入(route handler 回傳 config,client 動態 fetch)
問題 3:Vercel 部署後 cookies / IP 拿到的不對
原因:通常是 reverse proxy 沒設正確 headers,但 Vercel 自家不會有這問題;自架時要注意
解決方案:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
問題 4:Edge Runtime 使用 Prisma 失敗
症狀:Cannot find module 'fs' 或類似 Node API 錯誤
原因:傳統 Prisma 走 Node-only binary
解決方案:
- 改用 Prisma Accelerate / Data Proxy(HTTP)
- 換 Neon / Supabase REST 等 Edge-friendly DB
- 把 DB 操作改放 Node runtime 的 Route Handler
問題 5:build 時環境變數沒被替換
症狀:process.env.NEXT_PUBLIC_X 在 build 後是 undefined
檢查:
- 變數名是否以
NEXT_PUBLIC_開頭 .env*檔案是否被 Docker 複製進去(很多.dockerignore會排除)- CI 環境是否設定該變數
總結
核心要點
部署 = 選對 runtime(Edge/Node)+ 正確設環境變數 + 配 reverse proxy + 加監控
部署檢查清單:
-
next.config.ts設output: "standalone"(自架)或不動(Vercel) - 環境變數分清
NEXT_PUBLIC_vs server-only - 用 zod 驗證環境變數
- Middleware / OG image 跑在 Edge(更快)
- DB 連線跑在 Node runtime
- Reverse proxy 設好
X-Forwarded-*headers - 接 Sentry / Datadog 等錯誤追蹤
-
/api/health給 load balancer 探測
Runtime 選擇速查
| 場景 | Runtime |
|---|---|
| Middleware | Edge(強制) |
| 個人化 API(讀 cookies) | Edge |
| 動態 OG image | Edge |
| Prisma / PostgreSQL 直連 | Node |
| 檔案處理(sharp、ffmpeg) | Node |
| 重運算 / 長時間任務 | Node |
部署平台對比
| 平台 | 易用度 | 客製化 | 適合 |
|---|---|---|---|
| Vercel | ⭐⭐⭐⭐⭐ | ⭐⭐ | 新專案、原型、SaaS |
| Docker + 自架 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 企業內部、合規嚴格 |
| Cloudflare Pages | ⭐⭐⭐⭐ | ⭐⭐⭐ | 預算敏感、純 Edge |
| AWS Amplify | ⭐⭐⭐ | ⭐⭐⭐ | 已在 AWS 生態 |
相關閱讀
- Next.js 入門指南 — 基礎入門
- 渲染與快取 — Edge 對快取行為的影響
- Server Actions 與表單 — Edge 上的 Server Action 限制
- 進階路由 — Middleware 必跑在 Edge
- 資產與效能 — Bundle size 影響 Edge 部署
參考資源
- Vercel 部署文件:https://vercel.com/docs/frameworks/nextjs
- Self-hosting:https://nextjs.org/docs/app/getting-started/deploying
- Edge Runtime 文件:https://nextjs.org/docs/app/api-reference/edge
- Instrumentation:https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
- Sentry for Next.js:https://docs.sentry.io/platforms/javascript/guides/nextjs/
- OpenTelemetry:https://opentelemetry.io/docs/
建立日期:2026-05-16 最後更新:2026-05-16