Opbox runs as one self-contained docker-compose stack on the box - there is no managed cloud tier. Each
box is one tenant. The first-party services run from GHCR images (ghcr.io/wlilley93/opbox-*), pinned by
the OPBOX_*_TAG env vars (default main; see versions.yaml). Only the reverse proxy is internet-facing;
the data plane stays internal.
The services
| Service | Image | Role | Exposure |
|---|---|---|---|
postgres | pgvector/pgvector:pg18 | The single system of record (relational state + the vector store). | internal only |
docrender | opbox-docrender | The document engine (DOCX/PDF), HMAC-gated, on :8651. | internal only |
kernel | opbox-kernel | The Rust verb kernel - HTTP/MCP front door on :8088. Release image built with dev-identity dropped (ADR-0040). | via Caddy |
frontend | opbox-frontend | The Next.js front end (Next.js 16 / React 19) on :3000 - the site root (marketing, sign-in, the whole app). Reads the kernel-owned tables directly (RLS-scoped) and writes via kernel verbs. | via Caddy |
agent-chat | opbox-agent-chat | The agent engine: the caged-Hermes gateway (OpenAI /v1 + AG-UI /chat/stream) on :8099. | via Caddy |
drainer | opbox-agent-chat | The same image in drain-loop agent.run mode - a continuous caged seat that claims and works agent tasks. | internal |
cron-scheduler | alpine | Polls http://frontend:3000/api/cron/workflows (with the x-cron-secret header) on an interval to fire scheduled workflows. | internal |
docs | opbox-docs | This documentation site (static, Astro). Served at /docs (login-gated by default via the frontend’s /api/docs/gate, with a public allowlist). | via Caddy at /docs |
minio + minio-init | minio/minio | Object storage for file bytes. The kernel seals before the seam, so MinIO holds ciphertext only. (OPBOX_STORAGE_BACKEND=bytea keeps bytes in Postgres instead.) | internal only |
frontdoor | opbox-frontdoor | The header/SSO front door on :8092. | internal |
caddy | caddy:2 | TLS / HTTP reverse proxy - the only internet-facing service. | :80 / :443 |
hatchet + hatchet-worker | hatchet-lite / opbox-hatchet-worker | Optional durable automations runner (see Hatchet). | internal |
The data plane (postgres, docrender, minio) sits on an internal Docker network with no host port and
no egress; the front-facing services share the edge network; caddy is the single front door. The Beelink
override (docker-compose.beelink.yml) adds loopback host-port bindings, plain-HTTP Caddy (no ACME on a local
box), and a redis + srh pair for the frontend’s Upstash-style rate-limit store.
The secrets
Required secrets (copy .env.example to .env and fill them; the ${VAR:?} form fails the bring-up fast if
one is missing, so a release can never silently boot on a dev default):
| Secret | Used for |
|---|---|
POSTGRES_PASSWORD | The database password. |
OPBOX_PII_KEY | The AES-256 PII / file encryption key (32 bytes hex/base64). Fail-closed without it. |
DOC_RENDER_HMAC_SECRET | The shared kernel ↔ docrender HMAC (identical in both). |
OPBOX_WEBHOOK_KEY | Derives the per-tenant inbound-webhook HMAC (board / payment / e-sign callbacks). |
NEXTAUTH_SECRET, CSRF_SECRET, ENCRYPTION_KEY, APP_TOKEN_SIGNING_SECRET, IDENTITY_HASH_SECRET | The frontend’s session, CSRF, at-rest, token-signing, and identity-hashing secrets. |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | The MinIO object-store credentials. |
CRON_SECRET | The shared secret the cron-scheduler presents to /api/cron/workflows. |
OPBOX_AGENT_KEY / OPBOX_DRAINER_AGENT_KEY | The chat-agent and drainer bearers - minted by genesis, left blank beforehand. |
OPBOX_SITE_ADDRESS / FRONTEND_PUBLIC_URL | The public hostname (or :80 for plain-HTTP / tailnet access) and the frontend’s external URL. |
| an LLM key | The model/provider for agent-chat is configurable (GLM / OpenRouter / OpenAI / local Ollama / any OpenAI-compatible): e.g. GLM_API_KEY (the default, cloud GLM-4.6 via z.ai), OPENROUTER_API_KEY, or OPENAI_API_KEY. The local Ollama path needs no key. |
The Beelink override also wants SRH_TOKEN (the Redis-HTTP shim token); the optional Hatchet runner wants
HATCHET_CLIENT_TOKEN and OPBOX_HATCHET_SERVICE_KEY.
Bring it up
cp .env.example .env # fill the secrets (generation commands are in the file)
docker compose -f docker-compose.yml -f docker-compose.beelink.yml --env-file .env up -d
The first bring-up pulls the GHCR images, runs the kernel migrations on boot, and starts every service. On a
fresh box the founding ceremony (opbox initiate, which mints the first owner bearer and the bounded
chat-agent) is the one deliberate host act that turns the running stack into a usable tenant - see
Genesis. The genesis.sh wrapper runs the whole sequence (copy .env, pull,
migrate, up, found the owner) end to end.
Verify the release kernel dropped its dev identity (a no-bearer call must return 401) before exposing the
box, and confirm the stack is healthy with docker compose ps.