Skip to content

Deployment

Production deployment is one compose file plus one .env. The engine ships as ghcr.io/evs-cmd/cairn-engine; everything else is a compose profile you toggle on.

Always on:

  • caddy — TLS + reverse proxy (needs CADDY_DOMAIN)
  • server — the cairn engine (cairn-server, port 9090)
  • postgres — app store + checkpointer (pgvector image)

Enable with COMPOSE_PROFILES in the .env (and match --profile flags):

ProfileAdds
temporala cairn worker + Temporal server + Temporal UI (the durable runtime)
dedicated-temporal-dba separate Postgres for Temporal
llm-gatewaya LiteLLM proxy (port 4000) — point LLM_GATEWAY=proxy + LLM_GATEWAY_URL at it for routing, spend tracking, and its admin UI
mlflowan MLflow tracking server — set MLFLOW_TRACKING_URI and model/register promotes into a real registry
otelan OpenTelemetry collector (traces/metrics fan-out)
observabilityPrometheus + Grafana
natsa JetStream-backed durable trigger inbox
infisicalself-hosted secret management (+ its Postgres and Redis)
backupnightly pg_dump

Copy deploy/prod/.env.example to deploy/prod/.env on the box (secrets never ship through git or CI — the deploy workflow ships only the image and compose file). Four variables are required:

CADDY_DOMAIN=cairn.example.com
POSTGRES_PASSWORD=...
OBS_JWT_SECRET=... # presence turns auth ON; OBS_REQUIRE_AUTH=1 makes it mandatory
OBS_API_PASSWORD_HASH=... # from: cairn remote hash-password

Then validate before starting anything:

Terminal window
cairn deploy doctor --env deploy/prod/.env --profile temporal --strict

deploy doctor cross-checks env ↔ profiles: OBS_RUNTIME=temporal without the temporal profile is a failure; checkpointer, MLflow, OTel, Infisical, and NATS settings are all verified for agreement. (Known quirk: the llm-gateway profile currently triggers an “unknown profile” warning in doctor even though it’s valid.)

The control plane is open when OBS_JWT_SECRET is unset — fine for local dev, never for a reachable box. With the secret set, every route outside a small public list (/healthz, /token, /console assets, signed webhook ingress) requires a bearer JWT from POST /token. Users are admin-invited (POST /admin/users) — there is no public signup.

The engine emits OpenTelemetry traces and metrics when OTEL_EXPORTER_OTLP_ENDPOINT is set; LLM prompt/completion content export is gated by OBS_TRACE_CONTENT (off / redacted (default) / raw). Runs carry their trace_id so the console can deep-link into Langfuse/Grafana (LANGFUSE_BASE, GRAFANA_BASE).

For the full variable list see the environment reference.