Next.js 部署與 Runtime

Next.js 部署完整指南 — Edge vs Node Runtime、Vercel、Docker standalone、env vars、Logging、監控


目錄


部署模型總覽

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 才決定的值,用以下做法:

  1. 把值放 cookie / headers(從 server 端注入)
  2. 開個 /api/config route 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-compose environment 區塊
  • 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 --from=deps /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 --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /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 → 客戶端 IP
  • X-Forwarded-Protohttp / https
  • X-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 logskubectl 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.log JSON 字串
  • 用 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.ts
  • sentry.server.config.ts
  • sentry.edge.config.ts
  • next.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 --from=deps /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 --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /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/ 沒複製

解決方案

  1. next.config.ts 確認有 output: "standalone"
  2. 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

解決方案

  1. 改用 Prisma Accelerate / Data Proxy(HTTP)
  2. 換 Neon / Supabase REST 等 Edge-friendly DB
  3. 把 DB 操作改放 Node runtime 的 Route Handler

問題 5:build 時環境變數沒被替換

症狀process.env.NEXT_PUBLIC_X 在 build 後是 undefined

檢查

  1. 變數名是否以 NEXT_PUBLIC_ 開頭
  2. .env* 檔案是否被 Docker 複製進去(很多 .dockerignore 會排除)
  3. CI 環境是否設定該變數

總結

核心要點

部署 = 選對 runtime(Edge/Node)+ 正確設環境變數 + 配 reverse proxy + 加監控

部署檢查清單

  • next.config.tsoutput: "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 生態

相關閱讀


參考資源


建立日期:2026-05-16 最後更新:2026-05-16

🔗相關文章