Node.js 中 dotenv、pm2 与 process.env 的正确使用姿势
------为什么 pm2 能读到 env,而 node xxx.js 读不到?
这是一个我在真实项目中踩到的坑:
同一份代码,用 pm2 启动一切正常,但直接node xxx.js就报 MySQLAccess denied for user ''。最终定位到:dotenv 并没有在当前 Node 进程中执行。
一、问题现象
报错信息
text
Access denied for user ''@'localhost' (using password: NO)
从报错可以看出两个关键点:
- MySQL 用户名是空字符串:
user '' - 没有使用密码:
using password: NO
但 .env 明明已经写了:
env
DB_USER=root
DB_PASSWORD=xxxx
而且:
- 用 pm2 启动服务一切正常
- 用
node testApi/deleteUser.js就直接报错
二、核心结论(一句话版)
.env只是一个文件,不是环境变量
dotenv 只会在"当前 Node 进程"里生效一次
所以:
- pm2 启动的进程 👉 一开始就有 env
- 手动
node xxx.js👉 没有 dotenv,就没有 env
三、dotenv 到底做了什么?
js
require('dotenv').config()
这行代码的作用只有一个:
把
.env文件的内容注入到process.env
它不会:
- 自动全局生效
- 自动影响其他 Node 进程
- 自动影响你之后手动
node xxx.js启动的脚本
四、为什么 pm2 能读到 env,而 node 不行?
1️⃣ pm2 的常见行为
pm2 通常通过以下方式之一提供环境变量:
ecosystem.config.js
js
env: {
DB_USER: 'root',
DB_PASSWORD: 'xxx'
}
shell 环境变量
bash
export DB_USER=root
pm2 start index.js
pm2 启动目录中的 .env
👉 pm2 启动 Node 进程时,process.env 已经被填充
2️⃣ 手动 node 启动的情况
bash
node testApi/deleteUser.js
如果这个文件里:
- ❌ 没有
require('dotenv').config() - ❌ shell 里也没有 export env
那么:
js
process.env.DB_USER === undefined
process.env.DB_PASSWORD === undefined
最终 MySQL 连接配置就会变成:
js
user: ''
password: undefined
五、dotenv 写在哪才是"正确的"?
✅ 正确原则(非常重要)
dotenv 只应该写在「进程入口文件」里
什么是"入口文件"?
任何一个你可能直接运行的文件:
bash
node index.js
node testApi/deleteUser.js
node scripts/migrate.js
这些 都是入口。
六、正确示例
✅ 示例 1:最简单、安全(推荐)
js
// testApi/deleteUser.js
require('dotenv').config()
const { deleteById } = require('../repos/userRepo')
deleteById(1)
js
// repos/userRepo.js
// 不要写 dotenv
console.log(process.env.DB_USER) // 可以正常读取
✅ 示例 2:统一 bootstrap(工程级推荐)
js
// bootstrap.js
require('dotenv').config()
js
// index.js
require('./bootstrap')
require('./app')
js
// testApi/deleteUser.js
require('../bootstrap')
require('../repos/userRepo')
👉 所有入口统一初始化环境
七、为什么不要在 export 文件里写 dotenv?
❌ 错误示例:
js
// repos/userRepo.js
require('dotenv').config() // ❌ 不推荐
原因:
- 模块职责不清晰
- 被 require 时机不可控
- 单测 / 脚本 / CLI 混乱
- 容易造成"有时能读到、有时读不到"
八、一个非常实用的防坑技巧(强烈建议)
在创建数据库连接前,直接硬失败:
js
const must = (key) => {
const val = process.env[key]
if (!val) throw new Error(`${key} is missing`)
return val
}
mysql.createPool({
host: must('DB_HOST'),
user: must('DB_USER'),
password: must('DB_PASSWORD'),
database: must('DB_NAME'),
})
👉 以后不会再看到:
text
user ''
using password: NO
九、最终总结(工程级结论)
✅ dotenv 只影响当前 Node 进程
✅ pm2 ≠ node
✅ dotenv 只属于「入口文件」
❌ export 的模块里不要写 dotenv
记住一句话就够了:
"dotenv 是进程初始化的一部分,不是业务逻辑的一部分。"