生产环境变量缺失:5 个原因 + 修复路径

本地能跑、线上 500。要么宿主没设这个变量,要么客户端前缀不对(PUBLIC_ / VITE_ / NEXT_PUBLIC_)。10 分钟内排查并修好。

本机 npm run dev 一切正常,部署到 Vercel / Netlify / Cloudflare Pages 后页面直接 500,或者前端报 Cannot read properties of undefinedAPI key is required。几乎都是某个环境变量本地 .env 里有、却没传到生产运行时。背后是两件事在作怪:.env 文件默认(也应该)被 gitignore,不会推到 Git,所以宿主除非你在它的面板里配置,否则根本不知道这些变量;同时 Astro / Next.js / Vite 这类框架对”哪些变量能进客户端 bundle”有严格的前缀规则,不符合的会在 build 时被替换成 undefined

最快修复: 在宿主的 Production 环境里加上这个变量,如果浏览器要读它就确认前缀正确,然后清掉 build cache 重新部署。下面五类按命中率排序,匹配到第一条就能停。

你属于哪一类?

症状最可能的原因跳转
服务端路由 500;函数日志里 process.env.X is undefined宿主没设这个变量原因 1
服务端正常,只有浏览器里是 undefined缺客户端前缀原因 2
变量明明设了,但重新部署快得反常build cache 没失效原因 3
别的变量都能读到,唯独这个读不到变量名拼写 / 空格 / 大小写原因 4
单个应用没事,monorepo turbo build 就挂Turborepo 没把变量传进去原因 5

常见原因

按命中率从高到低。

1. 宿主面板里根本没设这个变量

本地有 .env.local,但宿主的 Environment Variables 面板里 Production 是空的(或者只勾了 Preview / Development)。.env 文件默认不会被推到 Git(也不应该),所以宿主拿不到。

# Vercel CLI 一键看线上有哪些 env
vercel env ls production
# 期望看到你需要的变量名;不在列表里就是没设

如何判断: 函数 / 服务端日志里报 process.env.X is undefined,或者 build log 里解析出来的 env 列表不含这个变量名。

2. 客户端代码读了没有前缀的变量

各框架只把带前缀的变量暴露给浏览器 bundle,不带前缀的在 build 时会被替换成 undefined——所以服务端读得到值,浏览器却读不到:

框架客户端前缀代码里怎么读
Next.jsNEXT_PUBLIC_process.env.NEXT_PUBLIC_X
AstroPUBLIC_import.meta.env.PUBLIC_X
Vite / SvelteKitVITE_import.meta.env.VITE_X
Create React AppREACT_APP_process.env.REACT_APP_X
Remix / React Router无前缀,从 loader 返回从 loader 数据里取
// Astro 组件里
const wrong = import.meta.env.API_URL;        // undefined(无前缀)
const right = import.meta.env.PUBLIC_API_URL; // 浏览器和服务端都能读

按 Astro 官方文档,只有 PUBLIC_ 前缀的变量会进客户端代码;其余(比如 DATABASE_URL)只在服务端可用,在客户端 bundle 里就是 undefined。较新的 Astro 项目还可以用 astro:env 声明带类型的变量,指定 context"client" / "server")和 access"public" / "secret")——必需变量缺失时会直接 build 失败,而不是悄悄塞个 undefined

如何判断: 打开生产页面 DevTools → Console,输入 import.meta.env(Vite/Astro)或 window.__NEXT_DATA__(Next.js)。看不到该变量就是被框架过滤掉了。注意:服务端专用的 secret 本来就不该出现在这里。

3. build cache 命中了上一次的构建产物

你刚加了一个新变量、重新部署,但平台直接复用了上次的构建产物,新变量根本没参与编译。常见于”只改 env 不改代码”的部署,因为有些平台在 Git SHA 没变时会跳过完整构建。

如何判断: build log 里看到 Restored build cache(Vercel),且部署时间异常短(几秒钟而不是几分钟)。

4. 变量名拼写 / 空格 / 大小写不一致

.env 写的是 STRIPE_SECRET_KEY,代码读的是 STRIPE_SECRET;或者面板里多了个尾随空格。环境变量名是大小写敏感的,宿主 UI 经常把一个多余空格藏起来。

如何判断:

# 在 serverless 函数里临时打印一下(用完删掉,别留生产)
console.log(Object.keys(process.env).filter(k => k.includes("STRIPE")))

如果打印出来是带尾随空格的 "STRIPE_SECRET_KEY ",或者干脆没有,就找到了。

5. monorepo(Turborepo / Nx)没把变量传到子包

根目录 .env 有变量,但子 workspace 里的 next build 看不到。从 Turborepo 2.x 起,task 默认运行在 strict 环境模式下,只会拿到你显式声明的变量。要在 tasks 键下列出(旧的 pipeline 键在 v2 已被移除):

// turbo.json(Turborepo 2.x)
{
  "$schema": "https://turborepo.com/schema.json",
  "globalEnv": ["NODE_ENV"],
  "tasks": {
    "build": {
      "env": ["DATABASE_URL", "NEXT_PUBLIC_API_URL"]
    }
  }
}

某个 task 需要的变量放进它的 env(这些变量变化会让该 task 的缓存失效),影响所有 task 的放进 globalEnv。如果你还在用 Turborepo 1.x,同样的配置写在 pipeline 而不是 tasks 下。

如何判断: 本地直接进子目录跑 cd apps/web && npm run build 没问题,但根目录 turbo build 就缺;说明变量没穿透 task 边界。

最短修复路径

按”先确认线上确实有这个变量、再确认前缀对、最后强制无缓存重建”的顺序。

Step 1:在宿主面板把变量加到 Production 环境

截至 2026 年 6 月的各平台路径(厂商经常改菜单名,所以按标签对、别死记点击步骤):

  • Vercel: Project → Settings → Environment Variables → 填 Name 和 Value → 选环境(勾上 Production)→ Save。敏感变量保存前先开 Sensitive 开关。
  • Netlify: Project configuration → Environment variables → Add a variable。要按上下文设不同值就选 Production 部署上下文;敏感变量勾 Contains secret values
  • Cloudflare Pages: Workers & Pages → 你的项目 → Settings → Variables and Secrets → Add → 填 Name 和 Value → 点 Encrypt 存成 secret。设给 Production 环境。
  • Firebase(Cloud Functions): 老的 firebase functions:config:set 已在 2025 年 12 月 31 日停用。普通配置改用 functions 目录下的 .env 文件,secret 用 defineSecret()(底层是 Cloud Secret Manager)。
# Vercel CLI 也能加
vercel env add STRIPE_SECRET_KEY production
# 它会交互式问你值

API key、DB 密码、token 这类一律标成 Sensitive / Secret。Vercel 上设为 Sensitive 后,面板和 vercel env ls 都看不到它的值;若值长度 ≥ 32 且出现在 build log 里,Vercel 会替换成 [REDACTED]

Step 2:客户端变量必须加正确前缀

变量名和代码里都要用对前缀:

# 本地 .env.production
NEXT_PUBLIC_API_URL=https://api.example.com    # Next.js 客户端
PUBLIC_API_URL=https://api.example.com         # Astro 客户端
VITE_API_URL=https://api.example.com           # Vite 客户端
DATABASE_URL=postgres://...                    # 服务端,不加前缀

代码里:

// Next.js
const url = process.env.NEXT_PUBLIC_API_URL;

// Astro / Vite
const url = import.meta.env.PUBLIC_API_URL;

宿主面板里也要用相同前缀的完整变量名,不能省略。绝对不要把服务端 secret 放在公开前缀后面,那等于把它发给每个访客的浏览器。

Step 3:清掉 build cache 重新部署

# Vercel CLI
vercel --prod --force

或在仪表盘:

  • Vercel: Deployments → 最近一次 → ⋯ → Redeploy,取消勾选 “Use existing Build Cache”。
  • Netlify: Deploys → Trigger deploy → 选 “Clear cache and deploy site”。
  • Cloudflare Pages: Deployments → Retry deployment(Pages 重建本来就不带缓存)。

Step 4:用启动期 schema 校验提前失败

在应用启动时用 Zod 校验所有必需变量,缺一个就直接抛错,而不是等用户请求时才 500:

// src/env.ts
import { z } from "zod";

const schema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  NEXT_PUBLIC_API_URL: z.string().url(),
});

export const env = schema.parse(process.env);

部署时若缺变量,build / start 会立刻抛 ZodError,比上线后在用户请求里崩掉好排查得多。

Step 5:用 curl 验证线上确实读到了

部署完成后:

# 服务端变量:访问一个会用到该变量的 API 路由
curl -s https://yourdomain.com/api/healthz
# 期望返回 200 + 包含基于 env 的内容

# 客户端变量:抓服务端返回的 HTML 看 bundle 里是不是替换了
curl -s https://yourdomain.com/ | grep -o "https://api\.example\.com" | head -1

服务端路由返回 200、grep 打出了你的 URL,就说明值生效了。注意服务端 secret 不该出现在第二条命令里——如果出现了,说明你不小心给它加了公开前缀。

如何确认修好了

  1. Production env 面板里能看到这个变量、且作用于 Production 环境。
  2. 重新部署跑的是完整构建(没有 Restored build cache 那一行)。
  3. 用到该变量的服务端路由返回 200。
  4. 客户端变量在生产页面的 import.meta.env / window.__NEXT_DATA__ 里看得到;服务端 secret 则正确地看不到。

常见问题

为什么本地能跑、生产不行? 本地 .env / .env.local 会被 dev server 自动读取,但它被 gitignore、不会部署。生产只读你在宿主面板或 CLI 里设的东西,所以”没设”才是生产环境的默认状态。

加了环境变量后必须重新部署吗? 必须。环境变量是在 build 时(内联的客户端变量)或启动时(服务端变量)读取的。已经在跑的旧部署会一直用旧值,直到你触发新部署——为保险起见再清掉 build cache,让新构建确实读到。

为什么我的 NEXT_PUBLIC_ / PUBLIC_ 变量在浏览器里还是 undefined 要么 build 时这个变量根本不在(先在宿主上设好再重建),要么面板里的名字和代码里带前缀的名字没完全对上。这类变量是在 build 时内联进去的,光在面板里改、不重建是没用的。

把 API key 放在 NEXT_PUBLIC_ 后面安全吗? 不安全。任何带公开前缀的值都会被内联进每个访客下载的 JavaScript 里。key 要保持服务端专用(不加前缀),在 API 路由、服务端组件或函数里读。公开前缀只用于非敏感值,比如公开的 API base URL。

我的 monorepo 在 Vercel 上 build 失败、本地却没事,为什么? Turborepo 2.x 默认 strict env 模式,只传你在 turbo.json 里声明过的变量。把这个变量加到对应 task 的 env 数组里(全局变量则加到 globalEnv),提交后重新部署。

预防建议

  • 仓库里维护一个 .env.example,列出所有必需变量名(不带值);README 写清楚哪些必填 / 选填。
  • 启动时用 Zod / Valibot / Envalid 做 schema 校验,缺一个就让 build 失败。
  • 在 CI 里加一个 check-env 步骤:对照 .env.example,列出宿主上缺的变量并 fail。
  • 客户端 / 服务端变量分文件存:.env.public 全带公开前缀,.env.secrets 全不带前缀;review 时容易看出泄漏。
  • 改完环境变量后默认走强制无缓存重建(--force / “Clear cache”),而不是指望点部署的人记得勾。

相关阅读

标签: #部署 / 托管 #排查 #排查