本机 npm run dev 一切正常,部署到 Vercel / Netlify / Cloudflare Pages 后页面直接 500,或者前端报 Cannot read properties of undefined、API key is required——99% 是环境变量没正确传到生产环境。Astro / Next.js / Vite 这些框架对”哪些 env 能进客户端 bundle、哪些只在服务端可用”有严格的前缀约定,本地 .env 文件里随便写都能跑通,生产却必须显式声明。本文按命中率列出 5 类失败、给一条可在 10 分钟内排查完的修复路径。
常见原因
按命中率从高到低。
1. 宿主面板里根本没设这个变量
本地有 .env.local,但 Vercel / Netlify / Cloudflare Pages 的 Settings → Environment Variables 是空的或只设了 Preview / Development,没勾 Production。.env 文件默认不会被推到 Git(也不应该),所以宿主拿不到。
# Vercel CLI 一键看线上有哪些 env
vercel env ls production
# 期望看到你需要的变量名;不在列表里就是没设
如何判断:宿主的 build log 末尾通常会输出环境信息;变量名不在里面,或运行时报 process.env.X is undefined,就是这条。
2. 客户端代码读了没有 PUBLIC_ 前缀的变量
各框架对”暴露给浏览器”的前缀要求不一样,没前缀的变量在 build 时会被替换成 undefined:
| 框架 | 客户端前缀 |
|---|---|
| Next.js | NEXT_PUBLIC_ |
| Vite / Astro / SvelteKit | PUBLIC_ (Astro) / VITE_ (Vite) |
| Create React App | REACT_APP_ |
| Remix | LOADER_FUNCTION 里手动暴露 |
// Astro 组件里
const wrong = import.meta.env.API_URL; // undefined(无前缀)
const right = import.meta.env.PUBLIC_API_URL; // 正常
如何判断:打开生产页面 DevTools → Console → 输入 window.__NEXT_DATA__ 或 import.meta.env,看不到该变量就是被框架过滤掉了。
3. Build cache 命中了上一次的构建产物
你刚加了一个新变量,重新部署,但 Vercel 的 build cache 直接拿上次 dist/ 复用,新变量根本没参与编译。常见于”只改 env 不改代码”的部署。
如何判断:build log 里看到 Restored build cache 或部署时间异常短(几秒钟而不是几分钟)。
4. 变量名拼写 / 大小写不一致
.env 写的是 STRIPE_SECRET_KEY,代码读的是 STRIPE_SECRET;或者 Vercel 面板里多了个尾随空格。环境变量名是大小写敏感的,宿主 UI 经常吃掉这种细节问题。
如何判断:
# 在 Vercel build 里临时打印一下(用完删掉,别留生产)
node -e 'console.log(Object.keys(process.env).filter(k => k.includes("STRIPE")))'
5. monorepo / Turborepo / Nx 没把变量传到子包
根目录 .env 有变量,但子 workspace 里的 next build 看不到——Turborepo 默认不会把环境变量传给 task,要在 turbo.json 里显式列出。
// turbo.json
{
"pipeline": {
"build": {
"env": ["DATABASE_URL", "NEXT_PUBLIC_API_URL"]
}
}
}
如何判断:本地直接进子目录跑 cd apps/web && npm run build 没问题,但根目录 turbo build 就缺;说明变量没穿透。
最短修复路径
按”先确认线上确实有这个变量、再确认前缀对、最后强制无缓存重建”的顺序。
Step 1:在宿主面板把变量加到 Production 环境
不同平台路径:
- Vercel:Project → Settings → Environment Variables → Add New → 名字 / 值 / 选 Production(preview / development 按需勾选)。
- Netlify:Site → Site configuration → Environment variables → Add a variable。
- Cloudflare Pages:Settings → Environment variables → Production → Add variable。
- Firebase Hosting + Functions:用
firebase functions:config:set或 Secret Manager。
# Vercel CLI 也能加
vercel env add STRIPE_SECRET_KEY production
# 它会交互式问你值
注意敏感变量(API key、DB 密码)一定要用平台的 “Sensitive” / “Secret” 选项,否则 build log 里会明文出现。
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;
宿主面板里也要用相同前缀的完整变量名,不能省略。
Step 3:触发一次”不带 build cache”的重新部署
# Vercel CLI
vercel --prod --force
或在仪表盘: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,比上线后 500 友好得多。
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
预防建议
- 仓库里维护一个
.env.example,列出所有必需变量名(不带值);README 写清楚哪些必填 / 选填。 - 启动时用 Zod / Valibot / Envalid 做 schema 校验,缺一个就让 build 失败。
- 在 CI 里加一个
check-env步骤:对照.env.example,列出宿主上缺的变量并 fail。 - 客户端 / 服务端变量分文件存:
.env.public全PUBLIC_前缀,.env.secrets全无前缀;review 时容易看出泄漏。 - 改完环境变量后默认强制无缓存重建,并在部署 webhook 里写
force=true而不是依赖人手记。