GEOZ

Next.js部署全指南:Vercel、自托管与Docker方案对比

2026/8/5
Next.js部署全指南:Vercel、自托管与Docker方案对比

AIAI Summary (BLUF)

本文详细介绍 Next.js 应用的部署方式,包括使用 Vercel 托管和自托管方案(Node.js 服务器、Docker 容器、静态导出)。生产构建通过 next build 生成优化版本,自托管时所有功能均受支持。同时涵盖图片优化和中间件在自托管环境下的注意事项。

核心洞察

这篇部署文档最让我意外的是,官方没有逼你选 Vercel,反而把自托管的路一条条铺好了。最实用的一点是:只要用 next start 启动,所有功能都在,想用 Docker、Kubernetes 还是普通的 Node.js 服务器都没问题。静态导出才需要额外确认功能支持,别一上来就牺牲灵活性。

核心结论

  1. 使用 next start 自托管时,所有 Next.js 功能均可用;静态 HTML 导出会禁用依赖服务器的功能,但服务器组件仍支持静态导出。

  2. next/image 图片优化在 next start 下零配置可用,按需优化并默认缓存 60 分钟(可通过 images.minimumCacheTTL 调整);若将 Next.js 嵌入自定义 Node.js 服务器,需手动处理 /_next/image 请求。

  3. 自托管默认的数据缓存存于内存(上限 50MB)和磁盘 .next/cache,Kubernetes 等多实例场景各 Pod 缓存不共享,可通过自定义 cacheHandler 接入 Redis/S3 实现共享。

  4. 多容器滚动发布时,Next.js 通过部署 ID 自动处理版本偏差:客户端请求携带部署 ID,检测到不一致时强制硬导航以对齐版本,副作用是 useState 等组件内存状态会丢失。

  5. App Router 流式响应若前挂 Nginx 等反向代理,需设置 X-Accel-Buffering: no 响应头,否则代理会缓冲整个响应,导致流式失效。

部署

恭喜,现在可以交付生产了。

部署方式有几条路。想省事可以直接上 Vercel,官方托管,不用做任何配置。想自己掌控,可以跑在 Node.js 服务器上,也可以用 Docker 镜像,甚至导出成静态 HTML 文件托管到任意 Web 服务器。只要是用 next start 启动的,Next.js 的完整功能都在。

生产构建

跑一遍 next build,得到的是优化过的生产版本。每个页面会生成对应的 HTML、CSS、JavaScript 文件,JS 会被编译器编译过,浏览器端打包文件也会被压缩。这套输出考虑到了性能,也照顾了现代浏览器的兼容性。

Next.js 的构建产物是一份标准部署输出,托管和自托管用的都是同一份东西。正因为这样,不管部署到哪个环境,功能不会有差异。官方计划在下个大版本里把这份输出迁移到构建输出 API 规范。

使用 Vercel 管理 Next.js

Vercel 就是 Next.js 背后的团队,框架是他们在维护。用 Vercel 托管等于基础设施和开发者体验都有人管了,部署不用做任何配置,全球范围内的扩容、可用性和性能还有额外优化。

不过别担心自托管会缺功能,所有 Next.js 功能在自托管环境下照样支持。

想试的话,可以直接用模板部署,也可以先看看 Vercel 上的 Next.js 文档。

自托管

自托管有三条路,按需选:

官方有一个 45 分钟的讲解视频,想深入了解自托管可以看看。

社区也维护了不少部署示例,覆盖了这些平台:Deno、DigitalOcean、Flightcontrol、Fly.io、GitHub Pages、Google Cloud Run、Railway、Render、SST。

Node.js 服务器

只要托管的服务商支持 Node.js,Next.js 就能跑。先把 package.json 里的脚本配好:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

然后 npm run build 构建,再用 npm run start 把 Node.js 服务器拉起来。这种部署方式下,Next.js 所有功能都是可用的。

Docker 镜像

只要服务商能跑 Docker 容器,Next.js 就能往上扔。Kubernetes 这类容器编排工具,或者任意云厂商的容器服务,都适用。

操作步骤也不复杂:

  1. 本机装好 Docker
  2. 把官方示例仓库克隆下来,或者用多环境那个示例
  3. 构建镜像:docker build -t nextjs-docker .
  4. 跑起来:docker run -p 3000:3000 nextjs-docker

通过 Docker 部署,Next.js 功能也是全的。

静态 HTML 导出

这条路比较特别。Next.js 可以先按静态站点或单页应用的方式跑起来,以后需要服务器能力了,再随时升级。

因为支持静态导出,产物可以扔到任何能托管静态文件的地方,比如 AWS S3、Nginx、Apache。

代价是,依赖服务器的 Next.js 功能,在这种模式下都不能用。具体哪些功能不支持,官方文档里有列表,动手之前先翻一眼。

静态导出也不是处处受限。文档专门确认了,服务器组件是支持静态导出的。在 App Router 里写的服务端组件,导出时完全没问题。

特性

自托管涉及的功能,文档一项一项列了支持情况。我按顺序过一遍,限制和坑会顺带说清楚。

图片优化

next/image 的图片优化功能,用 next start 启动时零配置就能自托管。想单独搞个图片优化服务,文档也留了口子,配置自定义加载器就行。

静态导出配自定义图片加载器,也能用图片优化。但图片是运行时才优化的,构建的时候不处理。

有几个细节容易踩坑。基于 glibc 的 Linux 系统上,图片优化可能需要附加配置,不然内存会吃得太狠。优化图片的缓存行为也值得看一眼,TTL 可以自己调。

你还可以直接禁用图片优化,next/image 的其它好处不受影响。比如你自己有一套图片处理流程,关掉反而省事。

中间件

中间件用 next start 也是零配置自托管。但它需要访问每一个传入的请求,所以静态导出用不了。

中间件跑在一个 Node.js API 子集的运行时上,放弃一部分 API 换低延迟。这个运行时不要求在边缘运行,单区域服务器就能跑。跑多区域,就得自己加配置和基础设施。

如果需要完整的 Node.js API,或者想用外部包,有个替代思路:把逻辑挪到 layout 里,写成服务器组件。检查 headers、做 redirect 都行。next.config.js 里也支持用 headers、cookie、query 参数判断,做 redirect 或 rewrite。要是还不够,就上定制服务器。

环境变量

构建时和运行时的环境变量都支持。默认情况下,环境变量只在服务端可用。要暴露给浏览器,必须加 NEXT_PUBLIC_ 前缀。但这些公共变量会在 next build 时被内联进 JavaScript bundle,敏感信息千万别放这里。

顺带说一句,服务端在动态渲染期间读 process.env 是安全的。文档给了个示例:在组件里先 await connection(),把渲染切成动态的,再读环境变量,拿到的就是运行时值。这个模式配合 Docker 很实用。打一个镜像,到不同环境用不同环境变量直接跑,不用重新构建。

还有两个点。可以用 register 函数在服务器启动时执行代码。官方不建议用 runtimeConfig 选项,这玩意儿在 standalone 输出模式下不工作。要迁移的话,建议渐进式切换到 App Router。

缓存和 ISR

Next.js 能缓存的东西不少:响应、生成的静态页面、构建输出,还有图片、字体、脚本这些静态资源。

页面缓存和 ISR 重新验证,用的是同一个共享缓存。默认情况下,这个缓存存在 Next.js 服务器的文件系统上,也就是磁盘。不管是 Pages Router 还是 App Router,自托管时这个机制都自动生效。

自托管涉及的功能,文档一项一项列了支持情况。我按顺序过一遍,限制和坑会顺带说清楚。

图片优化

next/image 的图片优化功能,用在小规模自托管场景下完全没问题。它是按需优化的,第一次请求图片时才生成优化版本,之后走缓存。默认缓存 60 分钟,可以通过 images.minimumCacheTTL 调整。

有个点需要注意:如果你用 next start 跑生产服务,图片优化是开箱即用的。但你要是把 Next.js 作为一个库嵌入到自己的 Node.js 服务器里,得手动处理图片优化请求。文档给了示例,用 imageOptimizer 中间件或者自己写路由都行,核心是把 /_next/image 这个路径的请求转发给图片优化逻辑。

另一个限制是优化后的图片默认只缓存到内存和磁盘,多实例部署时每台机器各自存一份。想共享图片缓存,得自己接 Redis 或者 S3,这个后面缓存部分会细说。

缓存

缓存这块分两块:数据缓存和构建缓存。

数据缓存就是 ISR 和 fetch 那套东西的落地存储。自托管时默认存在内存和磁盘上,内存上限 50MB,磁盘存在 .next/cache 目录里。

有个场景容易出问题:用 Kubernetes 这类容器编排平台部署时,每个 Pod 各自有一份缓存副本,Pod 之间不共享。结果就是同一个页面,打到 Pod A 可能走缓存,打到 Pod B 又重新生成一次。过时数据的问题倒不大,但缓存命中率会很难看,浪费 CPU。

文档给的解法是自定义 cacheHandler。在 next.config.js 里指定一个缓存处理模块:

module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, // 禁用默认的内存缓存
}

然后在项目根目录建 cache-handler.js,实现 getsetrevalidateTag 三个方法就行。官方示例里用的是 Map 存内存,实际生产环境可以接 Redis 或者 S3,保证所有 Pod 共享同一份缓存。

顺带说一句,revalidatePath 这个 API 是构建在缓存标签之上的。调用 revalidatePath 实际上会调用 revalidateTag,只不过为指定页面生成一个特殊的默认标签。理解这个关系之后,调试缓存失效问题会容易一些。

构建缓存

Next.js 在 next build 时生成一个构建 ID,用来标记当前应用的版本。多容器部署时,应该基于同一份构建产物启动所有实例。

如果每个环境都重新构建一次,比如开发环境构建一次、预发布环境再构建一次,那每个容器拿到的构建 ID 不一样。需要让构建 ID 保持一致的话,可以在 next.config.js 里指定 generateBuildId

module.exports = {
  generateBuildId: async () => {
    // 可以用 git hash,也可以用环境变量
    return process.env.GIT_HASH
  },
}

这个 ID 会出现在客户端请求的资源路径里,保持一致可以避免不同容器之间出现资源版本不一致的问题。

版本偏差

多容器部署时,新旧版本同时存在的情况很难完全避免。比如滚动发布期间,老容器还没退完,新容器已经起来了。这时候用户请求打到不同容器上,拿到的页面版本可能不一样。

Next.js 对这个场景做了自动处理。页面组件里有个 deploymentId,客户端每次请求时会带上。部署 ID 对不上的话,页面之间的跳转会跳过预取数据,直接硬导航,强制拉一遍新资源。相当于自己给自己做了一次兜底的版本对齐。

代价是页面刷新时可能会丢应用状态。URL 参数或者 localStorage 里存的状态能保留下来,但 useState 这种组件内存状态在硬导航后肯定没了。如果应用依赖跨页面保留大量内存状态,这个行为要提前考虑到。

下一部分继续讲自托管的配置项和部署细节。

倾斜保护

新版本上线后,还停在旧页面的用户继续操作,请求打到新版本上,新旧代码混在一起容易出问题。Vercel 的倾斜保护就是干这个的,保证旧客户端在新版部署后依然能访问旧版本的资源和函数。

自托管没有这套机制,可以手动在 next.config.js 里配 deploymentId。请求带上 ?dpl 查询参数,或者加 x-deployment-id 请求头,就能让请求落到正确的版本上。

流式和悬念

App Router 自托管支持流式响应。前面挂 Nginx 这类代理的话,需要把缓冲关掉,不然代理会等整个响应读完再往下传,流式效果就没了。

关缓冲只需要设置 X-Accel-Buffering: no 响应头。在 next.config.js 里加一段配置:

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*{/}?',
        headers: [
          {
            key: 'X-Accel-Buffering',
            value: 'no',
          },
        ],
      },
    ]
  },
}

部分预渲染

部分预渲染(实验性)默认跟着 Next.js 走,不依赖 CDN。不管是通过 next start 跑 Node.js 服务器,还是放 Docker 容器里跑,行为都一样。

搭配 CDN 使用

套 CDN 的时候,页面访问了动态 API,响应头会带 Cache-Control: private,生成的 HTML 不会被 CDN 缓存。页面完全预渲染成静态的话,响应头是 Cache-Control: public,CDN 可以正常缓存。

不需要静态动态混合,就把整个路由做成静态,输出的 HTML 直接交给 CDN 缓存。只要不用动态 API,next build 默认就是这个行为。

after

afternext start 自托管下完全支持。

停服务器的时候发 SIGINTSIGTERM 信号,等它优雅关闭。Next.js 会等到 after 里挂着的回调或 promise 全部跑完再退出。

想在自定义基础设施上用 after,先查服务商文档支不支持。serverless 的情况复杂一些,响应发完之后异步任务还得继续跑,平台得提供 waitUntil(promise) 这种原语,把函数生命周期延长到所有 promise 结束。Next.js 和 Vercel 就是这么配合的。

自己做平台的话,得实现一个行为类似的 waitUntil。Next.js 调用 after 时,通过全局对象访问它:

const RequestContext = globalThis[Symbol.for('@next/request-context')]
const contextValue = RequestContext?.get()
const waitUntil = contextValue?.waitUntil

globalThis[Symbol.for('@next/request-context')] 需要是这个形状:

type NextRequestContext = {
  get(): NextRequestContextValue | undefined
}

type NextRequestContextValue = {
  waitUntil?: (promise: Promise<any>) => void
}

一个完整的实现示例:

import { AsyncLocalStorage } from 'node:async_hooks'

const RequestContextStorage = new AsyncLocalStorage<NextRequestContextValue>()

// 定义并注入 Next.js 要用的 accessor
const RequestContext: NextRequestContext = {
  get() {
    return RequestContextStorage.getStore()
  },
}
globalThis[Symbol.for('@next/request-context')] = RequestContext

const handler = (req, res) => {
  const contextValue = { waitUntil: YOUR_WAITUNTIL }
  // 把 waitUntil 放进上下文里
  return RequestContextStorage.run(contextValue, () => nextJsHandler(req, res))
}

常见问题(FAQ)

Next.js 部署到服务器一定要用 Vercel 吗?

不是。自托管完全可行,支持 Node.js 服务器、Docker 容器等方式。只要用 next start 启动,所有功能都可用。静态导出才会限制部分功能。

Next.js 自托管时图片优化和中间件怎么配置?

用 next start 启动时两者都零配置可用。但静态导出不支持中间件。图片优化在自托管时按需生成,可自定义加载器或调整缓存 TTL,注意内存配置。

Next.js 静态导出有什么限制?

依赖服务器的功能不可用,如中间件、ISR 等。但服务器组件支持静态导出。适合纯静态站点,未来可随时升级到完整功能。

晓婷深圳
本文由 晓婷 审核,最后更新于 2026年8月8日
联系编辑 →
← 返回文章列表
分享到:微博

版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。

文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。

若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。