Skip to main content

cairn

A thin, opinionated wrapper around frappe/frappe_docker that makes running a custom ERPNext deployment (Frappe + ERPNext + custom apps) on a single VPS reproducible, immutable, and low-thought — without ever modifying upstream.

Distributed as datahenge-cairn on PyPI — installs the cairn-build, cairn-adopt, and cairn-registry commands.

Two pillars: reproducible custom image builds and a pull-based deploy lifecycle (git ref → image tag → running stack, with image-only rollback). A strict data-plane boundary keeps cairn out of your databases and volumes entirely — it ships code, not data.

Cairn: a trail marker of stacked stones. Each deploy drops a durable marker (ref → resolved commits → image tag → digest) you can navigate back to.

📖 Documentation

Three roles, one install

cairn is three commands, one package, told apart by what's configured on a machine rather than by what's installed:

Buildercairn-build Targetcairn-adopt Registrycairn-registry
Does Builds images, pushes them, moves environment pointers Polls for its pointer, pulls the image, converges the running stack Provisions and operates a local OCI registry: lifecycle, retention, garbage collection
Reads cairn.toml (the manifest) /etc/cairn/adopt.toml (a descriptor, generated by cairn-adopt examine) /etc/cairn/registry.toml (optional — built-in defaults otherwise)
Needs Docker Engine v23+ or podman v4+, git Docker Engine + docker compose Docker Engine + docker compose, openssl
Credential push access to the registry pull-only none — reads no manifest, no [cairn.environments]

The same pip install datahenge-cairn installs all three — a target simply never has a reason to run cairn-build's commands, and its pull-only registry credential means it couldn't push or retag even if it did. cairn-registry is only needed at all if you choose the self-hosted local-registry option (see Where your images live) — it is independent of the other two roles and is sometimes colocated with a builder or target, sometimes not.

Configuration

One manifest declares the image (cairn.toml, committed with the deployment); machine- local build settings, if you need any, live separately and are never shared:

# cairn.toml
[cairn]
image_name = "erpnext-v16"
series = "v16"

[cairn.frappe]
url = "https://github.com/frappe/frappe"
ref = "v16.25.0"

[[cairn.apps]]
name = "erpnext"
url = "https://github.com/frappe/erpnext"
ref = "v16.26.1"

ref takes a tag or a branch. A tag is reproducible — the same tag always resolves to the same commit. A branch such as version-16 is a moving pointer: it always builds that branch's newest commit, which is convenient if what you actually want is "always the latest release," at the cost of two builds from the same manifest potentially producing different images. cairn warns, but does not refuse, when a manifest pins to a branch.

Every command names its manifest explicitly — --manifest <path>, or $CAIRN_MANIFEST if you'd rather not repeat the flag. cairn never searches a directory for one: on a shared machine, "the nearest cairn.toml" is a silent way to act on the wrong deployment, not a convenience. There's no standalone scaffolding command — cairn-build setup --client <name> writes a starter manifest to /srv/cairn/<name>/cairn.toml, but only as one step of provisioning a whole build machine, and only if none exists there yet. Otherwise, hand-write one, starting from the example above.

See the published reference for the full manifest schema (cairn.toml), the machine-local /etc/cairn/builder.toml layer and its CAIRN_* environment-variable overrides — what each key means, how they're created, and how precedence works — and sharing /etc/cairn across several operators (builder.toml), and how a target's /etc/cairn/adopt.toml descriptor comes from cairn-adopt examine rather than being hand-authored (target descriptor).

Where images are pushed

Which registry you use, and who owns the credential, is worth thinking about deliberately — especially when you're building images for a client. See docs/technical/ABOUT_REGISTRIES.md for the tradeoffs, and docs/technical/ABOUT_GHCR.md for GitHub's registry specifically. cairn itself is registry-agnostic and stores no credentials — authenticate with docker login or podman login before pushing.

How to use

The examples below assume $CAIRN_MANIFEST is already exported for the session (e.g. export CAIRN_MANIFEST=/srv/acme/cairn.toml) — add --manifest <path> to any of them instead if you'd rather not.

On a builder:

cairn-build doctor                 # confirm the machine can actually build
cairn-build build                  # build the image declared by the manifest
cairn-build build --push           # ...and upload it
cairn-build images --local         # what's on this machine, and which builds are superseded
cairn-build prune                  # remove superseded local images (keeps build-cache layers)

Moving an environment's pointer — which is how you deploy, promote, or roll back — never rebuilds or re-pulls anything; it just writes a tag in the registry:

cairn-build new-tag staging --latest       # point staging at the newest build
cairn-build retag production --from staging --yes   # promote staging's image to production
cairn-build retag production --previous            # roll back production one image
cairn-build images                                 # what the registry holds, and which tags point where

On a target:

cairn-adopt examine             # describe this host's running stack (one-time, or after a manual change)
cairn-adopt systemd-units       # print the reconcile service + timer; review, then install them
cairn-adopt reconcile --dry-run # see what would change
cairn-adopt reconcile           # converge to whatever the environment's pointer says

reconcile is idempotent and meant to run on a timer — with nothing to do, it does nothing. It never rolls back on failure; it stops and reports, because a failed bench migrate is not something to silently reverse.

On a registry host (only if you self-host — see Where your images live):

cairn-registry doctor            # confirm the registry is reachable, its cert is valid, disk has room
cairn-registry images            # what's in the registry, and which tags point where
cairn-registry prune --dry-run   # what retention would delete, without deleting anything
cairn-registry gc --dry-run      # what garbage collection would reclaim

Where your images live

cairn builds an image and puts it in a container registry; your deployment targets pull from there. cairn is registry-agnostic and assumes nothing — but which registry is not a neutral choice when you build software for clients:

📦 docs/technical/ABOUT_REGISTRIES.md — start here. The image belongs in the account that owns the source; your credential should reach the engagement's images and nothing else; and what each option costs at ERPNext image sizes. Includes what to ask a client for.

🐙 docs/technical/ABOUT_GHCR.md — GitHub's registry in detail: tokens, scopes, how narrow access can be, visibility, and the deletion rule that is genuinely surprising. One option among several, not the default.

If you choose to self-host — cost dominates and off-host rollback history is genuinely not needed — cairn-registry provisions and operates that registry: lifecycle, retention, and garbage collection, so disk use stays bounded. See cairn-registry --help.

You should never be the sole owner of a client's image. If the relationship ends, they must still be able to deploy and roll back software they own. cairn is built so the registry can be an account you do not control, and so your push credential can be scoped to one repository.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

datahenge_cairn-0.2.1.tar.gz (678.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

datahenge_cairn-0.2.1-py3-none-any.whl (467.2 kB view details)

Uploaded Python 3

File details

Details for the file datahenge_cairn-0.2.1.tar.gz.

File metadata

  • Download URL: datahenge_cairn-0.2.1.tar.gz
  • Upload date:
  • Size: 678.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for datahenge_cairn-0.2.1.tar.gz
Algorithm Hash digest
SHA256 1a9634a63e90f7c6a941bd725d5a3bfe59add95b839c93718411d915fb02837d
MD5 8b57e461921eb21b8930f62bda266e5a
BLAKE2b-256 ffc4e4cb9b03de893bff04d46bae8760da3a2f92558ea11715608b781cff2432

See more details on using hashes here.

File details

Details for the file datahenge_cairn-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for datahenge_cairn-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d0b62cd83530b8ced4bdc0bc26114745e83b8a77bcbad72caf41aca134464cbc
MD5 e90bad012273d50e6550130fcf3db328
BLAKE2b-256 555c3de194ffc54bd08dd3d2f419e1b982892e5ab2cd29514a4a8185815595c9

See more details on using hashes here.

Release history Release notifications | RSS feed

0.4.12

2 files

0.4.11

2 files

0.4.10

2 files

0.4.9

2 files

0.4.8

2 files

0.4.7

2 files

0.4.6

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.8

2 files

0.3.7

2 files

0.3.6

2 files

0.3.5

2 files

0.3.4

2 files

0.3.3

2 files

0.3.1

2 files

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page