Services
cairn services owns the multi-service environment a spec pool needs: docker, conditional data seeding, and tmux session management — all config-driven, started once before the pool and stopped after the last spec. cairn services status is the read-only check; the lifecycle itself runs automatically on cairn run unless disabled.
cairn services status
cairn services status
cairn services status --config ./cairntrace.config.yml --project my-appReports the current state of the configured services environment (docker, seed freshness, tmux session). --project <name> overrides the project name (default: from config); --config <path> picks an explicit config. Output supports --format json|yaml|md.
The lifecycle (on cairn run)
When services: is configured, cairn run starts the environment once, runs the spec pool, then tears it down. The phases:
version: 1
environments:
local: {}
services:
docker:
command: "docker compose up -d"
reuseExisting: true
readinessCheck: "curl -sf http://localhost:27017"
healthcheck:
command: "curl -sf http://localhost:9200/_cluster/health | grep -q green"
intervalSeconds: 15
retries: 5
seed:
command: "yarn demo-import"
ttlSeconds: 21600
freshnessCheck: "mongosh --quiet --eval 'db.count()' mongodb://localhost:27017/db"
# Always run after seed (even when skipped as fresh) — lightweight fixture ensure.
postCommands:
- "mongosh mongodb://localhost:27017/db --quiet tools/ensure-fixture.js"
tmux:
session: myapp
reuseExisting: true
waitForReadyBeforeNext: true # opt-in: gate each later window on prior readyOn
options:
- { key: mouse, value: "on" }
env:
NODE_ENV: development
windows:
- name: web
cwd: web-app
command: "yarn serve"
readyOn: { url: http://localhost:8080 }
healthcheck:
command: "curl -sf http://localhost:8080/healthz"
intervalSeconds: 20
retries: 3
artifacts:
when: on-failure
capture: [lifecycle, tmux, docker, seed]
maxLinesPerSource: 2000
maxBytesPerSource: 524288
maxBytesPerRun: 8388608
stash:
enabled: true
autoStash: always
capture: [tmux, docker, seed]
tags: [services, myapp]
teardown:
- "tmux kill-session -t myapp"
- "docker compose down"- docker —
commandruns once;reuseExisting: trueskips if the readiness check already passes.readinessCheckgates startup;healthcheckpolls until green orretriesis exhausted. - seed — runs after docker is healthy. Freshness is tracked at
~/.cairntrace/services/<project>.seed.jsonwith a three-layer check (fingerprint + TTL + optional data-level command). A fresh-enough seed is reused; otherwise the seed command re-runs. OptionalpostCommandsalways run after that decision (skip or complete) — use them for lightweight fixture ensure scripts the bulk import does not ship. - tmux — a named session with one or more windows, each with its own
cwd,command,readyOn, andhealthcheck.readyOncan be{ url }or{ text }. By default Cairn boots every window before waiting for readiness. SetwaitForReadyBeforeNext: trueto boot in declaration order and wait for each window'sreadyOnbefore creating or starting the next; all windows share the singlereadyTimeoutMsdeadline, and a dead/disappeared pane fails immediately. On reuse, missing windows are created and idle panes (shell prompt, no running service) are re-launched; busy panes are left alone. If docker was freshly started this run, the whole session is recreated so app processes reconnect to new containers. Cairn waits for the interactive shell beforesend-keysand clears pane history first soreadyOntext cannot match stale scrollback. - artifacts — saves bounded, redacted lifecycle, docker/provisioner command output, tmux, local Compose, and seed/post-command evidence inside each run while the services are still alive. A remote provisioner such as Chalupa retains its launch/tunnel transcript without probing an unrelated local Compose project. The block is optional and defaults to
when: on-failure, all four sources, 2,000 lines and 512 KiB per source, and 8 MiB total per run. Setwhen: alwayswhile stabilizing a suite orneverto disable it. Collection errors are recorded inservices/manifest.jsonand never change the test verdict. - stash — optionally saves session artifacts (tmux panes, docker logs, seed output) in the local file.cheap vault. It does not upload or replicate them.
- teardown — after the last spec. When tmux reuse is on (the default), cairn leaves the session alive and also skips
docker compose downso infra the live panes need is not torn out from under them. Withtmux.reuseExisting: false, full teardown runs (tmux kill + docker down).
Reading service artifacts from a run
Runs that capture service evidence keep it inside the self-contained artifact pack under services/: services/manifest.json describes the capture and services/tmux/<window>.log contains each sanitized tmux window log. Use the run-aware log commands so the evidence remains tied to the exact behavioral run that produced it:
cairn logs latest --services
cairn logs latest --service web-api
cairn logs previous --service workerWhen ref is omitted, service lookup uses latest. Cairntrace checks the selected run first. If it has no run-local service pack (or no matching tmux window), the command falls back to the legacy pane logs under ~/.cairntrace/services (or CAIRN_SERVICES_LOG_ROOT). This compatibility fallback keeps older runs inspectable while new captures move into run directories managed by the normal retention policy.
Skipping and per-environment overrides
cairn run flows/x.yml --no-services # skip the whole lifecycle
cairn run flows/x.yml --services-dry-run # print the plan and exit; do not run specsThe printed plan keeps commands readable while replacing interpolated environment and selected-vault secret values with [redacted].
Per-environment overrides replace --no-services for remote envs:
version: 1
services:
tmux:
session: myapp
windows:
- name: web
command: "bun run dev"
environments:
dev:
services: false # disable all services (app is already deployed remotely)
staging:
services: # partial block deep-merges over the top-level one
seed:
command: "bun run seed:staging"
ttlSeconds: 3600
secrets: # an env-level secrets block REPLACES the top-level one
provider: tvault
tvault:
project: myapp-staging
remote:
services: # keep docker/seed phases, drop inherited local tmux windows
tmux: falseA partial services: block deep-merges over the top-level one. An env-level secrets: block replaces the top-level one entirely. Inside a partial services: block, tmux: false removes only the inherited local tmux windows while keeping the docker and seed phases — for apps running remotely over a tunnel that still own provisioning and seeding.
TinyVault seeding
secrets.provider: tvault resolves only the invocation's explicit secrets.keys, required, and root-spec/imported-action placeholder names, then supplies that scoped set to the seed command. It never exports a whole project or mutates global process.env; publisher-only and TinyVault client-control variables are removed from service children. The tvault: block supports direct (project) or inheritance (group + env) mode. See Secrets for the status command and the cairn secrets diagnostic.
Validation
cairn config validate --json validates the config file — the zod schema plus cross-field .refine() rules: unique window names, readyOn constraints, and tvault provider requires a tvault: block with either project or group+env. Run it before relying on a services block in CI.
See also
- Configuration — the
services:schema and env resolution - Secrets — the TinyVault integration the seed step uses
- Stash —
services.stashpersists session artifacts to fcheap