Node.js 环境变量与配置管理:别再往代码里写死密钥了
上周做安全自查,在公司某个老仓库的提交历史里翻出了生产数据库的密码。虽然已经改过密码了,但这事提醒我:配置管理这个"低级问题",值得正经写一篇。
底线:密钥永远不进代码库
先立规矩,再谈技术:
- 任何密码、API Key、证书私钥,只存在于环境变量或密钥管理服务里
.env文件必须进.gitignore,第一天就加- 仓库里可以放
.env.example,列出需要哪些变量但值留空,给新人当文档
# .gitignore
.env
.env.local
.env.*.local
# .env.example —— 这个可以提交
DB_HOST=
DB_USER=
DB_PASSWORD=
JWT_SECRET=
dotenv:本地开发的标配
Node 本身不读 .env 文件(20.6 之前),用 dotenv:
// 入口文件最顶上,越早越好
require('dotenv').config()
// 之后 process.env 里就有 .env 文件里的值了
const db = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
})
ESM 项目推荐这个写法,不用污染业务文件:
node --env-file=.env server.js # Node 20.6+ 原生支持,dotenv 都可以省了
一个常见坑:dotenv.config() 必须在所有用到 env 的模块之前执行。如果模块 A 在文件顶部 require 时就读了 process.env.DB_HOST,而 dotenv 在模块 B 里才加载,A 拿到的就是 undefined。所以要么入口第一行加载,要么用 node -r dotenv/config server.js 预加载。
多环境怎么组织
开发、测试、生产三套环境的配置差异,常见三种方案:
方案一:单 .env + 环境变量覆盖(推荐小项目)
服务器上的真实配置通过部署平台注入(PM2 的 ecosystem 文件、Docker 的 -e、K8s 的 ConfigMap/Secret),.env 只在本地开发用:
// ecosystem.config.js(PM2)
module.exports = {
apps: [{
name: 'api',
script: 'server.js',
env_production: {
NODE_ENV: 'production',
DB_HOST: '10.0.1.20',
},
}],
}
方案二:分文件 .env.development / .env.production
按 NODE_ENV 加载不同文件。可用但容易乱,我不推荐——环境多了以后文件泛滥,且容易误提交。
方案三:配置中心(大团队)
Nacos、Apollo、云厂商的 Parameter Store。适合几十上百个微服务的场景,小团队别硬上。
配置校验:启动时就该炸
最怕的不是没配置,而是配置错了服务还能启动,半夜才在某个边缘路径上炸。
启动时校验一把,缺了什么、格式不对,直接拒启动:
// config.js
const required = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'JWT_SECRET']
for (const key of required) {
if (!process.env[key]) {
console.error(`缺少必需的环境变量: ${key}`)
process.exit(1)
}
}
const config = {
db: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: Number(process.env.DB_PORT || 3306),
},
jwtSecret: process.env.JWT_SECRET,
port: Number(process.env.PORT || 3000),
}
// 数字校验:NaN 也拒启动
if (Number.isNaN(config.db.port) || Number.isNaN(config.port)) {
console.error('DB_PORT / PORT 必须是数字')
process.exit(1)
}
// 生产环境的弱密钥警告
if (process.env.NODE_ENV === 'production' && config.jwtSecret.length < 32) {
console.error('生产环境 JWT_SECRET 长度至少 32 位')
process.exit(1)
}
module.exports = config
业务代码全部从 config 模块取配置,不再散落 process.env——这样配置的来源和校验只有一处。
复杂项目可以上 zod 做 schema 校验,类型和默认值一并搞定:
const { z } = require('zod')
const envSchema = z.object({
DB_HOST: z.string().min(1),
DB_PORT: z.coerce.number().default(3306),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
})
const env = envSchema.parse(process.env) // 不合规直接抛错,启动失败
读取时的经典坑
env 值全是字符串:
if (process.env.CACHE_ENABLED) {
// ❌ CACHE_ENABLED=false 也会进这里!"false" 是 truthy 字符串
}
// ✅ 正确姿势
const cacheEnabled = process.env.CACHE_ENABLED === 'true'
不要在客户端代码里用服务端的 env:用打包工具往前端注入变量时(Vite 的 VITE_*、Nuxt 的 public runtimeConfig),带前缀的才会进客户端包。往客户端注入数据库密码的事故真实发生过。
小结
- 密钥只走环境变量,.env 进 .gitignore,.env.example 进仓库
- Node 20.6+ 可以
node --env-file=.env,少一个依赖 - dotenv 必须最先加载,否则模块级读取拿不到值
- 启动时校验配置,错了就拒启动;业务代码统一从 config 模块取
- env 值都是字符串,布尔判断用
=== 'true'
配置管理没有高深技术,全是纪律。但线上事故的半壁江山,恰恰都倒在这些"低级纪律"上。
看到这里了?如果这篇文章帮到过你
请我喝瓶可乐就好 🥤

评论0 条
还没有评论,来抢沙发~