Run as a service
serve vs start, a systemd unit that gets the details right, log handling, the control socket, and the pid-file guard.
There are two ways to run the router, and picking the wrong one under a supervisor produces a service that looks healthy and isn't.
| Command | Behaviour | Use it for |
|---|---|---|
bitrouter serve | Runs in the foreground, logging to stdout | Under a process supervisor — systemd, or any orchestrator |
bitrouter start | Spawns serve as a detached daemon, writes a pid file | Workstations, and hosts with no supervisor |
Both load the config, run any pending database migrations, and serve the HTTP API plus the Unix control socket.
Never point a supervisor at bitrouter start. It forks and exits immediately, so systemd sees the launcher die and — under the default Type=simple — either declares the unit failed or kills the process group it thinks it owns. Use bitrouter serve as the ExecStart and let the supervisor own the process lifecycle.
A systemd unit
# /etc/systemd/system/bitrouter.service
[Unit]
Description=BitRouter
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/bitrouter serve -c /etc/bitrouter/bitrouter.yaml
EnvironmentFile=/etc/bitrouter/bitrouter.env
Restart=on-failure
RestartSec=2
User=bitrouter
Group=bitrouter
StateDirectory=bitrouter
# The daemon needs no privileges beyond its own state directory.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/etc/bitrouter /var/lib/bitrouter
[Install]
WantedBy=multi-user.targetPoints that matter:
-cis not optional. The supervisor's working directory is not yours; without an explicit path, config resolution can fall through to zero-config and serve a router with none of your policy. See the resolution chain.EnvironmentFileholds the provider keys, mode0600, owned by the service user. It is not the config file and should not be committed.ReadWritePathsmust include wherever the daemon writes. Because the daemon chdirs into the config's directory, a relativedatabase.url, the control socket, the pid file, andpolicy-lock.yamlall land next tobitrouter.yaml. WithProtectSystem=strict, that directory must be writable or startup fails. If you'd rather keep/etcread-only, give the database an absolute path under/var/lib/bitrouter.Restart=on-failure, notalways. A config error should stop the unit and page you, not loop forever against a file that will never parse.
Then:
systemctl daemon-reload
systemctl enable --now bitrouter
systemctl status bitrouterLogs
Under serve, logs go to stdout and journald captures them.
Verbosity comes from RUST_LOG, not from the config file. The daemon builds its log filter from the standard RUST_LOG environment variable and falls back to info when it's unset, so this is the knob:
# in /etc/bitrouter/bitrouter.env
RUST_LOG=infoIt takes the full tracing filter grammar, so you can raise one module without drowning in everything else — RUST_LOG=info,bitrouter=debug.
server.log_level in bitrouter.yaml is inert in this version. The field exists in the config schema and validates fine, but nothing reads it — setting log_level: debug there changes nothing. Use RUST_LOG.
Under start, the daemon is detached and redirects stdout/stderr to a file. It defaults to bitrouter.log inside the config file's directory — alongside the socket and pid file, not in whatever directory you launched from. Override it:
bitrouter start -c /etc/bitrouter/bitrouter.yaml --log /var/log/bitrouter/bitrouter.logThere's no built-in rotation. If you use start on a long-lived host, hand the file to logrotate with copytruncate, or use serve under a supervisor and let the journal handle it.
RUST_LOG=debug is safe to reach for during an incident — api_key values are redacted in debug output. It's verbose enough to matter on a busy host, though, and because the filter is read at process start you need a restart to change it either way.
The control socket
Every daemon-control command — stop, restart, reload, status, observe status, route — reaches the daemon over a Unix domain socket, not over HTTP. It defaults to ./bitrouter.sock, resolved against the config's directory:
server:
control_socket: "/run/bitrouter/bitrouter.sock"Two operational consequences:
- Control commands need the same
-cas the daemon, because that's how they locate the socket.bitrouter statuswith no-con a host whose config lives in/etc/bitrouterwill look in the wrong place and reportrunning: nofor a perfectly healthy daemon. Pass-c, or--socketto name the path directly. - Filesystem permissions on the socket are the access control for daemon administration. Anyone who can write to it can stop your router. Keep it in a directory owned by the service user.
The pid-file guard
bitrouter start writes a pid file derived from the control-socket path, and refuses to start when a live daemon already holds it:
bitrouter is already running (pid 4711); use `restart` or `stop` firstA stale pid file — left behind by a killed process — is detected and cleaned up rather than blocking startup.
This guard is per pid-file path, which means per host. It is not a distributed lock: two hosts, or two configs on one host with different socket paths, will happily run at once. That's the intended behaviour for separate deployments; see State & backups before pointing two daemons at one database.
Stopping and restarting
bitrouter stop -c /etc/bitrouter/bitrouter.yaml
bitrouter restart -c /etc/bitrouter/bitrouter.yamlrestart drains in-flight requests before the replacement takes over, waiting up to 30 seconds for the endpoint to be released; past that it escalates to a force-kill against the pid file. Long agentic streams can exceed that window, so treat restart as "brief interruption possible" and prefer bitrouter reload for anything a reload can express — see Day-2 operations.
Under systemd, systemctl restart bitrouter is the equivalent and the one to use — going around the supervisor with bitrouter stop just makes the unit restart the process anyway.
How is this guide?
Production configuration
Commit bitrouter.yaml as infrastructure-as-code — config resolution, secrets via environment references, the working-directory rule, and CI validation.
Networking & TLS
Bind address, the endpoints the daemon serves, a streaming-safe reverse proxy with TLS, and the upstream timeouts that decide when a long stream dies.