Metadata-Version: 2.4
Name: datahenge-cairn
Version: 0.3.3
Summary: Wrapper around frappe_docker for reproducible ERPNext image builds and pull-based deploys on a single VPS.
Project-URL: Homepage, https://github.com/Datahenge/cairn
Project-URL: Repository, https://github.com/Datahenge/cairn
Project-URL: Issues, https://github.com/Datahenge/cairn/issues
Project-URL: Changelog, https://github.com/Datahenge/cairn/blob/main/docs/CHANGELOG.md
Author-email: Brian Pond <brian@datahenge.com>
License-Expression: MIT
License-File: LICENSE
Keywords: deployment,devops,docker,erpnext,frappe
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: System :: Installation/Setup
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.11
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: twine>=6.0; extra == 'dev'
Requires-Dist: ventwig<0.3,>=0.2; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Description-Content-Type: text/markdown

# cairn

A thin, opinionated wrapper around [`frappe/frappe_docker`](https://github.com/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](https://datahenge.github.io/cairn/)**

## 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:

| | **Builder** — `cairn-build` | **Target** — `cairn-adopt` | **Registry** — `cairn-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] environment` |

One `pip install datahenge-cairn` installs all three — see [Get
Started](https://datahenge.github.io/cairn/get-started/) for installing it, and note that 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](#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. See the [Builder
walkthrough](https://datahenge.github.io/cairn/builder/) for provisioning one from scratch
(`cairn-build setup --client <name>`), and the published reference for the full manifest
schema ([cairn.toml](https://datahenge.github.io/cairn/reference/manifest/)), the
machine-local `/etc/cairn/builder.toml` layer and its `CAIRN_*` environment-variable
overrides ([builder.toml](https://datahenge.github.io/cairn/reference/builder-config/)), and
a target's `/etc/cairn/adopt.toml` descriptor
([target descriptor](https://datahenge.github.io/cairn/reference/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](docs/technical/ABOUT_REGISTRIES.md)** for the tradeoffs, and
**[docs/technical/ABOUT_GHCR.md](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

**On a builder** — build, push, and manage environment pointers: see the [Builder
walkthrough](https://datahenge.github.io/cairn/builder/) and [Build
Automation](https://datahenge.github.io/cairn/builder/automation/).

**On a target** (a full walkthrough isn't published yet — this is everything there is to
know for now):

```
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](#where-your-images-live)): see the [Registry
guide](https://datahenge.github.io/cairn/registry/).

## 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](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](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.
