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.
The base stack
Section titled “The base stack”Always on:
- caddy — TLS + reverse proxy (needs
CADDY_DOMAIN) - server — the cairn engine (
cairn-server, port 9090) - postgres — app store + checkpointer (pgvector image)
Optional profiles
Section titled “Optional profiles”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.
| Profile | Adds |
|---|---|
temporal | a cairn worker + Temporal server + Temporal UI (the durable runtime) |
dedicated-temporal-db | a separate Postgres for Temporal |
llm-gateway | a LiteLLM proxy (port 4000) — point LLM_GATEWAY=proxy + LLM_GATEWAY_URL at it for routing, spend tracking, and its admin UI |
mlflow | an MLflow tracking server — set MLFLOW_TRACKING_URI and model/register promotes into a real registry |
otel | an OpenTelemetry collector (traces/metrics fan-out) |
observability | Prometheus + Grafana |
nats | a JetStream-backed durable trigger inbox |
infisical | self-hosted secret management (+ its Postgres and Redis) |
backup | nightly pg_dump |
One .env
Section titled “One .env”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:
cairn deploy doctor --env deploy/prod/.env --profile temporal --strictdeploy 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.)
Auth model
Section titled “Auth model”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.
Observability
Section titled “Observability”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.