# 配置

`bm2.toml` 描述**一个项目**：单个应用，配一个或多个实例。把它放在项目目录里，在该目录执行 `bm2 serve`。

这个文件里的任何内容都不会被执行。没有 shell 字符串，没有脚本钩子，也没有表达式语言——每个值都是数据，bm2 不认识的字段会直接报错，而不是被悄悄忽略。

不连守护进程也能校验：

```bash
bm2 check
```

`bm2 serve` 在登记任何东西之前会跑同一套校验，所以非法配置永远到不了被托管的进程那里。

## 完整模板

```toml
# 项目名：字母开头，后接字母、数字和下划线。
# 同时也是应用名，在所有已注册项目中必须唯一。
name = "api"

# 应用工作目录（绝对路径），默认为 bm2.toml 所在目录。
cwd = "/srv/api"

# 相对 cwd 的脚本路径，禁止 .. 段。
script = "src/index.ts"

# 运行时：bun 或 node。
runtime = "bun"

# 执行模式：cluster（默认，所有实例共享同一个 port，由内核分发
# 连接，应用监听需开启 reusePort）或 fork（端口从 port 起连续分配）。
exec_mode = "cluster"

# 实例数量（1..1024）。
instances = 2
port = 3000

# 单实例内存上限（MiB），超过按异常重启处理。
max_memory_mb = 512

# 连续异常重启预算，0 表示首次异常即 errored。
max_restarts = 10

# 自动重启前的延迟（毫秒），崩溃后固定，spawn 失败后按次数递增。
restart_delay_ms = 1000

# 干净退出早于此时长（毫秒）会计入重启预算。
min_uptime_ms = 10000

# SIGTERM 到 SIGKILL 的宽限期（毫秒），最大 60000。
stop_timeout_ms = 10000

# 日志轮转：单个日志超过 max_size_mb 即轮转，keep 决定保留几代
# 归档。keep = 0 表示代数不限。
[logs]
max_size_mb = 10
keep = 0
```

## 字段

| 字段 | 必填 | 默认 | 约束 |
| --- | --- | --- | --- |
| `name` | 是 | — | 字母开头，后接字母、数字或下划线 |
| `cwd` | 否 | 配置文件所在目录 | 绝对路径 |
| `script` | 是 | — | 相对 `cwd`，禁止 `..` 段 |
| `runtime` | 否 | `bun` | `bun` 或 `node` |
| `exec_mode` | 否 | `cluster` | `cluster` 或 `fork` |
| `instances` | 是 | — | 1 到 1024 |
| `port` | 是 | — | 1 到 65535，且 `port + instances - 1` 也必须落在 1 到 65535 之内 |
| `max_memory_mb` | 否 | 512 | 1 到 1048576 |
| `restart_delay_ms` | 否 | 1000 | 大于等于 0 |
| `max_restarts` | 否 | 10 | 大于等于 0，`0` 表示首次异常即 errored |
| `min_uptime_ms` | 否 | 10000 | 大于等于 0 |
| `stop_timeout_ms` | 否 | 10000 | 1 到 60000 |
| `lang` | 否 | 跟随操作者 | `en` 或 `zh` |
| `[logs] max_size_mb` | 否 | 10 | 1 到 102400 |
| `[logs] keep` | 否 | 0 | 0 到 1000，`0` 表示代数不限 |

`name` 同时是状态目录下的目录名，所以只允许路径中安全的字符。

## 项目语言

`lang` 决定这个项目自己的文案用哪种语言。

它影响的是关于这个项目的消息：`bm2 list <name>` 的表头、针对它的状态提示、它自己的通知文案。

不写就跟随操作者：终端里跟环境，守护进程自己写的日志跟它的环境。

这个字段存在的理由很实际：服务器在别的国家、操作者用中文，是常见的组合。

## 子表

两份子表分别管两件独立的事，各有专页。

`[logs]` 管应用日志的轮转，字段与策略见[状态与日志](/guide/state-and-logs)。

`[notify]` 管事件上报，字段、事件、阈值、周期报告的写法见[通知](/guide/notifications)。

## 端口

默认的 cluster 模式下，所有实例共享同一个 `port`，由内核分发连接。`exec_mode = "fork"` 时端口从 `port` 起连续分配。

所有已注册项目的端口范围不得重叠——cluster 项目只占一个端口槽。冲突时 bm2 拒绝启动，并点名占着那段范围的项目。

## 越界即拒绝，不猜

越界的值会让整份配置失败，而不是被夹取或替换成默认值：

- `logs.max_size_mb = 0` 被拒绝，而不是理解成「永不轮转」。一个静默无限增长的日志，比让你写清大小更糟。
- `logs.keep` 为负被拒绝，因为 `0` 已经表示不限代数。
- 两个项目不能共用同一份配置文件——一份配置文件就是一个项目。

## `__VAR__` 占位符

文件里的字符串可以引用环境变量，标记是 `__NAME__`，写在引号里：

```toml
mail_password = "__BM2_MAIL_PASSWORD__"
port = "__APP_PORT__"
max_memory_mb = "__APP_MEM_MB__"

[notify]
enabled = "__NOTIFY_ON__"
```

标记必须待在引号里：不带引号的 `__APP_PORT__` 不是 TOML 的值，文件当场就不成立了。数字与布尔字段也写引号里的标记，展开之后按字段类型解析，解析不了就报错并点名字段——`port = "__APP_PORT__"` 而 `APP_PORT=abc` 不会带着一个坏端口进入托管。

展开发生在解析之后，跑在解析出的字符串值上，所以注释里的 `__NAME__` 只是散文，不会因为变量没设而报错。

变量未设置是硬错误并点名该变量，绝不静默空值——一个空 URL 会变成一次真实的投递尝试。

通知凭据正是它存在的理由：标记留在仓库里，真正的密文留在 `.env` 文件里，或解析配置的那个进程的环境里。
