Next.js 生产环境部署怎么选,Vercel、Docker 和自托管差在哪?
Next.js 部署到生产环境,真正难的不是敲哪条命令,而是先判断项目需要哪种运行时。只要页面里有 SSR、Route Handler、Server Action、ISR、默认图片优化或读 cookies,就不能把它当成普通静态站扔到 CDN 上完事;如果只是文档、营销页、博客归档,静态导出反而更省钱、更稳定。
下面按生产环境里最常见的几条路来选:Vercel 适合想少管运维的团队;Docker standalone 适合自托管和云容器;PM2 + Nginx 适合传统服务器;output: 'export' 适合纯静态站。选对路线,比后面补十个配置都重要。
先按功能选择部署方式
| 部署方式 | 适合项目 | Next.js 功能支持 | 主要代价 |
|---|---|---|---|
| Vercel | App Router、SSR、API、预览环境都要省心 | 支持最好 | 成本和平台绑定更明显 |
| Node.js 服务 | 普通服务器、PaaS、需要完整 Next.js 能力 | 完整支持 | 要自己管进程、日志、发布回滚 |
| Docker standalone | Kubernetes、Cloud Run、ECS、Fly.io、Render、自建机器 | 完整支持 | 要维护镜像、环境变量和健康检查 |
| 静态导出 | 静态页面、SPA、文档站、营销页 | 有限制 | 不支持依赖服务器的特性 |
| 平台适配器 | Cloudflare、Netlify、AWS Amplify 等 | 取决于平台 | ISR、图片、Edge、缓存行为要逐项确认 |
一句话:需要服务端能力就选 Vercel、Node.js 或 Docker;不需要服务端能力才选静态导出。
Vercel:最省心的默认选择
Vercel 是 Next.js 官方团队维护的平台,GitHub 仓库一连,通常就能自动识别框架、安装依赖、构建并发布。它的价值不只是“能部署”,而是把预览环境、HTTPS、CDN、Serverless/Edge Runtime、环境变量、回滚这些杂事都收在一个流程里。
最小化的项目脚本一般这样写:
json{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start" } }
CLI 部署也很直接:
bashnpm i -g vercel vercel login vercel vercel --prod
大多数项目不需要手写 vercel.json。只有在你要改构建命令、设置安全响应头、调整函数超时或指定特殊路由行为时,才建议加配置。例如:
json{ "framework": "nextjs", "buildCommand": "npm run build", "headers": [ { "source": "/(.*)", "headers": [ { "key": "X-Content-Type-Options", "value": "nosniff" }, { "key": "X-Frame-Options", "value": "DENY" } ] } ] }
上 Vercel 前要重点检查两件事。第一,生产环境变量和预览环境变量要分开,不要把测试库地址带到 Production。第二,项目如果依赖特定区域的数据库,函数区域、数据库区域和用户主要访问区域要尽量靠近,否则页面渲染很快,数据库往返却把 TTFB 拖慢。
Docker standalone:自托管最推荐的打包方式
如果你要部署到自己的服务器、Kubernetes、Google Cloud Run、AWS ECS、Fly.io 或 Render,优先用 Next.js 的 standalone output。它会在构建时追踪运行所需文件,把最小运行集输出到 .next/standalone,镜像会比“把整个项目和 node_modules 全塞进去”干净很多。
先在 next.config.js 里启用:
js/** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone' } module.exports = nextConfig
再用多阶段 Dockerfile:
dockerfileFROM node:22-alpine AS base WORKDIR /app FROM base AS deps COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder COPY /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME=0.0.0.0 RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nextjs COPY /app/public ./public COPY /app/.next/standalone ./ COPY /app/.next/static ./.next/static USER nextjs EXPOSE 3000 CMD ["node", "server.js"]
这里有个容易踩的坑:.next/standalone 默认不会自动带上 public 和 .next/static。如果这些资源不交给 CDN,就要像上面的 Dockerfile 一样手动复制,否则页面能打开,静态资源却可能 404。
如果是 monorepo,Next.js 默认只从项目目录追踪文件。服务端代码如果读取了工作区上层的共享文件,要配置 outputFileTracingRoot,否则本地构建正常,容器里可能找不到文件:
jsconst path = require('path') module.exports = { output: 'standalone', outputFileTracingRoot: path.join(__dirname, '../../') }
用 Docker Compose 跑单机也可以:
yamlservices: web: build: . ports: - "3000:3000" environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} restart: unless-stopped
PM2 + Nginx:传统服务器仍然可用
如果项目部署在一台固定 Linux 服务器上,PM2 负责守护 Node 进程,Nginx 负责 HTTPS、反向代理、静态缓存和访问日志,是一套很常见的组合。
不用 standalone 时,可以直接跑 next start:
js// ecosystem.config.js module.exports = { apps: [ { name: 'nextjs-app', script: 'node_modules/next/dist/bin/next', args: 'start -p 3000', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000 }, error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' } ] }
常用命令:
bashnpm ci npm run build pm2 start ecosystem.config.js pm2 status pm2 logs nextjs-app pm2 reload nextjs-app
Nginx 反向代理可以这样写:
nginxserver { listen 80; server_name example.com; location /_next/static/ { proxy_pass http://127.0.0.1:3000; add_header Cache-Control "public, max-age=31536000, immutable"; } location / { proxy_pass http://127.0.0.1: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; } }
生产环境还要补 HTTPS、日志轮转、磁盘告警和回滚策略。PM2 能把进程拉起来,但它不等于完整发布系统;构建产物、环境变量、数据库迁移和回滚包仍然要自己管。
静态导出:只适合不依赖服务器的页面
纯静态站可以用 output: 'export'。从 Next.js 14 开始,老的 next export 已移除,应该通过配置启用静态导出,然后运行 next build。
js/** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', images: { unoptimized: true } } module.exports = nextConfig
构建后会生成 out/ 目录,可以放到 GitHub Pages、AWS S3 + CloudFront、Cloudflare Pages、Netlify、Firebase Hosting 或普通 Nginx 静态目录。
但静态导出不是“免费获得所有 Next.js 能力”。下面这些能力不适合静态导出:
- 运行时 SSR
- 依赖请求对象的 Route Handler
- cookies、headers、rewrites、redirects
- ISR 和按需重新验证
- 默认
next/image图片优化 - Server Actions
- 未通过
generateStaticParams()固定好的动态路由
如果项目现在是静态站,但半年后可能要登录态、支付回调、后台预览或个性化渲染,最好一开始就评估迁移成本。静态导出很稳,但边界也很硬。
云平台怎么选
云平台的选择通常不是技术优劣,而是谁来承担运维成本。
- AWS Amplify Hosting:适合已经在 AWS 上的团队,Next.js 支持较完整,但要留意 App Router、SSR、图片优化和缓存能力的版本说明。
- Google Cloud Run:很适合 Docker standalone,按容器扩缩容,发布模型清晰。
- AWS ECS / Kubernetes:适合已有容器平台的公司,能力强,但发布、扩缩容、日志和监控都要工程化。
- Fly.io / Render / Railway:比自建机器省心,适合中小项目快速上线。
- Cloudflare / Netlify:可以跑部分 Next.js 能力,但依赖各自适配器;用到 ISR、Edge Runtime、图片优化、Streaming SSR 时要先做验证。
- Azure Static Web Apps:更适合静态导出或前后端分离形态。
选择云平台时,不要只看“能不能部署成功”。要拿你的真实页面测一次:动态路由、API、图片、缓存、预览环境、环境变量、日志、回滚,这些全过了才算生产可用。
CI/CD:先验证,再发布
CI/CD 的底线是:依赖安装、类型检查、Lint、测试、构建必须先过,部署只是最后一步。GitHub Actions 可以从这个版本开始:
yamlname: build-and-deploy on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - run: npm run lint --if-present - run: npm test --if-present - run: npm run build
如果部署到 Vercel,可以在构建验证后增加 Vercel CLI 的预构建部署;如果部署 Docker,则在构建后推镜像,再由平台滚动更新。无论哪条路,都不要把密钥写在 YAML 里,统一放到 GitHub Secrets 或平台自己的 secret manager。
环境变量:区分构建时和运行时
Next.js 里环境变量最容易混的地方是 NEXT_PUBLIC_。带这个前缀的变量会被打进浏览器 bundle,适合公开的 API base URL、埋点 ID,不适合数据库密码、JWT secret、第三方私钥。
常见做法:
bash# .env.local:本地开发,不提交 DATABASE_URL=postgresql://localhost/app NEXT_PUBLIC_SITE_URL=http://localhost:3000 # 生产环境:放到平台环境变量或 secret manager DATABASE_URL=postgresql://prod/app NEXT_PUBLIC_SITE_URL=https://example.com
Docker 场景还要注意:如果变量在 next build 时就被读取,构建镜像时必须提供;如果只在服务端运行时读取,可以在容器启动时注入。两类变量混在一起,会出现“本地没问题,生产配置没生效”的怪问题。
性能配置:别再写 swcMinify
老文章里常见的 swcMinify: true 已经不适合新项目。Next.js 13 开始 SWC minify 默认开启;Next.js 15 之后继续把 swcMinify 留在 next.config.js 里,可能会出现 Unrecognized key(s) in object: 'swcMinify' 警告。现在应该直接删掉这项,而不是继续复制旧配置。
更值得保留的是这些配置和检查:
js/** @type {import('next').NextConfig} */ const nextConfig = { poweredByHeader: false, productionBrowserSourceMaps: false, images: { formats: ['image/avif', 'image/webp'] } } module.exports = nextConfig
还可以做几件更实际的事:
- 用
@next/bundle-analyzer定期看 bundle,别让后台图表库跑进首页。 - 首屏图片用
next/image并设置合理的priority,不要所有图片都抢优先级。 _next/static走长期缓存,HTML 和接口按业务设置缓存。- Node 自托管时可以开
compress,但如果 Nginx 或 CDN 已经负责 gzip/br,就不要重复压缩。 - 数据库和服务端渲染区域尽量靠近,很多慢页面不是 JS 慢,而是跨区域查询慢。
监控和日志:上线后才知道真问题
生产部署至少要能回答三个问题:现在挂没挂、哪里慢、用户报错在哪里。
Vercel 项目可以接 Vercel Analytics 和 Speed Insights;自托管项目可以接 Sentry、OpenTelemetry、Prometheus + Grafana 或云厂商 APM。错误监控建议在服务端和浏览器端都接入,尤其是 Route Handler、Server Actions、支付回调这类不容易从页面发现的问题。
一个简单的健康检查接口也很有用:
ts// app/api/health/route.ts export async function GET() { return Response.json({ ok: true, ts: Date.now() }) }
容器平台可以用它做 readiness/liveness probe。真正的生产检查还应包含数据库、缓存、对象存储等依赖,否则“应用进程活着”和“业务可用”不是一回事。
上线前检查清单
npm run build在干净环境里能通过,不依赖本机缓存。- Node.js 版本与 Next.js 当前版本要求一致,CI、Docker、服务器不要各用各的版本。
- 生产环境变量已配置,且没有把 secret 打进客户端 bundle。
- SSR、API、ISR、图片优化、动态路由在目标平台逐项验证过。
- Docker standalone 已复制
public和.next/static,monorepo 已检查文件追踪范围。 - Nginx/CDN 已配置 HTTPS、缓存、请求体大小、代理头和日志。
- CI/CD 有测试、构建、回滚和密钥管理,不只是一条 deploy 命令。
- Sentry/APM/日志/健康检查已接入,告警能找到负责人。
- 静态导出项目确认没有使用 cookies、Server Actions、运行时 API 等服务器特性。
Next.js 生产部署没有唯一答案。小团队想省心,Vercel 是最短路径;有容器基础设施,standalone Docker 更容易标准化;只是静态页面,就别硬上 Node 服务。先让部署方式匹配项目功能,再谈优化和自动化,后面的坑会少很多。