Skip to content

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:

/opt/zeroth/venv/bin/alembic upgrade head

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):

/opt/zeroth/venv/bin/zeroth serve --port 8000

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

curl -I https://api.example.com/health/ready
systemctl status zeroth

See the HTTP API Reference for the full surface you can now exercise through the proxy.

Common gotchas

  • Serving app_factory via uvicorn --factory: the factory calls asyncio.run() and uvicorn invokes it inside a running loop — RuntimeError: asyncio.run() cannot be called from a running event loop. Use zeroth serve.
  • Migrations not run: the zeroth serve console script runs alembic upgrade head automatically; raw uvicorn does not. Run it once before first start, and after every upgrade.
  • Encryption key: ZEROTH_DATABASE__ENCRYPTION_KEY must 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.