Target¶
Adopting an existing frappe_docker deployment, and converging it with cairn-adopt.
- Assumes cairn is already installed — see Get Started if it isn't yet.
- Assumes there's already something running to adopt: a
frappe_dockerdeployment on this host, with exactly one site.cairn-adoptnever creates a site, volume, or database — that stays the operator's job here exactly as it already was without cairn. - Assumes the registry side is already decided and an environment's tag already points at an image — see Builder: point an environment at it.
Verify with doctor¶
cairn-adopt doctor
On a host that hasn't been adopted yet, expect one failure, not just warnings — there's no descriptor yet, so cairn genuinely doesn't know what this host runs:
FAIL descriptor No environment descriptor at /etc/cairn/adopt.toml, so cairn does not
know what this host runs. Create it, declaring at least the image, the
tag to watch, and the site.
OK docker docker v29.6.2
OK docker compose Docker Compose version v2.32.1
WARN reconcile timer systemd not available, or the timer is not installed
WARN shared config /etc/cairn does not exist yet — run this CLI's `setup` subcommand, or
create it by hand
1 of 5 checks failed (2 warning(s)).
That FAIL is expected at this point, and resolves itself once a descriptor exists (next
section) — it isn't a sign anything is wrong with the host. Docker and Compose being OK
already is worth confirming before going further, since every later step depends on both.
Survey the running deployment¶
You don't hand-write /etc/cairn/adopt.toml. Run examine against the deployment already
running on this host — it reads the live stack and prints a draft descriptor; it writes
nothing:
--environment is a label you choose, not something examine detects or validates against
the host — nothing about the running deployment says what "environment" means. Use the same
name this environment already has on the build side (that environment's manifest, [cairn]
environment — production, staging, test, ...), so the descriptor, doctor, and
reconcile's own log lines read consistently with the rest of the deployment. It only fills
in the descriptor's environment field — it does not choose which registry tag gets watched;
see the note on tag below.
Run it with sudo. examine itself doesn't require root, but a hand-built deployment's
directory is outside cairn's control and commonly isn't world-readable — an existing
deployment we adopted this way turned out to be owned so that only root could even list it.
setup (below) and reconcile's own systemd unit both run as root anyway, so surveying with
the same privilege from the start means what examine reports is what reconcile will
actually see later, rather than a partial picture with unreadable fields quietly reported as
"could not be checked":
sudo cairn-adopt examine --environment production
Compose project acmecorp
Compose files /opt/frappe_docker/compose.yaml + 3 override(s)
Sites erp.acmecorp.com
Installed apps erpnext
Running image ghcr.io/acmecorp/erpnext-v16:production
# Generated by `cairn-adopt examine` from the deployment running on this host.
# Review it, then install it as /etc/cairn/adopt.toml.
environment = "production"
registry_host = "ghcr.io"
image = "acmecorp/erpnext-v16"
tag = "production"
site = "erp.acmecorp.com"
[compose]
directory = "/opt/frappe_docker"
# Read from the running project, not assumed — a hand-built deployment may not call it
# "compose.yaml".
file = "compose.yaml"
project = "acmecorp"
overrides = ["mariadb", "redis", "https"]
[health]
timeout_seconds = 600
interval_seconds = 5
Review the above, then install it as /etc/cairn/adopt.toml.
registry_host/image/tag are whatever is actually running right now — not
automatically what cairn manages. examine reads all three straight off the live
container, splitting the registry host from the repository rather than gluing them into one
string (mirroring the manifest's own [cairn.registry] host) — a copy-paste from
cairn-registry images lands in the right field without editing. If this host was brought
up by hand against production (the moving environment tag cairn-build assign-tag already
points), that's exactly what gets captured, and reconcile will correctly keep watching
production from here on. Three cases need a manual edit before installing:
- A pinned content-hash tag (the host was deployed by pulling one specific digest
directly) —
examinecaptures that exact tag, and a descriptor installed as-is would watch a tag that never moves again. Edittagto the environment's moving tag. - A pre-cairn deployment — the running image is something like the public
frappe/erpnext:v16.26.1from Docker Hub, not anythingcairn-buildever produced.registry_hostcomes back"docker.io"—registry_hostis required, and Docker Hub has a canonical name too, so a hostless running reference is recorded as that name rather than left unstated. This is the normal shape of a first adopt: the currently-running image is expected to be replaced on the first realreconcile, not preserved.registry_host,image, andtagall need to point at your own registry instead —cairn-registry imagesprints the registry host once and groups every tag by the build it names, so the exact values to paste in are right there.
file is read off the running project too, not assumed. A hand-built deployment doesn't
have to name its base compose file compose.yaml — examine reads whatever it's actually
called and fills it in explicitly, so reconcile addresses the same file later rather than
guessing frappe_docker's usual name and failing to find it.
If anything couldn't be determined, examine says so by name rather than guessing — a
transcription error here is a wrong deploy, not a typo:
Could not determine:
running image: no running 'backend' service was found
It also cross-checks a manifest's app list against what the site actually has installed, if
you pass one — worth doing, since a mismatch is the likeliest way a first reconcile fails
(bench migrate running against code the site doesn't expect):
sudo cairn-adopt examine --environment production --manifest /srv/cairn/acmecorp/cairn_production.toml
And if the host serves more than one site, examine stops rather than emitting a descriptor
that would silently drop every site but the first — decide how multiple sites should be
handled before adopting a host like that.
Install the descriptor¶
Two ways to get from here to an installed /etc/cairn/adopt.toml, both idempotent:
By hand — review examine's output above, then write it yourself:
sudo mkdir -p /etc/cairn
sudo tee /etc/cairn/adopt.toml # paste the TOML block from examine
With setup — does the same survey in-process, but also takes a pre-migration backup
first and shares /etc/cairn with a group, in one root-gated run:
sudo cairn-adopt setup --dry-run # review every action first
sudo cairn-adopt setup --environment production
cairn-adopt setup
workdir /home/brian
[preflight]
[ok] root running as root
[ok] docker engine docker v29.6.2
[ok] free disk 199 GB free on /var/lib/docker
[ok] available memory 15.8 GB available
[admin-group]
group 'cairn-admins' already exists (gid 1001)
[recon]
project acmecorp — running(4)
CUSTOM_IMAGE=ghcr.io/acmecorp/erpnext-v16
CUSTOM_TAG=production
SITES=`erp.acmecorp.com`
[backup]
backups present:
erp.acmecorp.com/private/backups/20260804_150312-erp_acmecorp_com-database.sql.gz
[descriptor]
describes environment 'production' on site 'erp.acmecorp.com'
--- summary ---
did: /etc/cairn shared with group 'cairn-admins' (mode 2775)
did: verified pre-install backup
did: installed /etc/cairn/adopt.toml
warn: the backup is on the box; copy it off before relying on it
Two things worth knowing about setup here:
- The backup is the point of running
setupover hand-installing the file.bench migrate(whichreconcileruns on every convergence) is irreversible, and this is the last moment before cairn ever touches this site that a backup is guaranteed to exist.--skip-backupis available, but skipping it is reported as a warning for exactly that reason. reconrecords how to put things back, from the.envcairn found before writing anything — a revert note, not a rollback cairn performs on its own (reconcilenever rolls back; see below).
Either path is safe to re-run: an existing descriptor is never silently overwritten, and
setup's other steps converge rather than repeat.
Verify with doctor again¶
cairn-adopt doctor
The descriptor now loads, so a registry check joins the list — this is the exact read
reconcile performs on every poll, so a failure here is one reconcile would also hit:
OK descriptor environment 'production', site 'erp.acmecorp.com', watching
ghcr.io/acmecorp/erpnext-v16:production
OK docker docker v29.6.2
OK docker compose Docker Compose version v2.32.1
WARN reconcile timer systemd not available, or the timer is not installed
OK registry ghcr.io/acmecorp/erpnext-v16:production resolves to sha256:3f9a1c2e...
OK shared config group 'cairn-admins' (mode 2775), writable by you
All 6 checks passed (1 warning(s)).
The reconcile timer warning is expected until automation is installed —
nothing has actually converged this host yet, and running it once by hand (next) is
deliberately what happens before that timer exists.
Run reconcile¶
Preview first — nothing is pulled, started, or migrated. Run it as whatever account the timer
will later use (root by default, matching systemd-units/setup-timer below) — reconcile
builds its docker compose invocation from the descriptor's directory/file, and needs the
same real filesystem read access into that directory examine did:
sudo cairn-adopt reconcile --dry-run
Environment production — site erp.acmecorp.com
Watching ghcr.io/acmecorp/erpnext-v16:production
Would pull sha256:3f9a1c2e..., start the stack, migrate, and verify health — this host holds
no image for that tag yet.
(If this host already runs the exact digest the tag resolves to, --dry-run reports that
instead, and a real run underneath would be a no-op — this is the common case once a timer is
polling regularly.)
Once it looks right, drop --dry-run:
sudo cairn-adopt reconcile
Environment production — site erp.acmecorp.com
Watching ghcr.io/acmecorp/erpnext-v16:production
Pulling ghcr.io/acmecorp/erpnext-v16:production
Starting the stack
Migrating erp.acmecorp.com
Verifying health
containers are up
Converged to sha256:3f9a1c2e...
reconcile runs bench migrate after every convergence — the sole database write cairn ever
makes — and, on a failure of any kind (a bad migration, or the stack not becoming healthy
within the timeout), halts and reports rather than rolling back. Nothing about a rollback
is automatic — repeat: reset the environment's tag to an earlier image on the build side, and
reconcile's next pass converges to that instead, migrating forward the same way it always
does. recon's note (above) is what lets you put the previous deployment's .env values
back by hand, if it ever comes to that.
reconcile is also single-flight: a second invocation while one is already running exits
immediately, reporting the lock, rather than queueing behind it — the timer (next) relies on
this so an overrunning pass can never stack.
Automate it¶
Once a manual reconcile has worked at least once and you've watched it succeed, install the
timer that repeats it unattended — see Reconcile Automation.
Next steps¶
- Reconcile Automation — install the systemd timer that polls the environment's tag and converges automatically.
- Build Automation — if you haven't already, automate the
build side too, so a
git pushreaches this host without a manual step on either end.
This page is written ahead of a live run against a real target — check back for a note once it's been verified, the same way Get Started and Builder already have been.