Operations
Health probes, secrets, data lifecycle, backups, and shutdown.
Operational runbook for running Bifrost in production: health probes, rotating secrets, managing operation retention, backups, and graceful shutdown. If you are still getting the gateway running locally, start with the Quickstart instead — this page assumes a real deployment. For observability (logs, telemetry, request/usage queries) see Observability; for pricing and spend, see Cost accounting.
Health and lifecycle
For how to deploy — Docker Compose, Coolify, Portainer, Dokploy, or a Linux VPS — see Deployment. This section covers the runtime behavior you wire into your orchestrator.
The app runs TypeScript directly on Bun — there is no build step. Run
bun run --filter @boelabs/bifrost db:migrate as a one-off before rolling out new
instances; migrations are generated from schema.ts with drizzle-kit and are forward-only — never
edit an already-applied migration. See Upgrades for the full rollout/rollback story.
Health probes — two probes with different jobs; wire each to the matching orchestrator probe:
GET /health/live— liveness. Always200while the process responds; does not touch Postgres/Redis. Use it for the liveness probe and the container healthcheck. Never point a liveness probe at a dependency-aware endpoint: a Postgres/Redis blip would otherwise restart every replica at once (a restart cannot fix the dependency) and turn a blip into an outage.GET /health/ready— readiness.200when Postgres, Redis and the extension runtime are healthy; otherwise503withRetry-After. Use it for the readiness probe so an unhealthy instance is pulled from the load balancer without being restarted, and rejoins automatically once dependencies recover.- During a dependency outage, in-flight inference requests return
503+Retry-After(not an opaque500), so well-behaved clients back off and retry.
Shutdown — the process handles SIGTERM/SIGINT, stops accepting traffic, drains in-flight HTTP,
flushes the bounded operation-finalization queue, then closes Redis/Postgres and finally OpenTelemetry.
Give the container at least SHUTDOWN_TIMEOUT_MS to drain.
Scaling — replicas are safe to run concurrently. Maintenance deletes are idempotent, due work is claimed transactionally, and shared router/rate-limit state lives in Redis rather than process memory.
Secrets
Store the root credential and encryption keyring in your secret manager (not in the image or git):
| Secret | Purpose | Format |
|---|---|---|
MASTER_KEY | Root admin credential; grants full access to /admin/*. | Strong random string, at least 32 characters. |
ENCRYPTION_KEYRING | Keys accepted for purpose-bound AES-256-GCM envelopes. | JSON object mapping stable key ids to 64-character hex keys. |
ACTIVE_ENCRYPTION_KEY_ID | Key id used for every new encrypted value. | A key present in ENCRYPTION_KEYRING. |
Generate values:
# MASTER_KEY
openssl rand -base64 48
# Generate one value for each ENCRYPTION_KEYRING entry
openssl rand -hex 32See Security for what each one actually protects and the redaction guarantees around it.
Rotating MASTER_KEY
The master key is a single static secret read from the environment. It is not stored in the database, so rotation is a deploy-time operation:
- Generate a new strong value.
- Update
MASTER_KEYin your secret manager. - Roll the deployment so every instance picks up the new value.
- Update any admin tooling/CI that authenticated with the old key.
Virtual keys are unaffected — they live in the database and are not derived from the master key.
Rotating the encryption keyring
Every envelope records its key id and purpose; new writes always use ACTIVE_ENCRYPTION_KEY_ID while
reads accept every id in ENCRYPTION_KEYRING. Rotation therefore requires no plaintext credential
export and no downtime:
- Add a new random id/value to
ENCRYPTION_KEYRING, leaving the current active id in place, and roll every replica so all can read both keys. - Set
ACTIVE_ENCRYPTION_KEY_IDto the new id and roll the replicas again. - Run
bun run --filter @boelabs/bifrost encryption:rotate. It re-encrypts deployments, extension artifacts, and retained payload samples in bounded batches and reports row counts. - Run it again; every reported count must be zero.
- Keep the old key for at least 14 days so stateless Responses compaction capsules issued before the rotation remain readable, then remove it and roll once more.
Leaked-secret response
If MASTER_KEY leaks: rotate it immediately (above) and audit gateway_operations, payload-access
audit logs, and admin access. If a
provider API key leaks, revoke it at the provider and PATCH the affected deployments with a new
key.
Operation retention
gateway_operations and upstream_attempts are the current operational source and expire after
OBSERVABILITY_METADATA_RETENTION_DAYS. Encrypted payload_samples expire after
OBSERVABILITY_PAYLOAD_RETENTION_DAYS; an in-process maintenance job
also reconciles stale in-progress operations and attempts as abandoned. Operation metadata never
stores full request or response bodies; bounded forensic samples are separately encrypted and expired.
response_states GC
Expired /v1/responses state rows (written when store=true) are deleted automatically by an in-app
job every RESPONSE_STATE_GC_INTERVAL_MS; an opportunistic prune on write traffic covers the gaps
between ticks. Retention is RESPONSES_STATE_RETENTION_DAYS. See Responses for what
gets stored and how to retrieve or delete it early.
Backups
Back up Postgres; Redis is disposable. Postgres is the source of truth — deployments, virtual keys, operation logs, response states, extension artifacts, and router settings. Redis only holds ephemeral runtime state (cooldowns, in-flight counters, rate-limit windows, the response cache), which rebuilds itself, so it needs no backup.
The bundled Postgres uses the standard postgres image and a named volume (pgdata) — exactly what
self-hosting platforms back up:
- Coolify recognizes the
postgresservice and can schedule logical (pg_dump) backups to S3-compatible storage with retention. Restoring through Coolify's UI is only available for its standalone managed databases, not Compose services — restore a Compose backup manually withpg_restore/psql. For one-click backup and restore, deploy Postgres as a Coolify-managed database and pointDATABASE_URLat it over the private network. - Dokploy schedules
pg_dumpbackups (to S3) for databases inside a Compose app as well as standalone ones, with restore. - Portainer has no database-aware backup: use a
pg_dumpcron, a volume-backup sidecar (e.g.offen/docker-volume-backup), or back up thepgdatavolume at the host.
Manual logical backup/restore (works anywhere):
# Backup
docker compose exec -T postgres pg_dump -U gateway -d bifrost --no-owner | gzip > backup.sql.gz
# Restore into an empty database
gunzip -c backup.sql.gz | docker compose exec -T postgres psql -U gateway -d bifrostPrefer
pg_dump(logical) backups: they are consistent without stopping the gateway. A rawpgdatavolume snapshot is only consistent if Postgres is stopped or the platform uses a consistent-snapshot method.
Dependency audit
bun audit --production is expected to be clean for the production dependency tree, and CI enforces it
at --audit-level=high.
What to read next
- Observability — logs, telemetry, and the
/admin/logsand/admin/usagequeries. - Cost accounting — how pricing and spend actually work.
- Production checklist — a condensed pre-launch checklist.
- Upgrades — rolling out a new version safely.