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.
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
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.
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/healthzwhen it is reachable from outside the host.
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.
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
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.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:
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.