Deploy with Docker
The Busbar image is a FROM scratch container: the static binary and the provider catalog, nothing else (~5 MB compressed). It’s the fastest way to run Busbar on a single host.
1. Pull and verify
Section titled “1. Pull and verify”docker pull getbusbar/busbar:1.5.3Verify the image’s build provenance before you trust it (the attestation binds the image digest to the release workflow):
gh attestation verify oci://index.docker.io/getbusbar/busbar:1.5.3 --repo GetBusbar/busbarThe GHCR mirror (ghcr.io/getbusbar/busbar) additionally carries a cosign signature.
2. Run on a single host
Section titled “2. Run on a single host”First, write a minimal config.yaml (see Getting Started for the full walkthrough and every field):
# config.yaml (dev/minimal, no client auth gate)providers: anthropic: api_key: { env: ANTHROPIC_KEY }models: claude-sonnet: provider: anthropicMount your config.yaml over the one baked into the image. The image ships /etc/busbar/config.yaml and /etc/busbar/providers.yaml already (both are COPY-ed in at build, see the Dockerfile), so bind-mount your file straight onto the existing path:
docker run -d --name busbar \ -p 8080:8080 \ -e ANTHROPIC_KEY \ -v "$PWD/config.yaml:/etc/busbar/config.yaml:ro" \ getbusbar/busbar:1.5.3
curl -s http://localhost:8080/healthz && echo ok-e ANTHROPIC_KEY (bare, no =value) forwards the variable from your host shell: export ANTHROPIC_KEY=sk-ant-... before running, or it reaches the container unset. The baked-in providers.yaml stays in place, so there’s nothing else to fetch. Only mount the whole /etc/busbar directory if you also want to override providers.yaml: a directory bind-mount replaces the entire directory and therefore shadows the image’s baked-in catalog, so in that case put both config.yaml and your own providers.yaml in the mounted directory.
Pin an exact tag (getbusbar/busbar:1.5.3) in production rather than riding latest.
Enabling auth (keys verifier) and minting a key
Section titled “Enabling auth (keys verifier) and minting a key”To require callers to present a signed key instead of running as an open relay, add to config.yaml:
identity-providers: # DEFINE the provider once; `auth.chain` / `auth.admin_auth` reference it BY BARE NAME. admin-tokens: { module: admin-tokens, token: { env: BUSBAR_ADMIN_TOKEN } }
auth: chain: [keys] signing_key: { env: BUSBAR_SIGNING_KEY } admin_auth: [admin-tokens]auth.signing_key is a required, explicit secret reference. Busbar 1.5.3 does not auto-generate one. Generate it once and keep the same value across every restart and every node in a fleet (rotating it revokes every key minted under the old one):
docker run --rm getbusbar/busbar:1.5.3 --generate-signing-keyThen re-run the container with the extra env vars:
docker run -d --name busbar \ -p 8080:8080 \ -e ANTHROPIC_KEY \ -e BUSBAR_SIGNING_KEY \ -e BUSBAR_ADMIN_TOKEN \ -v "$PWD/config.yaml:/etc/busbar/config.yaml:ro" \ getbusbar/busbar:1.5.3Now mint a caller key. The admin API binds to 127.0.0.1:8081 inside the container’s own network namespace, so it is unreachable via -p 8081:8081 (Docker port publishing forwards to the container’s external interface, not its loopback). And the image is FROM scratch (no shell, no package manager, no curl), so there’s nothing inside it for docker exec. Share the container’s network namespace from a throwaway client instead:
docker run --rm --network container:busbar curlimages/curl \ -s -X POST http://127.0.0.1:8081/api/v1/admin/keys \ -H "Authorization: Bearer $BUSBAR_ADMIN_TOKEN" -H "Content-Type: application/json" \ -d '{"name":"quickstart"}'This returns the signed token once (.token in the response). For a permanently reachable admin plane, set admin_listen to a non-loopback address and configure admin_tls (mTLS); see Admin API. Busbar refuses to boot a network-exposed admin bind without it.
3. Docker Compose
Section titled “3. Docker Compose”Grab the reference file (curl -O https://getbusbar.com/compose.yaml) or copy it:
services: busbar: image: getbusbar/busbar:1.5.3 ports: - "8080:8080" environment: # Read from the host env or a .env file; never inline keys here. ANTHROPIC_KEY: ${ANTHROPIC_KEY:-} BUSBAR_ADMIN_TOKEN: ${BUSBAR_ADMIN_TOKEN:-} BUSBAR_SIGNING_KEY: ${BUSBAR_SIGNING_KEY:-} # only if auth.chain includes `keys` volumes: # Mount your config over the image's baked-in one (see step 2). Mount the whole # ./config directory onto /etc/busbar instead only if you also override providers.yaml. - ./config.yaml:/etc/busbar/config.yaml:ro # Durable store only: a writable volume for the sqlite store plugin. - busbar-data:/var/lib/busbar healthcheck: test: ["CMD", "/busbar", "--validate"] interval: 30s timeout: 5s retries: 3 restart: unless-stopped
volumes: busbar-data:Put config.yaml next to compose.yaml, put your secrets in a .env file beside it, then:
docker compose up -dMint a key the same way as the plain docker run case above, substituting the Compose-assigned container name (docker compose ps to find it, typically <project>-busbar-1):
docker run --rm --network container:<project>-busbar-1 curlimages/curl \ -s -X POST http://127.0.0.1:8081/api/v1/admin/keys \ -H "Authorization: Bearer $BUSBAR_ADMIN_TOKEN" -d '{"name":"quickstart"}'- Healthcheck. The Compose
healthcheckabove runs/busbar --validate, which checks that config parses and every plugin manifest is valid. It is not a liveness probe (it passes even if the running server is wedged, and re-runs the full plugin preflight everyinterval). It also does not check that secret references (likesigning_key) actually resolve, so a clean--validatedoes not guarantee a clean boot when a referenced env var is unset. The liveness/readiness signal is the unauthenticatedGET /healthzendpoint on the data port; front a proxy or orchestrator at/healthzfor real health. - Admin plane. The admin API runs on its own listener (
:8081), loopback-only inside the container’s network namespace by default, so it is not reachable by publishing-p 8081:8081(see above). To manage it, publish:8081only behind mTLS (admin_tls); a network-exposed admin bind refuses to boot without it. See the Admin API. - Durable store. With the default
store: memory, governance ledgers are ephemeral per-container and Busbar needs no volume. To persist keys and usage, configure a durable store plugin (e.g.store: { module: sqlite, settings: { db_path: /var/lib/busbar/governance.db } }) and mount a writable volume (busbar-dataabove; the container runs as non-root uid 65532, so the volume must be writable by that uid). The cluster-sharedpostgres/valkeystore plugins need no local volume. - Custom catalog. The image bakes in
/etc/busbar/providers.yaml, so the default runs need no catalog mount. To add providers beyond the shipped catalog, mount your ownproviders.yamlover/etc/busbar/providers.yaml, or mount your whole config directory onto/etc/busbar(which replaces both files) and placeconfig.yaml+providers.yamlin it. - Scaling / HA. With
store: memoryBusbar is stateless: run several containers (Composedeploy.replicas, or multiple hosts) behind a proxy that health-checks/healthz. For shared governance across replicas, use a cluster-shared store plugin (postgres/valkey); note that requests/tokens rate windows are per-process (caps enforce per node), while budgets accrue to the shared store. See Running multiple instances (HA).