环境变量
一个来源:.env 文件
项目的环境变量只有一个来源:与 bm2.toml 并排的 .env 文件。bm2.toml 只消费它们,不定义变量。
启动时扫描四个文件,从低到高,后者覆盖前者:
.env.env.<模式>.env.local.env.<模式>.local
<模式> 由命令决定,只有两种取值:bm2 serve 是 production,bm2 dev 是 development。
文件不存在是正常的,四个里通常只有一两个。存在却读不了、或者写错了则是硬错误,报错会点名文件和行号。
解析规则跟随 Node 生态的事实标准 dotenv:支持 export 前缀、单双引号、双引号内的 \n 转义、# 注释(# 前有空白才算行尾注释,所以 a#b 是值)。
.env 文件里不做 __VAR__ 展开,展开是 bm2.toml 的能力,不是 .env 的。
# .env
MYSQL_HOST=127.0.0.1
# .env.production
MYSQL_HOST=db.internal
LOG_LEVEL=warnbun 自己也会自动加载这些文件。bm2 先读一遍再注入,是为了让 bun 与 node 两种运行时行为一致,而不是各自依赖运行时的实现——node 默认并不加载它们。
注入顺序
从低到高,后者覆盖前者:
- 白名单变量的当前值(
PATH、HOME、TMPDIR) - 扫描出来的项目环境
- 保留变量(
BM2_*、NODE_ENV)——永远覆盖不了
顺序不随文件书写顺序变化。保留变量排最后,因为实例的自我描述不应该由一个应用的配置文件改写。
只传三个白名单变量
除了保留变量和扫描出来的项目环境,bm2 只把自己环境里的 PATH、HOME、TMPDIR 传给被托管的进程。你 shell 里的其它变量不会跟着进去——应用拿到的是最小环境,而不是你终端里的一整套。
保留变量
| 变量 | 值 |
|---|---|
BM2_APP_NAME | 项目名 |
BM2_INSTANCE_ID | 实例编号,第一个实例为 "0" |
BM2_APP_INSTANCE | 同实例编号,命名对齐 PM2 的 NODE_APP_INSTANCE 习惯 |
BM2_APP_PORT | 分配给该实例的端口 |
NODE_ENV | 由命令决定:bm2 serve 是 production,bm2 dev 是 development |
BM2_APP_PORT 的取值取决于执行模式:cluster(默认)下恒为配置里的 port;fork 模式下等于 port 加实例编号,所以 port = 3000、instances = 3 时三个实例分别拿到 3000、3001、3002。
NODE_ENV 归在保留变量里,因为模式是人的输入,不是应用的产出。.env 文件里可以把它写成同一个值(很多 .env.production 里就有那一行),写成别的值会被当场拒绝——否则"我在哪个环境"就取决于哪份文件最后被读到。
__VAR__ 占位符读同一份项目环境
bm2.toml 里的 __VAR__ 先查扫描出来的项目环境,再查 bm2 进程自己的环境。端点、密钥这些就不必写进 bm2.toml,也不会跟着仓库走。
name = "api"
script = "__APP_ENTRY__"
instances = 2
port = "__APP_PORT__"
[notify]
mail_url = "smtp://__MYSQL_HOST__:25"标记写在引号里,因为那是 TOML 唯一允许任意文本待着的地方。不带引号的 __APP_PORT__ 不是 TOML 的值,配置文件当场就不成立了——配置文件首先要是一份合法的 TOML,其次才是 bm2 的配置。
用两个下划线而不像别处那样用 $,是因为 $ 在 URL 和密码里很常见,凭据里一个手滑的大括号就可能被当成占位符。__NAME__ 这个形状几乎不会碰巧出现。
数字与布尔字段也写引号里的标记,展开后按字段类型解析:
port = "__APP_PORT__"
max_memory_mb = "__APP_MEM_MB__"
[notify]
enabled = "__NOTIFY_ON__"展开发生在解析之后,跑在解析出来的字符串值上。所以注释天然不会被碰——注释根本不在解析结果里,# 端口见 __APP_PORT__ 只是散文。也正因为如此,这个环节不可能把一份合法文档变成非法文档。
没有配对下划线的 a__b 是普通文本,不是标记。变量没设是硬错误并点名它,绝不静默空值——一个空 URL 会变成一次真实的投递尝试。
同一个值既解析进配置,也注入给应用,所以应用和管理者不可能对同一个名字读出不同的值。两边都找不到该变量时报错并点名它,绝不静默变成空值——空 URL 不该变成一次投递尝试。
两套模式,同一套监督
bm2 serve 与 bm2 dev 只决定读哪些 .env 文件,监督行为完全相同:崩溃重启、内存上限、重启风暴预算、通知上报一视同仁。调试时的行为必须和线上一致,否则调试本身就没有意义。
实例数不一样:开发模式只跑一个实例,配置里的 instances 在 dev 下不生效。开发会话就是一个进程监听一个端口,多实例在那里没有用,而日志混在一起只会更难读。
模式记在项目的注册记录里,所以 bm2 list 看得到,bm2 reload 也不会把一个 dev 项目悄悄变回 production。
应用里的典型用法
- 用
BM2_APP_PORT绑定监听端口。 - 用
BM2_APP_INSTANCE === "0"判断主实例,只在主实例跑数据库迁移、定时任务这类一次性逻辑。 - 用
NODE_ENV区分构建产物与调试开关。
const PORT = Number(process.env.BM2_APP_PORT ?? 3000);
const isPrimary = process.env.BM2_APP_INSTANCE === "0";
Bun.serve({ port: PORT, fetch: () => new Response("ok") });
if (isPrimary) {
// 只在主实例执行:数据库迁移、定时任务等
}