自托管 Next.js:让缓存、多实例部署与流式响应保持一致

原文:Next.js 文档团队,How to self-host your Next.js application。本文依据 2026-10-05 读取的完整官方 Markdown 翻译整理;源文件标注文档版本 16.3.8,最后更新日期 2026-08-25。以下配置随版本演进,不能不经核对直接套用到旧版 Next.js。本文只做静态核验,没有构建、部署或压力测试。

一个 Next.js 应用能够构建并启动,只是自托管的起点。反向代理怎样接收请求,环境变量何时读取,缓存放在哪里,以及滚动部署时新旧实例如何共存,都会影响应用实际表现。本文沿官方指南逐项说明这些基础设施约束。

Next.js 多实例自托管结构:用户请求经 CDN 与反向代理分配到相同构建的实例;实例共享外部缓存和失效标签,并保持部署标识与 Server Functions 密钥一致。
未完纪绘制:多实例部署需要同时协调请求路径、构建、密钥和缓存标签。图中为技术关系,非实际部署截图。

在应用前设置反向代理

官方建议在自托管 Next.js 服务前放置 nginx 等反向代理。代理可以处理畸形请求、慢连接攻击、请求体大小限制、速率限制等问题,使 Next.js 把更多资源用于渲染。这里的建议不是说安装 nginx 就自动完成安全配置;相应限制必须在自己的代理配置中明确落实。

图片优化与 Proxy 的运行条件

通过 next start 运行时,next/image 的图片优化无需额外部署配置即可工作。若使用独立图片处理服务,可以配置自定义 image loader。静态导出也能结合自定义 loader 使用图片优化,但优化发生在运行阶段,不是 Next.js 构建时自动把所有图片预先处理好。

在采用 glibc 的 Linux 系统上,底层图片处理可能需要额外的内存分配器配置以避免过量占用。还应核对图片缓存与 TTL。若图片已经由其他系统优化,可禁用这项优化,并继续使用 next/image 的其他能力。

Next.js 的 Proxy 在 next start 自托管下可以工作;由于它需要访问到达服务器的请求,静态导出不支持它。若某段逻辑或外部包要求完整 Node.js API,可考虑把合适的逻辑放到作为 Server Component 的 layout 中,例如读取请求头并重定向。另一个选择是使用 next.config.js 中依据请求头、Cookie 或查询参数的 redirects / rewrites;仍不适合时再评估自定义服务器。本文沿用此版本文档的 Proxy 名称。

区分构建时与运行时环境变量

Next.js 同时支持构建时和运行时变量。默认情况下变量只在服务器端可见;加上 NEXT_PUBLIC_ 前缀后,变量会在 next build 时内联进浏览器 JavaScript。因此,它是公开构建产物的一部分,不能用来保存秘密,也不会因为部署后修改容器环境就自动变化。

在服务器的动态渲染期间,可以读取运行时变量。官方示例通过 connection() 明确进入请求时执行;读取 cookies、headers 等请求时 API 也会影响动态渲染:

import { connection } from 'next/server'

export default async function Component() {
  await connection()
  const value = process.env.MY_VALUE
  // 在服务器端使用 value;返回内容由具体业务补充。
}

上例是展示求值时机的局部代码,不是完整页面组件。这样,同一个 Docker 镜像可以在多个环境中使用不同的运行时值。需要在服务器启动时运行的逻辑,可参考 instrumentation 的 register。即使变量只从服务器读取,也不要无意通过响应、日志或传给客户端的 props 泄露秘密。

理解默认缓存位置与响应头

Next.js 可以缓存响应、生成的静态页面、构建结果,以及图片、字体、脚本等资源。页面缓存与 ISR(增量静态再生)共用 Next.js 服务器缓存,默认写入每个实例的本地磁盘。拥有持久化磁盘的单个 next start 实例可直接使用这一机制;多实例、临时计算环境,或前面还有 CDN / 反向代理时,还需要额外协调。

官方指南区分以下响应:

  • 真正不可变的资源使用 Cache-Control: public, max-age=31536000, immutable,这项设置不能覆盖。这类文件名包含内容哈希;修改内容会产生新的地址。图片优化的缓存 TTL 有自己的配置入口。
  • 原文以 Pages Router 的 getStaticProps 为例说明 ISR:共享缓存时间取自 revalidate 秒数,并使用 stale-while-revalidate;revalidate: false 对应默认一年缓存周期。这是该 API 的示例,不能把 getStaticProps 当作 App Router 的入口。
  • 动态渲染页面使用 private, no-cache, no-store, max-age=0, must-revalidate,避免用户相关数据被共享缓存;这也包括 Draft Mode,并适用于 App Router 与 Pages Router。

CDN 或代理必须尊重这些指令和缓存键的变化维度,否则可能完全绕过缓存,也可能在客户端导航中返回过期或不匹配的响应。不能只按 URL 缓存所有 HTML / RSC 响应,更不能覆盖用户私有页面的缓存策略。具体配置见官方 CDN Caching 指南。

静态资源域名与自定义缓存处理器

assetPrefix 可以让 JavaScript、CSS 资源从独立域名或 CDN 获取。代价是增加 DNS 查询及 TLS 建连开销,是否有益需按实际网络验证。它不是所有文件路径的通用重写开关,应查阅具体适用范围。

原文说明默认生成缓存使用内存(默认 50 MB)和磁盘。临时计算平台上的磁盘可能不能持久保存;Kubernetes 的每个 Pod 默认也各有一份缓存。要避免实例各自保留过期数据,可以接入自定义处理器并禁用默认内存缓存:

module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0,
}

下面是原文用于说明接口的 cache-handler.js,保留其 Map 结构;它只在一个 Node.js 进程的内存中保存数据:

const cache = new Map()

module.exports = class CacheHandler {
  constructor(options) {
    this.options = options
  }

  async get(key) {
    return cache.get(key)
  }

  async set(key, data, ctx) {
    cache.set(key, {
      value: data,
      lastModified: Date.now(),
      tags: ctx.tags,
    })
  }

  async revalidateTag(tags) {
    tags = [tags].flat()
    for (let [key, value] of cache) {
      if (value.tags.some((tag) => tags.includes(tag))) {
        cache.delete(key)
      }
    }
  }

  resetRequestCache() {}
}

静态审核:Map 既不是持久化存储,也不是分布式缓存;进程重启后数据消失,另一个 Pod 看不到它。此示例没有容量淘汰、错误处理或分布式标签协调;revalidateTag 逐项扫描,并假定 value.tags 可调用 some。关闭框架默认内存缓存,并不会把这个 Map 自动变成共享存储。

生产实现应补上持久化后端、淘汰策略、失败处理及标签协调。原文指向官方 Redis 示例,也提到把缓存值存入 S3 等外部存储的可能性。处理器接口与底层存储的职责仍须自己实现。revalidatePath 建立在缓存标签之上,会使用对应路径的特殊默认标签调用标签失效流程。

必须区分两个配置名:单数 cacheHandler 与复数 cacheHandlers 服务于不同的缓存接口;为 'use cache' 指令配置后端时,原文要求使用后者。不能将上面的单数接口示例直接认作 'use cache: remote' 的完整实现。

让构建、部署标识与密钥一致

next build 会生成标识当前构建的 ID。多容器最好使用同一份构建产物。如果每个阶段都重新构建,原文给出的思路是通过 generateBuildId 返回统一 Git 哈希。以下是增加了缺值检查的编辑修正版:

module.exports = {
  generateBuildId: async () => {
    const id = process.env.GIT_HASH
    if (!id) throw new Error('GIT_HASH is required')
    return id
  },
}

与原例相比,只增加了变量未设置时明确失败的检查。当前文档特别指出:设置 deploymentId 后,Next.js 使用恒定的 build ID,generateBuildId 不再生效,版本错配检测改由部署标识承担。统一 ID 也不能把不同构建内容变成同一份产物。

Next.js 会在把 Server Function 的闭包变量发送到客户端前加密,默认每次构建生成新密钥。运行多个实例时,必须使用一致的密钥,否则某个实例加密的调用状态可能无法由另一个实例解密,表现为 “Failed to find Server Action”。用 NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 在构建阶段提供一致密钥;其值必须是 Base64 编码,解码后的 AES 密钥长度为 16、24 或 32 字节,默认生成长度为 32 字节。

原文命令中的 your-generated-key 是占位文字,不是可用密钥。实际秘密应由受控的构建秘密管理机制注入,避免写入版本库、共享命令历史或构建日志。密钥会进入构建产物并在运行时自动使用,因此产物也需要访问控制。此机制不能代替应用自身的鉴权与授权,更多细节见Data Security。

滚动部署还应配置稳定、非空、能识别这次部署的 deploymentId。以下代码是在原文基础上增加了缺值检查:

const deploymentId = process.env.DEPLOYMENT_VERSION
if (!deploymentId) throw new Error('DEPLOYMENT_VERSION is required')

module.exports = { deploymentId }

理解版本错配的处理方式

滚动部署时,客户端可能仍在使用旧版本代码,却访问新实例:它需要的 JS / CSS 已不存在,Server Function ID 无法识别,或预取数据与新服务器不兼容。配置部署 ID 后,静态资源地址携带 ?dpl=<deploymentId>,客户端导航请求携带 x-deployment-id,服务器据此比较版本。

发现不匹配时,Next.js 使用完整页面重载代替客户端导航,使客户端重新获取一致版本的资源。页面重载可能丢失仅放在 useState 等组件状态中的数据;URL 状态或已持久保存的状态则可跨导航保留。应用要根据自己的恢复需求设计状态存放方式,不能把用户秘密随意放进 URL 或本地存储。

让整个链路都支持流式响应

App Router 自托管支持 Streaming 与 Suspense。nginx 或类似代理的缓冲可能使用户直到完整渲染结束才收到内容。原文给出通过响应头关闭 nginx 缓冲的例子:

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

此处路径匹配语法按文档 16.3.8 保留。应与项目现有 headers() 合并,不要把前面几个 module.exports 片段依次粘贴、互相覆盖。是否实际流式到达仍取决于代理是否尊重该响应头、负载均衡器以及中间每一级代理。

负载均衡器必须支持分块传输或 HTTP/2 流式传输;某些集成方式(原文举 AWS ALB 与 Lambda 集成为例)可能缓冲响应。使用 Partial Prerendering 时尤其要检查端到端链路,否则静态壳与动态内容一起延迟交付,失去它的首字节时间优势。本文没有测量 TTFB 或验证任一云平台配置。

共享缓存值,也要共享失效标签

采用外部存储的 'use cache: remote' 处理器,可以让实例共享缓存内容;但多实例 App Router 还需要同步标签状态。在一个实例调用 revalidateTag(),默认只使该实例感知失效,其他实例可能继续使用旧内容。

原文要求在相应的自定义 cacheHandlers 处理器中实现 refreshTags():它在每次请求前从 Redis 等共享存储同步标签状态,让各实例及时获知失效。共享数据、共享标签、密钥和部署 ID 是相互配合的事项,不能只完成其中一项就宣称多实例已经一致。标签架构详见How Revalidation Works。

Cache Components、CDN 与优雅退出

Cache Components 可以在 next start 的 Node.js 服务或 Docker 容器中使用,它不是只有 CDN 平台才有的能力。页面访问动态 API 时,HTML 带 Cache-Control: private;完全预渲染的静态页面则带 public。不需要混合静态与动态部分的路由,可保持完全静态并由 CDN 缓存;没有动态 API 时,构建阶段默认会尝试自动静态优化。不同平台的 PPR 支持应参考平台指南与Deployment Adapter API。

after() 在 next start 自托管下受到支持。停止服务时,发送 SIGINT 或 SIGTERM 后应给它时间结束在途请求和待执行的 after() 回调。官方建议平台提供可配置的 10–30 秒排空期;它是建议范围,不是任何后台任务都能在此时间内完成的保证。容器或进程管理器过早强杀仍可能中断工作。

本文配置片段用于解释官方行为,部署前仍应在自身版本与基础设施上核对代理缓存、流式传输、实例切换和退出行为。以上均为静态审核,不构成这些环节已经测试通过的结论。

作者与许可:Next.js 文档团队 / Vercel, Inc.,仓库 MIT License,Copyright (c) 2025 Vercel, Inc.;完整许可证全文见下方。本文增加风险解释及两处变量缺值检查,原创配图归未完纪。

MIT License 全文

The MIT License (MIT)

Copyright (c) 2025 Vercel, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容