A tiny terminal tool for running small binaries on a Linux box as per-user
systemd services. It wraps systemctl --user, so you get start/stop/restart,
crash auto-restart, reboot persistence, and per-binary log files — no sudo
day-to-day, and no daemon of its own to babysit.
Single static binary. JSON registry. Deliberately minimal.
If you SSH into a Linux server and just want to keep a binary running, the usual answers all carry baggage:
- Writing
.servicefiles by hand is tedious and easy to get wrong. - pm2 / supervisor install their own supervisor daemon and restart logic — redundant on a systemd box, since systemd already does this natively.
- Docker / Compose is great for stacks, but overkill when you have one compiled binary and just want it to stay up.
bm takes the lightest path: it generates systemd user units for you and
gets out of the way. systemd supervises; bm just makes it one command to set
up. No daemon, no containers, no root (after a one-time enable-linger).
When not to use bm: if you need process isolation, reproducible
images, multi-container stacks, or a team-grade deployment pipeline — use
Docker. If you want Node.js cluster mode or language-specific features — use
pm2. bm is for the simple middle: "run this binary, restart it if it dies,
survive reboots, give me logs."
- Daemons —
bm addwrites a user unit withRestart=always(default), so your binary comes back after a crash and at boot. - Scheduled jobs —
--every 1hor--on dailyrenders a oneshot service- a systemd timer (a friendlier cron).
bm runtriggers it on demand.
- a systemd timer (a friendlier cron).
- No sudo day-to-day — runs entirely as your user via
systemctl --user. - Survives logout/reboot — via systemd "lingering" (one-time setup).
- Per-binary logs — stdout/stderr appended to
~/.local/share/bm/logs/<name>.log. - Upsert by name —
bm addthe same name again to update config and restart. - XDG-aware — honours
XDG_CONFIG_HOME/XDG_DATA_HOME.
- A Linux server with systemd (Ubuntu, Debian, Fedora, Arch, …).
Commands that need
systemctlexit gracefully on macOS / WSL —bmis meant to run on the server. - Go only to build it; the result is a standalone binary with no runtime dependency.
From source (produces a bm binary on your PATH):
git clone https://github.com/berkaycubuk/binary-manager
cd binary-manager
make build # or: go build -o bm .
sudo install -m 0755 bm /usr/local/bin/bm # optional, to put it on PATHCross-build on your dev machine and ship to the VPS (the VPS doesn't need Go installed):
# pick the arch that matches your VPS CPU
make install-linux VPS_ARCH=amd64 VPS_HOST=user@your-vps
# then on the VPS: ~/bm setupOr by hand:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -o bm .
scp bm user@your-vps:~/bmbm setup # checks systemd, dirs, and linger
sudo loginctl enable-linger $USER # do this once: survive logout/rebootbm setup will tell you if lingering is off and print the exact command. That
one sudo is the only privilege escalation you'll ever need.
bm add web --exec /usr/bin/python3 -- -m http.server 8000 # register + start
bm list # see everything
bm logs web -f # follow logs
bm remove web # take it down# Register a binary (starts it immediately). Args after '--' go to the binary.
bm add web --exec /usr/bin/python3 -- -m http.server 8000
bm add bot --exec ~/bin/mybot --env TOKEN=secret -- --verbose
bm add job --exec ~/bin/etl --restart on-failure --no-start
# See what you've got
bm list
bm status # all binaries, detailed
bm status web # one binary, detailed
# Lifecycle
bm start bot
bm stop bot
bm restart bot # use this after you scp a new binary in
# Logs (tail-style)
bm logs web
bm logs web -f
bm logs web -n 100
# Remove (stops, disables, deletes unit; leaves your binary + log on disk)
bm remove web| Flag | Default | Description |
|---|---|---|
--exec PATH |
required | Absolute path to the binary (~ is expanded) |
--cwd PATH |
binary's dir | Working directory |
--env KEY=VAL |
— | Environment variable (repeatable) |
--restart POLICY |
always |
always | on-failure | no (daemons only) |
--desc TEXT |
— | Human-readable description |
--every INTERVAL |
— | Run on an interval: 15m, 2h, 1d, 30s |
--on CALENDAR |
— | Run on a systemd calendar: daily, hourly, *-*-* 03:00:00 |
--no-start |
off | Register without (re)starting now |
-- |
— | Everything after -- is passed verbatim to the binary |
Notes:
- Names must be lowercase, start with a letter/number, and use only
[a-z0-9_-](they become part of the systemd unit name) — e.g.web,api-1. --everyand--onare mutually exclusive (pick one schedule per job).- Re-running
bm add <same-name>updates config in place and restarts.
Just replace the file and restart — bm reuses the existing unit:
scp mybot user@your-vps:~/bin/mybot
ssh user@your-vps 'bm restart bot'To change how it runs (args, env, restart policy), run bm add bot ... again
with the same name — it's an upsert.
For binaries that should run periodically instead of staying up (a backup, a
report, a poller), pass --every <interval> or --on <calendar>. bm writes a
oneshot service plus a systemd timer that triggers it:
bm add backup --exec ~/bin/backup --every 1h # interval
bm add report --exec ~/bin/report --on daily # systemd OnCalendar keyword
bm add tick --exec ~/bin/tick --on "*-*-* 03:00:00" # every day at 03:00Scheduled jobs behave slightly differently from daemons:
bm start <name>arms the timer;bm stop <name>disarms it (and stops any running instance).bm list/bm statusshow the next-run countdown.bm run <name>triggers an immediate run now, without touching the schedule — handy for testing a job.bm restart <name>re-arms the timer (picks up a changed schedule).- Scheduled jobs are
Type=oneshot;--restartdoes not apply to them. Missed firings are caught up on boot (Persistent=true), so a daily backup still runs even if the box was off at the scheduled time.
| What | Path |
|---|---|
Registry (your bm add config) |
~/.config/bm/registry.json |
| systemd user units | ~/.config/systemd/user/bm-<name>.service |
| Timer units (scheduled jobs) | ~/.config/systemd/user/bm-<name>.timer |
| Per-binary logs | `~/.local/share/bm/logs/.log |
Honours XDG_CONFIG_HOME / XDG_DATA_HOME if set.
| Command | What it does |
|---|---|
bm setup |
One-time environment check |
bm add <name> |
Register / update a binary (daemon by default; --every/--on for scheduled) |
bm list (ls) |
Table of all binaries + status (incl. next run) |
bm status [name] |
Detailed status (one or all) |
bm start <name> |
Start a daemon, or arm a schedule |
bm stop <name> |
Stop a daemon, or disarm a schedule |
bm restart <name> |
Restart a daemon, or re-arm a schedule |
bm run <name> |
Trigger an immediate run (ignores the schedule) |
bm logs <name> |
Tail the log (-f follow, -n N lines) |
bm remove <name> (rm) |
Stop, disable, delete unit (+timer), drop from registry |
bm version |
Print version |
- Restart policies — daemons default to
Restart=always(good for long-running processes). Use--restart on-failurefor things that should stay stopped after a clean exit, or--restart noto never auto-restart. Scheduled jobs ignore this. - Env vars are stored in plaintext in the generated unit file
(
Environment=KEY=VAL), not in a secret manager. Don't put high-value secrets there; prefer reading from a file your binary loads itself. - Logs are appended, never rotated. For long-running daemons, wire up
logrotate(or have your binary handle its own rotation) to keep them bounded. - Your binary and its log file are never deleted by
bm;removeonly deletes the systemd unit/timer and the registry entry. - Single-user VPS. Multi-user isolation and production-grade hardening are out of scope — use full systemd (or Docker) for that.
MIT — see LICENSE.