跪拜 Guibai
← Back to the summary

bm2: A 5.5 MB MoonBit Process Manager That Runs Bun and Node.js with 1 ms Command Latency

bm2

bm2 is a lightweight Linux process manager written in MoonBit for managing Bun and Node.js applications.

bm2 consists of a command-line tool and a background daemon. The CLI sends commands over a Unix socket, and the daemon continuously hosts application processes.

Each user gets one independent daemon that can manage multiple projects simultaneously.

Hosting capabilities include multi-instance, automatic crash restart, memory limits, graceful stop, and state persistence.

Reverse proxying and load balancing are left to gateways like Nginx or Caddy; bm2 focuses solely on process hosting.

English documentation: README.en.md

bm2 vs pm2

Comparison bm2 pm2
Form Native static binary Node.js application
Extra dependencies None Node.js runtime
Size ~`5.5 MB` (two binaries) ~`23 MB`
File count 2 3036
Idle daemon memory ~`2.6 MB` ~`50 MB`
Command response ~`1 ms` ~`200–400 ms`
Log rotation Built-in, 10 MB × 10 generations Requires extra pm2-logrotate module

Features

It does not manage Nginx, domains, certificates, hot reload, auto-start on boot, or remote management.

Environment requirements

Installation and upgrade

Install the MoonBit toolchain first:

curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash

Then install bm2:

moon install chensuiyi/bm2/...

bm2 installs by default into ~/.moon/bin alongside the moon toolchain, requiring no PATH configuration.

Both bm2 and bm2d binaries are installed together.

Both must stay on PATH because bm2 launches bm2d by name.

Upgrade to the latest version on mooncakes; the new daemon is automatically swapped in after installation:

bm2 upgrade          # compares versions, runs moon install, and automatically swaps in the new daemon

Configuration parameters

Create bm2.toml in the directory where you run bm2.

Below is the full template with all fields and defaults; copy and modify directly:

# Project name: starts with a letter, followed by letters, digits, and underscores.
# Also serves as the application name and must be unique among all registered projects.
name = "api"

# Application working directory (absolute path), defaults to the directory containing bm2.toml.
cwd = "/srv/api"

# Script path relative to cwd; `..` segments are forbidden.
script = "src/index.ts"

# Runtime: bun or node.
runtime = "bun"

# Number of instances (1..1024); ports are assigned consecutively starting from `port`.
instances = 2
port = 3000

# Per-instance memory limit (MiB); exceeding it is treated as an abnormal restart. Minimum 1.
max_memory_mb = 512

# Consecutive abnormal restart budget; 0 means first abnormal exit puts the instance into errored state.
max_restarts = 10

# Delay before auto-restart (ms); fixed after a crash, increments per attempt after a spawn failure.
restart_delay_ms = 1000

# Clean exit earlier than this duration (ms) counts toward the restart budget.
min_uptime_ms = 10000

# Grace period from SIGTERM to SIGKILL (ms), maximum 60000.
stop_timeout_ms = 10000

Port ranges of all registered projects must not overlap; bm2 refuses to start on conflict.

Environment variables

bm2 passes only its own PATH, HOME, and TMPDIR to managed processes, plus these reserved variables:

Port and instance number correspond one-to-one: BM2_APP_PORT = port + BM2_APP_INSTANCE. For example, with port = 3000 and instances = 3, the three instances listen on 3000, 3001, and 3002 respectively.

Typical in-app usage:

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) {
  // Run only on the primary instance: DB migrations, cron jobs, etc.
}

The application's own environment variables are loaded by the app and its runtime; bm2 does not parse .env or participate in loading.

Variables already injected by bm2 cannot be overwritten by the runtime's .env loading.

Command usage

bm2 start             # register/update the project in the current directory and start it
bm2 kill <name>       # stop a project and deregister it; bm2d keeps running
bm2 kill -y           # stop all projects, deregister them, and exit bm2d (bare `kill` refuses to run)
bm2 list [name]       # show status of all registered projects
bm2 reload            # swap in a new bm2d; managed applications keep running
bm2 upgrade           # upgrade bm2 to the latest version on mooncakes
bm2 version           # show bm2 version

start and kill <name> are asynchronous: the daemon responds immediately, and the CLI polls until the operation completes, so the daemon never blocks on a stop timeout.

bm2 list, bm2 kill, bm2 reload, and bm2 version can be run from any directory.

Only bm2 start must be run in the directory containing bm2.toml, because it registers the project from that configuration.

A bare bm2 kill (without -y) refuses to execute and prints a hint.

Re-running bm2 start in a project (or in another directory with the same name) updates the configuration and performs a full restart, so changing any field — including instance count, port, or script — takes effect on the next start.

A killed project (bm2 kill <name>) is fully deregistered: it disappears from bm2 list and does not reappear on daemon restart.

reload swaps in a new bm2d without stopping managed applications: the old daemon detaches, and the new daemon adopts the still-running instances under an unchanged PID.

Use this after manually replacing the binary; bm2 upgrade performs this step automatically.

list prints one line per active or errored instance, including PID, port, running status, memory, uptime, and the full project working directory in the last column CWD.

Intentionally stopped instances are omitted; restarting and errored instances remain visible for operational diagnosis.

If the daemon crashes unexpectedly and leaves a stale Unix socket, the next CLI request briefly waits for a response, removes the stale socket, starts a new daemon, and retries the request once.

Load balancing

bm2 focuses solely on process hosting; reverse proxying and load balancing are left to gateways like Nginx or Caddy.

Point the gateway at the instances' consecutive ports, then update the upstream list and reload the gateway when instances are added or removed.

Nginx example (assuming instances = 2, port = 3000):

upstream bm2_app {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://bm2_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Caddy example:

example.com {
    reverse_proxy 127.0.0.1:3000 127.0.0.1:3001
}

State and logs

bm2 always stores its socket, PID, state, and management logs under the current Linux user's ~/.bm2.

One daemon per user, managing all registered projects:

bm2.sock                         # Unix socket, permissions 0600
bm2d.pid                         # daemon PID
bm2.events.jsonl                 # CLI connection and retry events
bm2d.log                         # daemon stderr / runtime diagnostics
bm2d.events.jsonl                # daemon and supervision events
<name>/project.json              # per-project registration info (config path)
<name>/<name>-<id>.json          # persisted instance state
<name>/logs/<name>-<id>.out.log    # application stdout
<name>/logs/<name>-<id>.error.log  # application stderr
<name>/logs/<name>-<id>.crash.log  # abnormal exit diagnostics

Logs rotate by size: each file rotates when it reaches 10 MB, keeping ten generations (.1 .. .10, up to ~100 MB per file).

Application logs and bm2's own management logs are strictly separated.

The two *.events.jsonl files contain one JSON object per line.

They record only management metadata: timestamp, event name, and when applicable the app/instance/PID and operational reason.

They do not contain environment variable values, protocol payloads, or application output.

Common commands:

tail -f ~/.bm2/bm2d.events.jsonl
jq -c . ~/.bm2/bm2d.events.jsonl
Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

亚雷

Deployment boundaries are clear

前端之虎陈随易

[Fist salute]