Skip to main content

Wrapper around frappe_docker for reproducible ERPNext image builds and pull-based deploys on a single VPS.

Project description

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 command.

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.

Status

Working, unreleased. The build and deploy commands are implemented and tested; cairn build, push, images, prune, doctor, and vendor have been exercised against real images and a real registry, while the pointer verbs (new-tag / retag / retire) and the target-side reconcile are written and tested but have not yet run against live infrastructure.

Two roles, one install

cairn is one tool with two roles, told apart by what's configured on a machine rather than by what's installed:

Builder Target
Does Builds images, pushes them, moves environment pointers Polls for its pointer, pulls the image, converges the running stack
Reads cairn.toml (the manifest) /etc/cairn/environment.toml (a descriptor, generated by cairn adopt)
Needs Docker Engine v23+ or podman v4+, git Docker Engine + docker compose
Credential push access to the registry pull-only

The same pip install datahenge-cairn gives you every command either role needs — a target simply never has a reason to run the build-only commands, and its pull-only registry credential means it couldn't push or retag even if asked.

Installation

For a machine anything else depends on — a client's builder or target VPS, not a laptop you alone operate — install system-wide rather than into your own account:

sudo pipx install --global datahenge-cairn

This matters specifically if you're a consultant rather than an in-house operator: pipx --global installs to a shared system location (/opt/pipx by default), not under your personal home directory. A plain personal pip install/pipx install works too and is fine for a builder you alone use — but a client's production systemd timers shouldn't depend on your individual account still existing.

pip install datahenge-cairn        # fine for a personal/experimental install

Either way, that's it for the cairn command itself, on either a builder or a target. It carries everything a build needs, including the vendored image-build recipe.

To actually build an image, the machine additionally needs:

  • Docker Engine v23+ (with buildx) or podman v4+ — auto-detected, docker preferred when both are present.
  • git — every manifest ref is resolved to a commit at build time.

Run cairn doctor after installing to check all of this at once — it reports every missing prerequisite in one pass rather than failing partway into a long build.

To run a target (the machine that actually serves ERPNext), you additionally need Docker Engine and docker compose v2, and a Frappe/ERPNext stack already running via frappe_docker — cairn converges an existing deployment, it doesn't create one from nothing.

Provisioning a whole machine

For standing up a fresh builder or target VPS — not just the cairn command, but the registry, TLS certificate, systemd timers, and (for a target) a pre-install backup — cairn-provision installs alongside cairn automatically; there's no separate download or checkout needed. Run it once with root, from the directory that will hold (or already holds) your deployment's cairn.toml:

sudo cairn-provision --role builder --dry-run   # see what it would do first
sudo cairn-provision --role builder

--role is builder, target, or both (one box doing each — the common starting point). It needs root on every role: a builder writes a TLS certificate and trusts it system-wide, and both roles install systemd units under /etc/systemd/system; a target additionally writes its descriptor under /etc/cairn. --dry-run prints every command it would run and writes nothing, so you can review it before handing over root. It is idempotent — safe to re-run — and never silently overwrites a file it didn't create; anything it would replace is kept alongside, renamed.

For development

Contributing to cairn itself, or re-syncing the vendored upstream, needs a checkout:

git clone https://github.com/Datahenge/cairn.git
cd cairn
python3 -m venv .venv
.venv/bin/pip install --editable '.[dev]'

The dev extra adds ventwig (vendoring), ruff, and pytest — none of which a normal install needs.

Configuration

The manifest — cairn.toml

One file declares one image: the Frappe source, the ordered list of apps, and build knobs. It's meant to be committed and shared — it carries no machine- or registry-specific settings.

[cairn]
image_name = "erpnext-btu-v16"
series = "v16"                      # the readable half of the image tag

[cairn.registry]
host      = "ghcr.io"
namespace = "your-org"

[cairn.frappe]
url = "https://github.com/frappe/frappe"
ref = "version-16"

# Order matters: apps install in this order, and cairn never reorders or resolves
# dependencies for you. List every app after the apps it depends on.
[[cairn.apps]]
name = "erpnext"
url = "https://github.com/frappe/erpnext"
ref = "version-16"

[[cairn.apps]]
name = "your_custom_app"
url = "https://github.com/your-org/your_custom_app"
ref = "version-16"

[cairn.build]
python_version = "3.14.2"
node_version = "24.13.0"
install_chromium = true

[cairn.environments]
production = "production"
staging    = "staging"

cairn discovers cairn.toml by walking upward from the current directory, or you can point at one explicitly with --manifest.

Machine-local settings

Registry coordinates live in the manifest above deliberately — they describe the deployment, not the machine building it. What's genuinely machine-local (which build engine to use, an alternate transcript directory) lives in two layers instead, lowest precedence first:

  1. ~/.config/cairn/config.toml — machine-wide defaults.
  2. cairn.local.toml, beside the manifest, never committed — a per-checkout override, e.g. to build with podman on a laptop that has no Docker daemon.
# cairn.local.toml
engine = "podman"

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 ABOUT_REGISTRIES.md for the tradeoffs, and 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.

The target's descriptor

A target doesn't hand-author configuration. Run cairn adopt against its running frappe_docker stack, review the descriptor it prints, and install it yourself at /etc/cairn/environment.toml. That descriptor — not the manifest — is what cairn reconcile reads, and its presence is what marks a machine as a target at all.

How to use

On a builder:

cairn doctor                 # confirm the machine can actually build
cairn build                  # build the image declared by cairn.toml
cairn build --push           # ...and upload it
cairn images --local         # what's on this machine, and which builds are superseded
cairn 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 new-tag staging --latest       # point staging at the newest build
cairn retag production --from staging --yes   # promote staging's image to production
cairn retag production --previous            # roll back production one image
cairn images                                 # what the registry holds, and which tags point where

On a target:

cairn adopt                     # describe this host's running stack (one-time, or after a manual change)
cairn systemd-units             # print the reconcile service + timer; review, then install them
cairn reconcile --dry-run       # see what would change
cairn 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.

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:

📦 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.

🐙 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.

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.

Design docs

Doc Contents
docs/00-project-scope.md Purpose, pillars, what it is/isn't, principles
docs/01-decisions-closed.md Settled decisions with rationale
docs/02-decisions-open.md Unresolved questions + current leans
docs/03-discussion-log.md Chronological design reasoning

Vendored upstream

src/cairn/vendored/frappe_docker/ is a pinned, read-only copy of upstream, vendored as plain committed files via ventwig and never edited by hand. It lives inside the cairn package itself, so it's part of every install — pip or checkout alike. It's pinned to upstream release tag v3.2.1.

cairn vendor status   # verify the vendored tree is clean / unmodified
cairn vendor sync      # re-materialize from the pinned ref (a checkout only)

To upgrade upstream: bump ref in pyproject.toml, cairn vendor sync, review the diff, test-build, then commit — including the regenerated frappe_docker.pin.toml alongside it.

Project details


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.1.0.tar.gz (551.2 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.1.0-py3-none-any.whl (439.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: datahenge_cairn-0.1.0.tar.gz
  • Upload date:
  • Size: 551.2 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.1.0.tar.gz
Algorithm Hash digest
SHA256 4b0dd7f14ca0b3413d8b38118a67038b24cb79fdc30390a45dc1ac97af4d1736
MD5 c0c2b4bb9a8cf9084e167328d80dbb48
BLAKE2b-256 d795027bd103884dce1f21d77aec6f42434d4342666c875e9db989c0465882f3

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for datahenge_cairn-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7348a22ccef8017b8348dfb2fb1d2db211b0140bdc4874f3c6680580700e10f8
MD5 6cd61456ee57992bf86501ae5472d212
BLAKE2b-256 b1b22c5168432b8b880fd8c9b9905f132ae953f44c19dc5abbe994d3fba3614a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page