本机 npm run dev 一切正常,部署到 Vercel / Netlify / Cloudflare Pages 后页面直接 500,或者前端报 Cannot read properties of undefined、API 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.js | NEXT_PUBLIC_ | process.env.NEXT_PUBLIC_X |
| Astro | PUBLIC_ | import.meta.env.PUBLIC_X |
| Vite / SvelteKit | VITE_ | import.meta.env.VITE_X |
| Create React App | REACT_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 不该出现在第二条命令里——如果出现了,说明你不小心给它加了公开前缀。
如何确认修好了
- Production env 面板里能看到这个变量、且作用于 Production 环境。
- 重新部署跑的是完整构建(没有
Restored build cache那一行)。 - 用到该变量的服务端路由返回 200。
- 客户端变量在生产页面的
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”),而不是指望点部署的人记得勾。