Standalone service¶
Standalone service mode runs zeroth-core as a production single-node deploy
fronted by a reverse proxy. Use it when you want real Postgres, TLS, and a
process manager (systemd, supervisord, or a pod) — but without the overhead
of Docker Compose or Kubernetes.
Use case¶
- Production single-node deploy on a VM or bare-metal host
- Fronted by nginx or Caddy for TLS termination
- Managed by systemd (or another process supervisor)
- Postgres for durable state, optional Redis for arq dispatch
Prerequisites¶
- Python 3.12+
- Postgres 14+ with a database and role provisioned
- (Optional) Redis for
ZEROTH_DISPATCH__ARQ_ENABLED=true - A reverse proxy (nginx, Caddy, or Traefik) terminating TLS
- A secret-management story for
ZEROTH_*env vars
Install¶
python3.12 -m venv /opt/zeroth/venv
/opt/zeroth/venv/bin/pip install "zeroth-core[memory-pg,dispatch]"
Configure¶
Create /etc/zeroth/zeroth.env with at least the following (excerpt — see the
full Configuration Reference):
ZEROTH_DATABASE__BACKEND=postgres
ZEROTH_DATABASE__POSTGRES_DSN=postgresql://zeroth:secret@db:5432/zeroth
ZEROTH_DATABASE__ENCRYPTION_KEY=<base64-32-bytes>
ZEROTH_SERVICE_API_KEYS_JSON='[{"credential_id":"ops","secret":"<opaque-token>","subject":"ops","roles":["admin"]}]'
ZEROTH_DISPATCH__ARQ_ENABLED=true
ZEROTH_REDIS__HOST=127.0.0.1
ZEROTH_REDIS__PORT=6379
OPENAI_API_KEY=sk-...
Run Alembic before the first start:
Run¶
The zeroth-core serve console script runs migrations, bootstraps the
service (including agent runners built from the deployment's graph), and
serves uvicorn with TLS from zeroth.service.entrypoint — bootstrap
and server share one event loop, which the durable run worker requires.
Use it directly under systemd (Type=simple):
Do not serve zeroth.service.entrypoint:app_factory through
uvicorn --factory: uvicorn calls the factory inside its running event
loop, where the bootstrap's asyncio.run() raises RuntimeError. For
horizontal scale, run one zeroth-core serve process per port behind
your reverse proxy (each instance polls the shared run queue; Postgres
SKIP LOCKED leasing keeps them from double-claiming runs).
systemd unit¶
[Unit]
Description=Zeroth Core API
After=network.target postgresql.service
[Service]
Type=simple
User=zeroth
EnvironmentFile=/etc/zeroth/zeroth.env
ExecStart=/opt/zeroth/venv/bin/zeroth-core serve --port 8000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
nginx front¶
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Verify¶
See the HTTP API Reference for the full surface you can now exercise through the proxy.
Common gotchas¶
- Serving
app_factoryviauvicorn --factory: the factory callsasyncio.run()and uvicorn invokes it inside a running loop —RuntimeError: asyncio.run() cannot be called from a running event loop. Usezeroth-core serve. - Migrations not run: the
zeroth-core serveconsole script runsalembic upgrade headautomatically; rawuvicorndoes not. Run it once before first start, and after every upgrade. - Encryption key:
ZEROTH_DATABASE__ENCRYPTION_KEYmust be stable across restarts — losing it makes previously stored secrets unrecoverable. - Worker count: start with
--workers $(nproc)and tune based on observed latency; agent runs are async-heavy so CPU is rarely the bottle.