部署指南
Next.js 与 TanStack Start 应用的部署策略与最佳实践
自托管部署对比
在企业环境中,自托管(K8s + Helm)是最常见的部署方式。两种框架在自托管场景下有显著差异:
部署模式对比
| 维度 | Next.js | TanStack Start (Nitro) |
|---|---|---|
| 架构 | Hybrid Standalone Server | Vite + Nitro(可分离部署) |
| Node Server | ✅ 支持 | ✅ 支持 |
| Static + Server 分离 | ❌ 不支持 | ✅ 支持 |
| 纯静态导出 | ⚠️ 支持但功能受限 | ✅ SPA 模式 |
| 静态资源性能 | 🔴 所有请求经过 Node | 🟢 可直接 CDN |
| 动态路由 | ✅ 原生支持 | ✅ 原生支持 |
| ISR | ✅ 支持 | ❌ 不支持 |
| 部署复杂度 | 🟢 简单(单容器) | 🟡 中等(可选分离) |
Next.js 部署局限性
Standalone 模式性能问题
Next.js output: "standalone" 生成的是一个 Hybrid Server,所有请求(包括静态资源)都经过 Node.js 处理:
- 📉 静态页面多时性能下降明显
- 🔄 无法将静态资源直接部署到 CDN
- 💾 内存占用较高
Static Export 不支持的特性(官方文档):
| 不支持的特性 | 说明 |
|---|---|
动态路由(无 generateStaticParams) | 必须预先生成所有路径 |
dynamicParams = true | 无法运行时生成页面 |
| Route Handlers(非 GET) | POST/PUT/DELETE 等不可用 |
| Cookies / Headers | 服务端 API 不可用 |
| Rewrites / Redirects | 需在 CDN/Nginx 层配置 |
| Middleware | 完全不可用 |
| ISR | 增量静态再生成不可用 |
| Image Optimization | 需自定义 loader |
| Draft Mode | 预览模式不可用 |
TanStack Start (Nitro) 部署优势
TanStack Start 基于 Nitro,支持灵活的部署策略:
| 部署模式 | 说明 | 适用场景 |
|---|---|---|
| Node Server | 完整服务端渲染 | 需要 SSR 的应用 |
| Static + Node 分离 | 静态资源 CDN + API 到 Node | 🌟 推荐:高性能 + 完整功能 |
| SPA 模式 | 纯客户端渲染 | 后台管理系统 |
| Hybrid Presets | 按平台优化 | Cloudflare、Vercel 等 |
分离部署架构:
┌─────────────┐ ┌─────────────┐
│ CDN/OSS │ │ K8s Pod │
│ (静态资源) │ │ (Node API) │
│ │ │ │
│ .output/ │ │ .output/ │
│ public/ │ │ server/ │
└─────────────┘ └─────────────┘
↑ ↑
└───────┬───────────┘
│
┌─────┴─────┐
│ Ingress │
│ /api/* │ → Node
│ /* │ → CDN
└───────────┘Docker 容器化(自托管)
Next.js Dockerfile
必须配置:在 next.config.js 中开启 output: "standalone"
# syntax=docker/dockerfile:1
FROM node:24-alpine AS base
# 安装 Bun
RUN apk add --no-cache bash curl
ENV BUN_INSTALL=/usr/local/bun
RUN curl -fsSL https://bun.com/install | bash
ENV PATH="${BUN_INSTALL}/bin:${PATH}"
# 依赖安装
FROM base AS deps
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun i --frozen-lockfile
# 构建
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run build
# 运行
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
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 HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]TanStack Start Dockerfile
方案 A:完整 Node Server(简单)
# syntax=docker/dockerfile:1
FROM node:24-alpine AS base
# 安装 Bun
RUN apk add --no-cache bash curl
ENV BUN_INSTALL=/usr/local/bun
RUN curl -fsSL https://bun.com/install | bash
ENV PATH="${BUN_INSTALL}/bin:${PATH}"
# 依赖安装
FROM base AS deps
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun i --frozen-lockfile
# 构建
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN bun run build
# 运行
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
COPY --from=builder --chown=appuser:nodejs /app/.output ./
USER appuser
EXPOSE 3000
ENV PORT=3000 HOST="0.0.0.0"
CMD ["node", ".output/server/index.mjs"]方案 B:静态资源分离(推荐)
# 仅构建 Server 部分
FROM node:24-alpine AS base
# ... 同上 ...
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# 只复制 server 目录
COPY --from=builder --chown=appuser:nodejs /app/.output/server ./
USER appuser
EXPOSE 3000
CMD ["node", "index.mjs"].output/
├── public/ # → 上传到 CDN/OSS
│ ├── _build/
│ └── assets/
└── server/ # → 打包到 Docker 镜像
└── index.mjsHelm Chart 配置示例
replicaCount: 3
image:
repository: registry.example.com/my-app
tag: latest
pullPolicy: Always
service:
type: ClusterIP
port: 3000
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
hosts:
- host: app.example.com
paths:
- path: /
pathType: Prefix
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
env:
- name: NODE_ENV
value: production
- name: API_URL
valueFrom:
configMapKeyRef:
name: app-config
key: api-url
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5CI/CD Pipeline(GitLab CI 示例)
stages:
- test
- build
- deploy
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
test:
stage: test
image: oven/bun:latest
script:
- bun install --frozen-lockfile
- bun run lint
- bun run test
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- helm upgrade --install my-app ./helm
--set image.tag=$CI_COMMIT_SHA
--namespace production
only:
- main云平台部署
环境变量管理
安全原则
| 类型 | 存放位置 | 示例 |
|---|---|---|
| 🔓 公开配置 | 代码仓库 .env.example | NEXT_PUBLIC_API_URL |
| 🔒 敏感凭证 | K8s Secrets / Vault | DATABASE_URL, API_KEY |
| 🔐 运行时密钥 | HashiCorp Vault / KMS | 加密密钥、JWT Secret |
K8s ConfigMap & Secret
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
API_URL: "https://api.example.com"
APP_NAME: "MyApp"apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
DATABASE_URL: "postgresql://..."
JWT_SECRET: "your-secret-key"🚨 永远不要将 Secret YAML 提交到 Git 仓库!使用 Sealed Secrets 或外部密钥管理。
监控与可观测性
| 类型 | 工具 | 说明 |
|---|---|---|
| 📊 APM | Sentry | 错误追踪、性能监控 |
| 📈 指标 | Prometheus + Grafana | K8s 原生监控 |
| 📝 日志 | ELK / Loki | 集中式日志管理 |
| 🔍 追踪 | Jaeger / OpenTelemetry | 分布式追踪 |
健康检查端点
export async function GET() {
return Response.json({ status: "ok", timestamp: Date.now() })
}import { createAPIFileRoute } from "@tanstack/react-start/api"
export const APIRoute = createAPIFileRoute("/api/health")({
GET: () => Response.json({ status: "ok", timestamp: Date.now() }),
})Checklist
- 选择适合团队的部署架构(单容器 / 静态分离)
- 配置 CI/CD 自动化流程(GitLab CI / GitHub Actions)
- Helm Chart 配置完成并测试
- K8s ConfigMap & Secret 配置正确
- 健康检查端点
/health就绪 - Liveness / Readiness Probe 配置
- 资源限制(CPU/Memory)合理设置
- 日志采集与监控告警配置
- 回滚策略文档化