Self-host BitRouter
A production walkthrough for running self-hosted BitRouter — config file, keys, daemon lifecycle, metrics, and hardening.
Self-host BitRouter
This is the production path for running BitRouter on your own infrastructure: a committed config file, real provider keys, the router running as a managed daemon, metrics export, and basic hardening. If you just want it running in 60 seconds, start with Installation — this guide picks up where that leaves off. Deciding between self-host and the hosted product? See Self-host vs Cloud.
The router listens on 127.0.0.1:4356 by default — loopback only, until you
explicitly choose otherwise.
1. Generate and structure bitrouter.yaml
Scaffold a commented starter file:
bitrouter init # writes ./bitrouter.yaml
bitrouter init -c /etc/bitrouter/bitrouter.yamlbitrouter init writes a starter config with skip_auth: true. Edit it to
configure providers, routing, and the rest. Treat bitrouter.yaml as
infrastructure-as-code: commit it, review changes, and keep secrets out of it
(use ${VAR} references, resolved from the environment at load time).
The config is keyed into top-level sections — the ones you'll touch first are
server, providers, and models:
# yaml-language-server: $schema=https://bitrouter.dev/schema/v<VERSION>/config.schema.json
server:
# Loopback by default; set 0.0.0.0 only when you intend to expose the router
# on all interfaces.
listen: 127.0.0.1:4356
log_level: info
providers:
openai:
api_base: https://api.openai.com/v1
api_key: ${OPENAI_API_KEY}
models:
- id: gpt-4o
anthropic:
api_base: https://api.anthropic.com
api_key: ${ANTHROPIC_API_KEY}
# `api_protocol` is a glob-prefix pattern list: the head of each set is the
# preferred outbound protocol.
api_protocol:
- "*": messages
models:
- id: claude-sonnet-4-6
# A virtual model that fails over from one provider to another in declared
# order (the default `priority` strategy).
models:
smart:
strategy: priority
endpoints:
- provider: anthropic
service_id: claude-sonnet-4-6
- provider: openai
service_id: gpt-4oStructure that matters:
providersis a map keyed by provider id (openai,anthropic, …). Each entry takesapi_base(upstream base URL),api_key(usually a${VAR}reference), an optionalapi_protocolpattern list, and amodelslist whose entries each require anid.api_protocolselects the outbound wire protocol per provider — e.g.messagesfor Anthropic. Known values includechat_completions,messages,generate_content, andresponses.modelsdeclares virtual models: named aliases with astrategy(defaultpriority) and an ordered list ofendpoints, each pointing at aprovider+service_id. This is how you get failover.servercarrieslisten,log_level, an optionalskip_auth, and an optionalcontrol_socketpath.
The full set of top-level sections is server, providers, models,
presets, variants, mcp, mcp_servers, agents, server_tools,
database, plugins, and inherit_defaults. The authoritative schema lives at
schemas/bitrouter.config.schema.json in the core repo, and the
# yaml-language-server header gives editors autocomplete and inline validation.
Validate before you ship:
bitrouter config validate -c bitrouter.yaml2. Environment and keys
Secrets stay in the environment, never in the committed file. The ${VAR}
placeholders in bitrouter.yaml are resolved at load time:
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...In production, deliver these through your process manager's environment (a systemd
EnvironmentFile, a secrets mount, etc.) rather than a shell profile. When you
rotate a key, you don't need to restart — bitrouter reload forwards any provider
API keys present in the current environment to the running daemon, so
export OPENAI_API_KEY=…; bitrouter reload takes effect immediately.
bitrouter init writes skip_auth: true, which admits credential-less requests.
That is fine for local development behind loopback, but for a production
deployment that is reachable by anything other than localhost you must put
authentication in front of the router (a reverse proxy / gateway, or a
deployment that implements its own auth hook). Do not expose a skip_auth: true
router on 0.0.0.0 without an auth layer.
3. Run as a daemon
For production you want the router running detached and supervised. The daemon lifecycle commands (verified against the CLI reference):
bitrouter start # spawn `serve` as a detached background daemon
bitrouter status # pid, listen address, routable model count, socket path
bitrouter reload # hot-reload config + routing table (also via SIGHUP)
bitrouter restart # drain in-flight requests (up to 30s), then start fresh
bitrouter stop # stop the daemonbitrouter serveruns the server in the foreground (logs to stdout) — this is what you point a systemd unit or container entrypoint at.bitrouter startspawns a detached daemon and refuses to start if one is already running. Logs default tobitrouter.lognext to the config file.bitrouter reloadhot-reloads config and the routing table without dropping connections.
All commands accept -c / --config <path>; the daemon-control commands
(stop, reload, status) also accept --socket <path> to override the Unix
control socket. Under a process supervisor, prefer bitrouter serve as the
foreground entrypoint and let the supervisor handle restarts:
# /etc/systemd/system/bitrouter.service
[Service]
ExecStart=/usr/local/bin/bitrouter serve -c /etc/bitrouter/bitrouter.yaml
EnvironmentFile=/etc/bitrouter/bitrouter.env
Restart=on-failure4. Expose metrics
BitRouter exports observability per request and to standard backends. Self-hosted metrics flow through two surfaces:
- A
GET /metricsendpoint, served when a Prometheus-style metrics renderer is wired into the router (the observability plugin wires one). Point Prometheus at it like any scrape target. - OTLP export of traces and metrics to any OpenTelemetry backend, provided by
the
bitrouter-observeplugin (OTLP/HTTP or OTLP/gRPC).
You can also query spend straight from the CLI without standing up a dashboard, and trace how a model name resolves before it hits an upstream:
bitrouter route gpt-4o # print the full fallback chain for a modelSee Observability for the full export and attribution model.
5. Basic hardening
- Bind deliberately. Keep
listen: 127.0.0.1:4356unless you have a reason to expose it. If you set0.0.0.0, put a reverse proxy with TLS and auth in front. - Don't ship
skip_auth: trueon any non-loopback deployment (see §2). - Keep secrets in the environment. Only
${VAR}references belong in the committedbitrouter.yaml; the config redactsapi_keyin debug output. - Validate in CI. Run
bitrouter config validate -c bitrouter.yamlon every change, and pin the# yaml-language-serverschema URL to your version. - Add a content firewall with Guardrails to block or redact request/response content.
- Reload, don't restart, for config changes so in-flight requests aren't dropped.
Next steps
How is this guide?