首页/后端问题/为什么你改了 .env,线上还是旧值:Next.js 部署中最隐蔽的环境变量冻结问题
后端问题

为什么你改了 .env,线上还是旧值:Next.js 部署中最隐蔽的环境变量冻结问题

在 Node.js / Next.js 部署里,真正折磨人的往往不是服务起不来,而是另一类更阴的故障:项目能启动、页面也能打开,但接口地址错了、登录回调错了、埋点错了,甚至你明明改了服务器环境变量,线上仍然还是旧值。这类问题最常见的根源,是 Next.js 把一部分变量在构建阶段就写死了,而你以为它会在运行阶段重新读取。

发布时间:2026年3月29日 17:35阅读量:4

在 Node.js / Next.js 部署里,真正折磨人的往往不是服务起不来,而是另一类更阴的故障:项目能启动、页面也能打开,但接口地址错了、登录回调错了、埋点错了,甚至你明明改了服务器环境变量,线上仍然还是旧值。这类问题最常见的根源,不是 Nginx,不是 PM2,也不是 Docker,而是 Next.js 把一部分变量在构建阶段就写死了,而你以为它会在运行阶段重新读取。

一、这个问题为什么棘手

它棘手在于:本地开发往往是正常的,只有部署后才暴露。

因为在开发环境里,大家频繁 next dev、频繁重启,变量变化很容易看起来生效;但到生产环境后,Next.js 的一部分环境变量会在 next build 时被内联进前端 bundle,而 Node.js 进程本身也只会读取它启动时拿到的环境变量。于是你会看到一种很迷惑的现象:代码已经更新了,服务器 .env 也改了,但线上用户拿到的仍是旧配置。

最典型的症状有这几种:

  • 你把 NEXT_PUBLIC_API_BASE_URL 从测试域名改成正式域名,页面上线后仍请求测试接口
  • 你把 OAuth 回调域名改了,登录仍然跳去旧域名
  • 你在服务器上改了 .env.production,但只重启了 Nginx,没有重建或重启 Node 进程,接口还是旧值
  • 同一个 Docker 镜像从 staging 提升到 production 后,浏览器里仍拿到 staging 的 NEXT_PUBLIC_* 配置

这些现象并不矛盾,它们恰好对应了 构建时变量 和 运行时变量 的边界。

二、真正的根因:你把两种变量混在了一起

Next.js 官方文档写得很明确:它同时支持 build time 和 runtime 环境变量。默认情况下,环境变量只在服务器可用;如果要暴露给浏览器,必须加 NEXT_PUBLIC_ 前缀。问题就在这里:这类 public 变量会在 next build 时被直接内联进浏览器端 JavaScript bundle。一旦构建完成,它们就不会随着服务器环境变量变化而自动变化。

官方还特别提醒:如果你把同一个构建产物或者同一个 Docker 镜像从一个环境提升到另一个环境,那么所有 NEXT_PUBLIC_ 变量都会保持构建时的值,也就是冻结状态。换句话说:

NEXT_PUBLIC_* 不是浏览器在运行时读取服务器环境变量,而是构建时把值写进前端代码。

与此同时,Node.js 这一层也有自己的规则。Node 官方文档说明,process.env 挂的是进程启动那一刻拿到的环境变量。也就是说,就算你在系统里改了 .env 或 shell 导出的变量,只要 Node 进程没重新启动,进程内看到的值仍可能是旧的。

所以这个问题通常是两层叠加:

  1. 前端拿到的是 构建时冻结的 public 变量
  2. 服务端拿到的是 进程启动时读取的 server 变量

你如果分不清这两层,就会一直在错误的地方排查。

三、一个最常见的翻车场景

假设你有这样的配置:

env
NEXT_PUBLIC_API_BASE_URL=https://staging-api.example.com
DATABASE_URL=postgres://...

你在 CI 里执行了:

bash
next build
next start

此时,NEXT_PUBLIC_API_BASE_URL 会在构建时被写进浏览器 bundle;而 DATABASE_URL 则主要供服务器进程通过 process.env 使用。后来你把这个镜像直接提升到生产环境,并在服务器里把变量改成:

env
NEXT_PUBLIC_API_BASE_URL=https://api.example.com
DATABASE_URL=postgres://prod...

结果会出现一种非常迷惑的局面:

  • 浏览器仍然请求 staging-api.example.com
  • 但服务端连接数据库可能已经变成 prod

这不是 Next.js 抽风,而是它本来就这么设计的:浏览器端 public 变量在构建阶段已确定,服务端变量则依赖运行时进程环境。官方自托管和环境变量文档都明确说明了这一点,并建议要读取运行时变量时,走服务端动态渲染能力。

四、哪些变量应该是构建时,哪些应该是运行时

这是部署时最关键的判断。

1)适合构建时固定的变量

这类变量通常在每个环境下基本不变,或者你本来就愿意每个环境单独构建一次:

  • 前端品牌名
  • 固定 CDN 地址
  • 固定公开埋点 ID
  • 明确随构建版本绑定的前端常量

这类值放到 NEXT_PUBLIC_* 没问题,因为你接受它在构建后固定。

2)必须按运行环境切换的变量

这类变量不适合直接写成 NEXT_PUBLIC_* 后再用一个镜像跑多环境的方式部署:

  • API Base URL
  • OAuth 回调域名
  • 租户级域名
  • 灰度环境开关
  • 需要按服务器、区域、客户而变化的前端配置

如果你希望同一个镜像在 staging、preprod、prod 之间流转,那么这类值更适合走服务端运行时读取,再通过服务端渲染、接口下发或配置接口提供给前端。Next.js 官方文档明确建议:要读取运行时环境变量,Pages Router 用 getServerSideProps,或者逐步采用 App Router 的服务端动态渲染方式。

五、最容易犯的 4 个部署错误

错误一:以为改 .env 就等于前端变量生效

不是。

如果变量是 NEXT_PUBLIC_*,那么它在 next build 后就已经写进 bundle 了。你只改服务器 .env,不重建,浏览器看到的还是旧值。

错误二:把多环境切换需求硬塞给 NEXT_PUBLIC_*

如果你的部署策略是 build once, deploy many,那就不要把会随环境变化的浏览器配置直接做成 NEXT_PUBLIC_*。官方文档点名说过:同一个 Docker 镜像或同一个构建产物提升到多个环境时,public 变量会被冻结。

错误三:改了服务器环境,但没重启 Node 进程

Node 的 process.env 是进程启动时拿到的环境。只改系统变量或 .env 文件,不重启进程,服务端代码也可能仍然读到旧值。

错误四:继续依赖 next.config.js 里的 env

Next.js 官方把 next.config.js 里的 env 选项标成了 legacy API,而且说明这种方式会始终把值放进 JavaScript bundle,并在构建时替换掉。它更容易让人误以为自己在做运行时配置,实际却是在做编译期常量替换。

六、正确的修复思路

方案一:把会变的前端配置改成服务端下发

这是最稳的方案。

不要让浏览器直接依赖 NEXT_PUBLIC_API_BASE_URL 这类会变的值,而是改成:

  1. 浏览器只请求你自己的 Next.js 路由
  2. 由 Next.js 服务端在运行时读取 process.env
  3. 再转发、拼接或下发配置

这样你就能做到:

  • 同一个镜像在多环境复用
  • 改服务器环境变量后,只需重启服务端进程
  • 不必为了改一个 API 域名就重新 next build

这条思路与官方运行时变量用服务端读取的推荐方向一致。

方案二:如果必须用 NEXT_PUBLIC_*,就接受每环境重建

有些公开变量确实适合放在浏览器端,那就不要混淆:

它是构建时变量,就要在对应环境里构建。

也就是说:

  • 测试环境一套 build
  • 生产环境另一套 build

不要拿 staging build 出来的产物直接提升到 production,再期待 NEXT_PUBLIC_* 自动切换。官方已经明确说这种方式不会生效。

方案三:服务端变量更新后,必须重启进程

如果你改的是 DATABASE_URL、REDIS_URL、INTERNAL_API_TOKEN 这类服务端变量,那么通常不需要重建前端,但需要重启 Node 进程,因为 process.env 取决于进程启动时的环境。

七、一套可靠的部署规则

我建议你把这几条写进部署规范里。

规则 1

凡是 NEXT_PUBLIC_*,默认视为构建产物的一部分。改它,就准备重新 next build。

规则 2

凡是希望一个镜像跑多个环境的配置,不要直接放浏览器 bundle。优先改成服务端运行时读取,再由服务端输出给前端。

规则 3

凡是只给 Node 服务端用的变量,改完必须重启进程。因为 Node 读取的是进程启动时环境。

规则 4

别再把 next.config.js 的 env 当成动态配置中心。官方已经把它标为 legacy,而且它本质上仍是构建期替换。

八、实战排查顺序

遇到我明明改了环境变量,为什么线上还是旧值时,我建议按这个顺序查:

1)这个变量是不是 NEXT_PUBLIC_*?

如果是,优先怀疑构建冻结。检查当前线上产物到底是在哪个环境构建的。

2)这个变量是不是只在服务端使用?

如果是,检查 Node 进程是否真的重启了。只 reload Nginx、只改 .env、只更新容器挂载文件,都不等于 Node 进程已重新读取环境。

3)你是不是在用同一个镜像跨环境推广?

如果是,再看浏览器端是否依赖 NEXT_PUBLIC_*。这几乎是最常见根因。

4)你是不是用了 next.config.js env?

如果是,把它当成编译期常量,而不是运行时配置。

九、一个更稳的落地方式

如果你想把部署链路做得更专业,我建议这样分层:

前端浏览器层只拿两类东西:

  • 真正可以公开、且允许构建时固定的常量
  • 来自服务端接口的动态配置

Next.js 服务端层负责:

  • 在运行时读取 process.env
  • 处理不同环境下的 API 域名、密钥、回调域名、特性开关
  • 把需要给浏览器的动态配置通过 SSR、Route Handler 或配置接口返回

Node 进程启动层则统一管理:

  • 系统环境变量
  • --env-file 或进程管理器注入的环境
  • 重启策略

Node 官方文档说明,node --env-file 可以从文件加载环境变量,而且如果同一个变量同时存在于系统环境和 env 文件中,系统环境优先;还支持多个 --env-file 叠加,后面的文件覆盖前面的文件。这种机制很适合做更可控的部署注入。

十、结语

在 Next.js 部署里,最危险的不是报一个大红错,而是这种看起来都正常,只是值不对的问题。它最容易让人误判成缓存、代理、CDN、Nginx、甚至数据库故障。实际上,很多时候真正的根因只有一句话:

你把构建时变量,当成了运行时变量。

只要记住下面这条,就能少踩很多坑:

NEXT_PUBLIC_* 先想要不要重建,process.env 先想要不要重启。

这就是 Next.js 部署里最隐蔽、也最常见的环境变量陷阱。

问题求助

没能解决你的问题?直接问我

如果你遇到任何技术问题无法解决,可以在这里提交求助。我会尽快查看并回复你。

支持作者

如果这篇文章帮到了你,可以支持我

扫码打赏,支持我持续更新原创排障文章。

打赏二维码