Standalone service¶
Standalone service mode runs zeroth-platform 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-platform[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 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 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 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 serve. - Migrations not run: the
zeroth 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.