Skip to content

Deployment

Production deployment is one compose file plus one .env. The engine ships as a container image (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). Services covers each one in detail — the extra it needs, the containers it starts, and every variable it reads.

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 and fill it in (secrets never ship through git or CI — the deploy workflow ships only the image and compose file). At minimum you set the TLS domain, the Postgres password, a JWT signing secret (its presence turns auth on), and an admin password hash (from cairn remote hash-password). The example file documents each setting.

Then validate before starting anything:

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

deploy doctor cross-checks the .env against the enabled profiles — e.g. selecting the Temporal runtime 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 no JWT secret is configured — 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 an OTLP endpoint is configured; LLM prompt/completion content export is gated with three modes — off, redacted (the default), or raw. Runs carry their trace_id so the console can deep-link into Langfuse/Grafana.

The .env.example file, and cairn doctor at runtime, are the source of truth for every setting a deployment reads.