Skip to content

Services

cairn runs with none of these. Each is opt-in and needs the same three things: a Python extra, a compose profile (if it runs as a container), and a few environment variables. This page lists all three per service.

Enable profiles with COMPOSE_PROFILES in deploy/prod/.env (comma separated) and matching --profile flags:

Terminal window
COMPOSE_PROFILES=temporal,mlflow,otel
docker compose --env-file .env --profile temporal --profile mlflow up -d

Validate before starting anything — it cross-checks the .env against the profiles you enabled:

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

Survives restarts, retries per activity, and holds human-approval waits for days. Without it, runs execute in-process and a restart loses them.

Terminal window
uv sync --extra runtime-temporal --extra runtime-postgres
docker compose --env-file .env --profile temporal up -d

Profile temporal starts three containers: the Temporal server (temporalio/auto-setup, backed by the same Postgres), its UI on 127.0.0.1:8233, and a cairn worker running the cairn-worker console script. Add dedicated-temporal-db to give Temporal its own Postgres.

VariableDefaultPurpose
OBS_RUNTIMEinprocessset temporal to route runs to the cluster
OBS_TEMPORAL_ADDRESSlocalhost:7233cluster address (temporal:7233 in compose)
OBS_TEMPORAL_NAMESPACEdefaultnamespace
OBS_TEMPORAL_TASK_QUEUEcairnbase queue; the worker build id is appended
OBS_WORKER_BUILDinstalled versionbuild id stamped on the queue
OBS_TEMPORAL_REMOTE_TASK_QUEUEunsetdedicated queue for remote/GPU legs
OBS_TEMPORAL_DATASET_TASK_QUEUEunsetdedicated queue for dataset batches
OBS_TEMPORAL_ALLOW_EMPTYunsetlet the worker boot with zero servable graphs

The worker needs SSH_PRIVATE_KEY too if any leg runs on a remote box.

Terminal window
uv sync --extra engines
docker compose --env-file .env --profile metaflow-service up -d

Profile metaflow-service starts the Netflix metadata service on 127.0.0.1:8080 plus its own Postgres.

VariableDefaultPurpose
OBS_ML_ENGINEbuiltinset metaflow to run kind: ml flows on it
OBS_DATAFLOW_ENGINEbuiltinsame selector for kind: data flows
OBS_METAFLOW_SERVICE_URLhttp://metaflow-service:8080metadata service
OBS_METAFLOW_ROOT~/.cairn/metaflowlocal metadata root
OBS_METAFLOW_DATASTORElocals3 for shared artifacts
OBS_METAFLOW_DATASTORE_ROOTrequired when the datastore is s3
OBS_METAFLOW_COMPUTElocallocal | kubernetes | batch
OBS_METAFLOW_IMAGErequired for kubernetes/batch
OBS_METAFLOW_QUEUEAWS Batch queue name
OBS_METAFLOW_RETRYsubstrate-level retry count
OBS_METAFLOW_SECRETScomma-separated connection names to inject
OBS_METAFLOW_ISOLATIONautoauto | inherit | strict

cairn derives the downstream METAFLOW_* variables itself — you set the OBS_* ones only.

Prefect is not part of the compose stack; you point cairn at an existing Prefect deployment.

Terminal window
uv sync --extra engines
export OBS_DATAFLOW_ENGINE=prefect
export PREFECT_API_URL=http://127.0.0.1:4200/api # required — unset is a hard error

The Prefect side must also run cairn’s bridge, registered as the deployment cairn-dataflow-bridge/cairn:

Terminal window
python -m operator_dataflow.prefect_bridge
Terminal window
uv sync --extra rag --extra rag-sqlite-vec # or rag-pgvector / rag-qdrant

RAG_VECTOR_STORE selects the backend (default sqlite_vec). An unrecognised value silently falls back to sqlite-vec with a warning, so check doctor if retrieval looks empty.

BackendExtraConfiguration
sqlite_vec (default)rag-sqlite-vecOBS_SQLITE_VEC_DB (falls back to OBS_RUN_DB, then ~/.cairn/runs.db)
pgvectorrag-pgvectorPGVECTOR_DSN required (hard error if unset). The prod compose Postgres is already a pgvector image.
qdrantrag-qdrantQDRANT_URL (default http://localhost:6333), QDRANT_API_KEY. Runs as a container in the dev compose file (--profile qdrant); external in prod.
pineconeNot implemented — every method raises. Use qdrant or pgvector.

Embedding and retrieval settings, whichever store you pick:

VariableDefaultPurpose
RAG_EMBED_PROVIDERgeminigemini | openai; unknown values fall back to gemini
GEMINI_API_KEY / OPENAI_API_KEYkey for the chosen embedder
RAG_EMBED_MODELgemini-embedding-001the OpenAI embedder overrides to text-embedding-3-small
RAG_EMBED_DIM30721536 for the OpenAI default
RAG_COLLECTION_NAMEobs_runbookscollection/table name
RAG_RERANKERidentityidentity | cross_encoder | cohere
OBS_RAG_AUTOINDEXoffindex declared corpora at startup

Index pack-declared corpora on demand with cairn knowledge index-packs — declared-but-unindexed knowledge retrieves nothing.

One proxy in front of every model: routing, spend tracking, and an admin UI.

Terminal window
docker compose --env-file .env --profile llm-gateway up -d

No Python extra — the gateway is pure HTTP configuration.

VariablePurpose
LITELLM_MASTER_KEYrequired by compose when the profile is on (it fails fast without it)
LLM_GATEWAYdirect (default) or proxy
LLM_GATEWAY_URLe.g. http://litellm:4000. Empty with proxy warns and silently falls back to direct
LLM_GATEWAY_API_KEYsame value as LITELLM_MASTER_KEY
LLM_GATEWAY_JOB_TAG_HEADERdefault x-job-tag, for per-job spend attribution
LLM_GATEWAY_ADD_PROVIDER_PREFIXdefault true

Model routing itself lives in deploy/prod/litellm-config.yaml.

MLflow — experiment tracking and the model registry

Section titled “MLflow — experiment tracking and the model registry”
Terminal window
docker compose --env-file .env --profile mlflow up -d

Set MLFLOW_TRACKING_URI (http://mlflow:5000 self-hosted, or any managed URL with no profile at all). Without it, training logs nothing to MLflow and model/register keeps only the local append-only registry.

Compose-only knobs: MLFLOW_BACKEND_STORE_URI (defaults to SQLite inside the volume — move it to Postgres for real use) and MLFLOW_PUBLIC_HOST (must include your hostname or MLflow rejects proxied requests). Behind basic auth, also set MLFLOW_TRACKING_USERNAME / MLFLOW_TRACKING_PASSWORD.

Terminal window
uv sync --extra telemetry
docker compose --env-file .env --profile otel up -d
VariablePurpose
OTEL_EXPORTER_OTLP_ENDPOINTe.g. http://otel-collector:4318 — set on both server and worker; presence is what enables tracing
OTEL_EXPORTER_OTLP_HEADERSauth headers for a managed collector
OTEL_SERVICE_NAMEdefault cairn (the worker reports cairn-worker)
OTEL_CONSOLEalso print spans locally, for debugging
OBS_TRACE_CONTENToff | redacted (default) | raw — controls prompt/completion export

The collector fans out to your backends (Langfuse, Sentry, and any OTLP endpoint) via deploy/otel-collector.yaml, reading LANGFUSE_OTEL_ENDPOINT / LANGFUSE_OTEL_AUTH, SENTRY_OTEL_ENDPOINT / SENTRY_OTEL_AUTH, and OTEL_BACKEND_ENDPOINT / OTEL_BACKEND_AUTH from the same .env. Add --profile observability for Prometheus plus Grafana (127.0.0.1:3001).

ProfileStarts
(none — always on)caddy, server, postgres
temporalTemporal server, Temporal UI, cairn worker
dedicated-temporal-dba separate Postgres for Temporal
metaflow-serviceMetaflow metadata service + its Postgres
llm-gatewayLiteLLM proxy
mlflowMLflow tracking server
otelOpenTelemetry collector
observabilityPrometheus + Grafana
natsJetStream-backed durable trigger inbox
infisicalself-hosted secrets (+ Postgres, Redis)
backupnightly pg_dump

Compose fails fast without CADDY_DOMAIN, POSTGRES_PASSWORD, OBS_JWT_SECRET, and OBS_API_PASSWORD_HASH (plus LITELLM_MASTER_KEY when the gateway profile is on). deploy/prod/.env.example documents every setting, and cairn doctor shows what the running process actually resolved.