Skip to content

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_docker deployment on this host, with exactly one site. cairn-adopt never 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] environmentproduction, 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) — examine captures that exact tag, and a descriptor installed as-is would watch a tag that never moves again. Edit tag to the environment's moving tag.
  • A pre-cairn deployment — the running image is something like the public frappe/erpnext:v16.26.1 from Docker Hub, not anything cairn-build ever produced. registry_host comes back "docker.io"registry_host is 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 real reconcile, not preserved. registry_host, image, and tag all need to point at your own registry instead — cairn-registry images prints 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.yamlexamine 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 setup over hand-installing the file. bench migrate (which reconcile runs 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-backup is available, but skipping it is reported as a warning for exactly that reason.
  • recon records how to put things back, from the .env cairn found before writing anything — a revert note, not a rollback cairn performs on its own (reconcile never 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 push reaches 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.