Builder¶
Building and pushing images with cairn-build.
- Assumes cairn is already installed — see Get Started if it isn't yet.
- Assumes the registry decision is already made.
cairn-build push,assign-tag, and every build automation timer all assume it — and the target role has nothing to poll without it.- Self-hosting? Provision it first — see Registry.
- Using a client- or cloud-hosted registry instead? There's nothing to provision.
Just note its address in the manifest's
[cairn.registry]table and continue below.
Verify with doctor¶
cairn-build doctor
A machine with nothing configured yet is expected to show a couple of warnings, not failures — for example:
WARN config No manifest given. Pass --manifest <path>, or set $CAIRN_MANIFEST.
OK build engine docker v29.6.2
OK docker buildx github.com/docker/buildx v0.35.0 ...
OK free disk 199 GB free on /var/lib/docker
OK available memory 63.8 GB available
OK git v2.47.3
OK vendored tree matches its recorded pin
OK vendor .git no nested .git
OK build inputs Containerfile complete
WARN shared config /etc/cairn does not exist yet — run this CLI's `setup` subcommand, or create it by hand
OK known manifests none found under /srv/cairn
All 11 checks passed (2 warning(s)).
Both warnings above are expected at this point: there's no manifest yet (next section), and
/etc/cairn is only created by cairn-build setup, which comes after you've confirmed a
manifest builds correctly by hand.
Provision the manifest directory¶
Once doctor shows the two expected warnings above, run setup as root, naming your client
and the environment this manifest is for:
sudo cairn-build setup --client acmecorp --environment production
A manifest declares at most one environment (see the manifest
reference) — for test/staging/production, run
setup again with each --environment, once per environment, and you'll get one
distinctly-named manifest per environment in the same client directory.
This does two things in one privileged run:
/etc/cairn— created and shared with thecairn-adminsgroup (mode2775), the same group your own account joined in Get Started./srv/cairn/acmecorp/— provisioned, with a startercairn_production.tomlscaffolded into it, since none existed yet.
Example output:
cairn-build setup
workdir /home/brian
[preflight]
[ok] root running as root
[ok] build engine docker v29.6.2
[ok] docker buildx github.com/docker/buildx v0.35.0 a319e5b15052cf6557ceb666eb8ff6e32380b782
[ok] free disk 199 GB free on /var/lib/docker
[ok] available memory 63.8 GB available
[ok] git git version 2.47.3
[admin-group]
group 'cairn-admins' already exists (gid 1001)
[manifest]
write /srv/cairn/acmecorp/cairn_production.toml (starter manifest)
--- summary ---
did: /etc/cairn shared with group 'cairn-admins' (mode 2775)
did: /srv/cairn/acmecorp provisioned
did: scaffolded a starter manifest at /srv/cairn/acmecorp/cairn_production.toml
skipped: group 'cairn-admins' (already exists)
setup is idempotent — re-running it later won't overwrite an edited manifest, and steps
that already exist (like the group above) are reported as skipped, not redone.
Edit the manifest¶
Edit the scaffolded cairn_production.toml for your deployment — see the manifest
reference for every field.
Run the build¶
Preview first — nothing is built, pushed, or touched:
cairn-build build --manifest /srv/cairn/acmecorp/cairn_production.toml --dry-run
This resolves every ref (contacting each app's remote), computes the tags, and prints the
exact command a real build would run. Once it looks right, drop --dry-run:
cairn-build build --manifest /srv/cairn/acmecorp/cairn_production.toml
Progress prints as it works — resolving refs, building, verifying the image landed, naming the reusable build-cache layer — and, at a terminal, the whole run is also saved to a transcript file, since nothing else is keeping it. It finishes with a per-phase timing report, worth a glance every time: it's the fastest way to notice a build that's started thrashing the layer cache instead of reusing it.
Timing
checks + ref resolution 4.2s
image build 4m 52s
verify image 0.6s
name build cache 0.3s
started 2026-08-04 15:03:09 -0700
finished 2026-08-04 15:08:24 -0700
total 5m 15s
As a real-world data point: a clean build of erpnext-v16 (Frappe + ERPNext only, no
custom apps) took 5 minutes 15 seconds end-to-end against a typical VPS. Expect
something in that range for a similar image; a much longer first run is usually the layer
cache warming up rather than anything wrong.
Where the image goes¶
By default a build stays local — tagged cairn/<image_name> in the build machine's own
Docker/Podman image store, nowhere else. It only leaves the machine once you configure a
registry (the manifest's [cairn.registry], or /etc/cairn/builder.toml) and explicitly
push — building never pushes on its own unless you pass --push. See Machine-local build
config for how the image base is chosen.
That local image store lives on disk somewhere cairn deliberately doesn't assume —
cairn-build doctor/setup already name it, as part of their disk-space check:
OK free disk 199 GB free on /var/lib/docker
The path after "free on" is the engine's own data root, read with docker info --format
'{{.DockerRootDir}}' (or podman info --format '{{.Store.GraphRoot}}') rather than
assumed — a separate mount for it is common on a build machine, and that's exactly the
case a hardcoded /var/lib/docker would get wrong. Whatever a system admin needs to
monitor for disk headroom, this is the path.
To know for certain what a build machine is actually holding, ask cairn rather than reading
a raw docker images — cairn groups by what was actually built, distinguishing a current
build from one it has since superseded:
cairn-build images --local
cairn/erpnext-v16:v16-d47f139c6ffe (input hash d47f139c6ffe)
frappe v16.25.0 9a8daf34
erpnext v16.26.1 fd00cebb
built with vendored base v3.2.1
a1b2c3d4e5f6 1.79 GB 2m cairn/erpnext-v16:v16-d47f139c6ffe, cairn/erpnext-v16:latest
1 image(s) built by cairn across 1 input hash(es); 0 superseded, holding 0 B.
Leads with the tag you'd actually recognize, not the input hash — the hash is still there,
parenthetically, and is usually visible again in the tag's own suffix
(<series>-<hash>). frappe_docker is cairn's own vendored fork of frappe_docker
(ADR-001) — its version is part of what produced this image, since it supplies the
Containerfile itself, so two images from an identical cairn.toml can still differ if it
moved between builds. Every image cairn builds carries its full provenance as OCI labels —
resolved commits, the effective build args, that same vendored pin — so docker inspect
(or the listing above) can always answer "what exactly is this" later, long after the
terminal output has scrolled away.
Next steps¶
- Push it, if this deployment uses a registry:
cairn-build push. With no registry configured there's nothing to push to yet — see the manifest's[cairn.registry]orbuilder.toml's registry keys. - Point an environment at it. An environment (
production,staging, …) is just a named, moving registry tag that a target machine watches — this manifest's own declaredenvironment(see Environments, which has a full worked example of what this buys you: build once, let each environment's own manifest prove a match, roll back by repointing).cairn-build assign-tag --manifest /srv/cairn/acmecorp/cairn_production.tomlresolves this manifest's refs, checks the registry, and — only if it finds a match — pointsproductionat it, creating the pointer the first time and moving it every time after. Nothing is rebuilt or pulled either way; movingproductionasks for confirmation first, whether that's the first time it's pointed at anything or the fiftieth. No--latest/--previous/--from— there is only ever one correct answer: what this manifest's own refs currently resolve to. - Adopt it.
cairn-adoptis the target-side binary — it polls the environment's tag and converges on its own next poll once the pointer moves; nothing pushes into the target. See Target for the target-side walkthrough. - Automate it. Once a manual build, push, and pointer move all work,
cairn-build setup-timerinstalls a systemd timer that repeats the same sequence on a schedule unattended — see Build Automation.