Skip to content

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.

Terminal window
docker pull getbusbar/busbar:1.5.3

Verify the image’s build provenance before you trust it (the attestation binds the image digest to the release workflow):

Terminal window
gh attestation verify oci://index.docker.io/getbusbar/busbar:1.5.3 --repo GetBusbar/busbar

The GHCR mirror (ghcr.io/getbusbar/busbar) additionally carries a cosign signature.

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: anthropic

Mount 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:

Terminal window
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):

Terminal window
docker run --rm getbusbar/busbar:1.5.3 --generate-signing-key

Then re-run the container with the extra env vars:

Terminal window
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.3

Now 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:

Terminal window
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.

Grab the reference file (curl -O https://getbusbar.com/compose.yaml) or copy it:

compose.yaml
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:

Terminal window
docker compose up -d

Mint 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):

Terminal window
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 healthcheck above 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 every interval). It also does not check that secret references (like signing_key) actually resolve, so a clean --validate does not guarantee a clean boot when a referenced env var is unset. The liveness/readiness signal is the unauthenticated GET /healthz endpoint on the data port; front a proxy or orchestrator at /healthz for 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 :8081 only 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-data above; the container runs as non-root uid 65532, so the volume must be writable by that uid). The cluster-shared postgres / valkey store 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 own providers.yaml over /etc/busbar/providers.yaml, or mount your whole config directory onto /etc/busbar (which replaces both files) and place config.yaml + providers.yaml in it.
  • Scaling / HA. With store: memory Busbar is stateless: run several containers (Compose deploy.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).