Self-hosting the full Obol stack is an Enterprise-plan feature, offered at a premium. The hosted platform is the default way to use Obol — see the quickstart. Contact the Obol team to have self-hosting enabled for your organization.
The rest of this page is the operational reference for enterprise customers running Obol themselves. Self-hosting Obol is docker compose up against infra/docker-compose.yml. That is not a stripped-down variant of the development environment — it is the same file with the same images. Development layers a hot-reload overlay on top of it; nothing is added for production (ADR-0009). Three deploy targets exist, and all three enforce the same topology:
The deploy references — Docker Compose, Fly.io, and Helm — describe Enterprise self-host deployments. They do not apply to the hosted platform, which Obol operates for you.

What runs

or, equivalently:
The control image is one image with three roles selected by the container command: api, worker, and migrate. apps/control/docker-entrypoint.sh dispatches them.
There is no connectors service, in any target. Out-of-process Python workers are unimplemented — apps/connectors/ ships no package and no image — so the service, its connector-workers Compose profile, the Fly app and the Helm workload were all removed rather than left as a profile that could not start. Reviewed native connector packs run through the gateway’s own HTTP executor and need no worker. See Trusted workers.

Boundaries the topology enforces

  • The gateway has no DATABASE_URL. Postgres never sits on the streaming path; the gateway reads snapshots from Redis and falls back to control’s /internal/v1/*.
  • Only the gateway can unwrap a credential. No other workload receives a vendor secret or key-encryption key, and no target sets OBOL_CONNECTORS_URL. Were a worker deployment added, it would need a private-only address and an ingress policy admitting the gateway alone — and it would today receive the vendor credential over ADR-0014’s hand-off, which is why one has not been.
  • Control’s /internal/* is never routed by the public edge. Under Compose the whole control port is published, so put control behind a reverse proxy that allows only /api, /oauth, /.well-known, and /healthz when it is reachable from outside the host.
These are the same invariants the code enforces, restated at the infrastructure layer.

Required configuration

infra/.env is the only file you edit. make -C infra env seeds it from infra/.env.example.

Secrets you must set

Compose refuses to start without POSTGRES_PASSWORD and OBOL_SERVICE_JWT_SECRET; neither has a default.
With no CLERK_ISSUER set, control answers 503 on every /api/v1 operator route rather than serving them unauthenticated. That is deliberate. A control plane that silently opens because a variable was unset is the worst available outcome.
NEXT_PUBLIC_* values are compiled into the dashboard image, so a runtime environment variable cannot change them. Rebuild web after changing the publishable key or the public URLs.

Key-encryption key

OBOL_KEK_PROVIDER selects how vaulted credentials are wrapped.
make -C infra age-key generates the local identity and prints its public recipient. The private identity is mounted as a Compose secret into the gateway only, at /run/secrets/age.key. Control gets OBOL_AGE_RECIPIENT and can therefore seal a credential but never open one. Connectors and web receive no key material at all. See vault and key management.

Optional configuration

Never set OBOL_FORWARDED_ALLOW_IPS to *. Scope it to the exact proxy addresses or ranges in front of control.
Leaving the Stripe variables blank is a supported self-host posture: usage is still recorded, and only delivery to Stripe waits. See metering.

Database migrations

migrate runs alembic upgrade head and exits. Both control and control-worker depend on it with the service_completed_successfully condition, so no application process starts against an un-migrated schema. Control also has a /healthz healthcheck the gateway waits on before it starts. The Kubernetes chart applies the same rule differently — a revision-named Job plus an init container that waits for the image’s full Alembic head set (ADR-0043). See Helm.

Upgrading

1

Read the release notes for schema changes

Migrations must stay compatible with the application revision still running during a rollout. A breaking schema change needs a staged migration plan, not a faster restart.
2

Back up Postgres

3

Pull the new source and rebuild

prod-up rebuilds the images and recreates changed services. migrate runs before control and control-worker come back.
4

Verify

/startupz is the first-boot probe: Compose’s gateway healthcheck uses it so an empty install becomes healthy, and the self-host smoke check fails if it returns 503 (Redis gone or draining). /readyz is traffic readiness and still requires at least one policy snapshot — Helm and Fly keep it as the load-balancer probe because those installs have a snapshot before customer traffic. /healthz is liveness only (always 200). See Telemetry.
Never erase a database to repair a failed deployment. Inspect the migrate service logs first — a failed migration is a diagnosable state, and the data behind it is the audit record.

Upgrading a development stack

The development control image synchronizes its locked Python dependencies at startup (OBOL_DEV_SYNC_DEPS=true) into a persistent .venv volume. Rebuilding the image alone does not refresh that volume. After changing control dependencies or migrations:
Python hot reload watches application source only. It does not install dependencies and it does not apply migrations. Production images keep their baked environment and install nothing at startup.

Data and state

Redis runs with --appendonly yes and --maxmemory-policy noeviction. The eviction policy is not incidental: snapshots and idempotency records must not be evicted under memory pressure. Back up pgdata. Redis is a cache and a stream buffer in front of Postgres, but usage events in flight are only in Redis until the worker drains them, so drain before a destructive Redis operation.

Next steps

Compose reference

Service-by-service detail on the Compose files and Makefile targets.

Helm

The Kubernetes self-host path.

Invariants

The rules the deployment is shaped to enforce.

Telemetry

Metrics, traces, and where they go.