import 'dotenv/config' 不加载 .env.local,密钥永远 undefined
从 Next.js 迁移出一个独立的 Node.js API 服务时,我习惯性地把密钥放在 .env.local(gitignore 掉)、默认值放在 .env(可提交)。服务一起,报错就来:
Error: DEEPSEEK_API_KEY 未设置
打开 .env.local 一看,key 明明在那里。重启、检查文件编码、确认路径——什么问题都没有。整整折腾了十多分钟。
现象
import 'dotenv/config'
// ...
if (!process.env.DEEPSEEK_API_KEY) throw new Error('DEEPSEEK_API_KEY 未设置')$ grep DEEPSEEK_API_KEY .env.local
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxgrep 有结果,运行时 process.env.DEEPSEEK_API_KEY 却是 undefined。服务用的是 Fastify + tsx,不是 Next.js。
根因
import 'dotenv/config' 是 dotenv.config() 的快捷写法——它只加载 process.cwd() 下的 .env 文件,仅此而已。
.env.local、.env.development、.env.<NODE_ENV> 这套级联规则是 框架层 实现的,不是 dotenv 本身的能力:
- Next.js:
@next/env包实现了完整的.env.*.local优先级链 - Vite:
loadEnv()实现了同套规则 - 裸 dotenv:只认
.env,到此为止
所以在 Fastify / Express / tsx 脚本 / BullMQ worker 等纯 Node.js 环境里,.env.local 对 dotenv 完全不可见。
Next.js 项目里 .env.local 总是被自动加载,且优先级高于 .env——这是 @next/env 的魔法。一旦你把代码移到独立的 Node.js 进程,这个魔法就消失了,但你很可能不会意识到,因为代码看起来完全一样。
正解
在入口文件最顶部,用两次显式加载替换那个一行版:
// src/index.ts — 必须在所有其他 import 之前
import dotenv from 'dotenv'
import { resolve } from 'path'
import { existsSync } from 'fs'
const localEnv = resolve(process.cwd(), '.env.local')
if (existsSync(localEnv)) dotenv.config({ path: localEnv })
dotenv.config() // .env 作为兜底,已存在的 key 不会被覆盖顺序是关键:dotenv.config() 默认不覆盖已设置的变量,所以先加载 .env.local,其中的值就会赢过 .env 里的同名 key——与 Next.js / Vite 的优先级语义一致。
加完之后加一行快速验证:
console.log('[env] API key loaded =', !!process.env.DEEPSEEK_API_KEY)输出 true 后删掉这行,完事。
先 dotenv.config({ path: '.env.local' }),再 dotenv.config()(无参数加载 .env)。由于 dotenv 默认不覆盖已有 key,先加载的 .env.local 天然拥有更高优先级。
如果项目还需要 .env.development、.env.production 等分环境文件,可以用 dotenv-flow 替代裸 dotenv:
import 'dotenv-flow/config'它完整复刻了 Next.js 的级联语义。但对于只有两个文件的大多数项目,手写两行更简单,也不增加依赖。
一句话外卖
.env.local的自动加载是框架特性,不是 dotenv 特性——从框架迁出的服务必须自己实现这套优先级。