Running the service¶
Overview¶
To deploy a Zeroth graph you run exactly one process: a uvicorn worker
that imports zeroth.service.entrypoint and calls its app_factory.
Everything else — Alembic migrations, identity, storage, dispatch, the
orchestrator, webhooks, econ — is wired for you by bootstrap_service.
This page shows the production entrypoint, how to mount under a prefix,
and the exact startup ordering you must respect.
Minimal example¶
The shipped production entrypoint is src/zeroth/service/entrypoint.py.
In a container you run:
uvicorn zeroth.service.entrypoint:app_factory \
--factory \
--host 0.0.0.0 \
--port 8000 \
--proxy-headers
or equivalently just invoke the module's main(), which also runs
Alembic migrations when the Postgres backend is configured:
For tests and scripts you can skip uvicorn entirely and build an app in-process:
import asyncio
from zeroth.service.app import create_app
from zeroth.service.bootstrap.factory import bootstrap_service
from zeroth.platform.storage.factory import create_database
from zeroth.platform.config.settings import get_settings
async def make_app():
settings = get_settings()
db = await create_database(settings)
boot = await bootstrap_service(db, deployment_ref="default")
return create_app(boot)
app = asyncio.run(make_app())
Common patterns¶
- Mounting under a prefix — Put Zeroth behind a reverse proxy and
strip the prefix, or mount the returned
FastAPIapp as a sub-app of an outer router. Do not hand-edit route prefixes insidecreate_app. - Healthchecks —
GET /healthreturns deployment ref, deployment version, and graph version ref. Wire it into your orchestrator's liveness + readiness probes. - Multiple deployments, one image — Set
ZEROTH_DEPLOYMENT_REFto select which deployment the process serves. One image, N services. - TLS termination — Pass
ssl_keyfile/ssl_certfilevia settings when you want uvicorn to terminate TLS directly.
Pitfalls¶
- Startup ordering — Bootstrap builds components in a specific order: settings → identity → storage → dispatch worker → orchestrator → service routers → webhook delivery worker. Breaking that order (e.g. constructing the worker before storage) deadlocks the lifespan hook.
- Skipping migrations — Running
app_factorydirectly (bypassingmain()) does not run Alembic. In production always go throughpython -m zeroth.service.entrypoint. - Mutating the app after lifespan starts — The lifespan context starts background workers; adding routes after that is a race.
- Missing
ZEROTH_DEPLOYMENT_REF— Defaults to"default", which is usually not what you want in production. - Proxy headers disabled — Behind a load balancer, forgetting
--proxy-headersbreaks identity propagation and audit correlation.
Reference cross-link¶
See the Python API reference for zeroth.service.
Related: concepts/service · dispatch how-to · secrets how-to.