Operating the Stack¶
Once a target is under cairn's management, cairn-adopt gives you verbs for the everyday work
— looking at logs, opening a console, taking the stack down for maintenance, reclaiming the disk
old images hold — so you never need to reach for docker compose yourself.
That matters more than convenience. cairn builds each docker compose invocation from the
environment descriptor: the project name, the compose file and its overrides, and the image
variables the file interpolates. A hand-written command that gets any of those wrong can start
the wrong image or, worse, appear to work. The verbs below get it right by construction, and
none of them needs an argument — everything comes from /etc/cairn/adopt.toml.
Looking at a running stack¶
cairn-adopt logs # every service
cairn-adopt logs -f # follow
cairn-adopt logs --tail 200 backend
cairn-adopt shell # a shell in the bench container
cairn-adopt shell db # ...or in another service
cairn-adopt console # bench console, for this environment's site
cairn-adopt mariadb # a MariaDB console, for this environment's site
console and mariadb already know which site to open — you don't pass a site name.
Reading your own site config
cairn has no verb that prints site_config.json or common_site_config.json, and that is
deliberate: site_config.json holds your database password, and cairn does not read or
print secrets. Use cairn-adopt shell and read the file yourself — the credential then
never passes through cairn or its output.
cairn-adopt shell
cat sites/common_site_config.json
cat sites/<your-site>/site_config.json
Taking the stack down¶
cairn-adopt stop --reason "adding a port mapping"
cairn-adopt start
cairn-adopt restart
stop brings the containers down and holds the environment there. start releases the
hold and brings it back, converging to whatever image the environment's tag points at.
restart does both in one step.
Volumes are never touched by any of these. stop stops containers; it does not remove them,
and it does not remove data.
The hold¶
A held environment is one you have deliberately stopped. It is a real state, distinct from "running" and from "broken", and cairn needs it: without a hold, the reconcile timer cannot tell a stack you stopped on purpose from a host that has died — so it would bring it straight back up, and run a database migration while it was at it.
While a hold is in place:
reconcilereports the environment as held and changes nothing. The timer keeps running and keeps doing nothing, which is the point.doctorreports the hold on every run.
A hold survives a reboot. That is on purpose — a machine that reboots in the middle of your
maintenance window should stay down, not come back up half-configured. The consequence is that
a hold nobody clears will suspend deployments indefinitely, with nothing to expire it. This is
why doctor mentions it every single time:
WARN maintenance hold held 2026-08-19 14:02:11Z — adding a port mapping.
This host will not converge until `cairn-adopt start`.
Two ways out: cairn-adopt start, or cairn-adopt restart — which clears a hold if it finds
one and tells you it did, so you can't resume a host somebody else stopped without noticing.
restart never leaves an environment held, even if it fails partway.
Worked example: changing the compose file¶
The compose file is yours to edit — cairn writes it once and never regenerates it. Say you want to publish MariaDB on the host's loopback so you can reach it through an SSH tunnel:
cairn-adopt stop --reason "publishing 3306 on loopback"
Edit the db service in /etc/cairn/compose.yaml:
ports:
- "127.0.0.1:3306:3306"
Bind to loopback, not to everything
Write "127.0.0.1:3306:3306", not "3306:3306". The second form publishes your database
to the open internet, and Docker writes its own firewall rules — ufw will not stop it.
With the loopback prefix the port is reachable only from the host itself, and therefore
only through SSH.
Then bring it back:
cairn-adopt start
start recreates any container whose configuration changed, so the new port mapping takes
effect. cairn-adopt restart would have done the same in one step, without holding it in
between — use stop/start when you want the environment to stay down while you work.
Reclaiming disk¶
Every image this host has ever run stays in local storage after the stack moves on to a newer one. That's deliberate — it's what makes a rollback a tag change rather than a download — but left alone it grows without bound, and an ERPNext image is not small.
cairn-adopt prune --dry-run
Will remove 2 image(s) this host previously ran:
7b3c1d9e0a42 1.4 GB input hash a1b2c3d4e5f6 registry.acmecorp.net/erpnext-v16:v16-a1b2c3d4e5f6
2f88ba60c715 1.4 GB input hash 4d5e6f708192 registry.acmecorp.net/erpnext-v16:v16-4d5e6f708192
Reclaims 2.8 GB.
Keeping 1 image(s) for rollback headroom.
Currently running: d47f139c6ffe. Never removed.
3 other image(s) in local storage are not cairn-adopt's and are not listed — including
anything `cairn-build` produced here itself.
Drop --dry-run to be asked for confirmation, which defaults to no; add --yes to skip
the prompt when running unattended. If an individual image can't be removed, cairn says so and
carries on with the rest rather than abandoning the run.
What it will not remove¶
- The image the stack is currently running. This is read from the running container, not
guessed from which image is newest — the distinction matters after a rollback, where the
running image is deliberately older than one that superseded it.
--keepdoes not override this. - Volumes and containers.
pruneremoves images and nothing else. Your database and sites volume are not reachable from this command. - Images
cairn-buildmade on this host. On a machine that both builds and runs, the two roles share one local image store, and each cleans up only its own. They're counted in the summary so the omission is visible, not itemized.
Choosing --keep¶
--keep is how many superseded images to retain beyond the running one, and it defaults to
1.
Treat that as a grace window, not a rollback guarantee. It buys you the immediately-previous image — enough to reverse a deploy you regret within minutes of making it, without waiting on a pull. It is not an archive, and it is not the mechanism you should rely on to reach a specific older release: a registry that still holds the tag is. Raise it if the target has disk to spare and you want more headroom; there is no reason to lower it.
Why the builder's prune won't do this for you
cairn-build prune deliberately leaves these images alone. Once an image has been pushed,
the builder has no way to know whether something on the host is still running it — so it
refuses to guess. cairn-adopt prune is the side that can know, because it reads the
running container. On a host with both roles, run both.
Verified against a live target: doctor, logs, shell, console, mariadb, and
restart. Not yet exercised in the field: the held state itself — stop placing a hold,
doctor warning about one, restart clearing a hold that genuinely exists, and a hold
surviving a reboot — along with prune. Treat those as documented-but-unproven, and check
back for a note once they have been run.