The base variant is composed from Fedora packages and nothing else. No inherited bootc parent stands behind it, so the shipped rootfs holds exactly what the compose put there, and its Containerfile is public. The layered variants add source-built kernel modules on top.

The compose

THREE STAGES, ONE COPY thrown away in full the image that ships STAGE 1 repos Fedora repo + GPG STAGE 2 builder compose rootfs STAGE 3 FROM scratch one COPY

The builder only produces the composed rootfs; nothing else enters the image.

rpm-ostree compose rootfs runs against manifests/base.yaml under container=systemd-nspawn, which skips an unshare-net selftest that fails under rootless podman. bootc container lint runs in CI on the finished image, and any warning fails the build. The treefile fixes the image’s shape first: no documentation, no weak dependencies, read-only executables, and a normalized rpmdb so the same input composes to the same bytes.

Two settings carry all the way downstream. postprocess.yaml writes /usr/lib/ostree/prepare-root.conf with composefs enabled and the sysroot read-only. cairn pulls in no upstream minimal/ostree.yaml, so without that write the image would boot a read-write sysroot, and the sealed root that /architecture/sealing/ rests on starts at this line. System ids freeze at a committed reference: manifests/passwd and manifests/group are seeded through rpm-ostree’s check-passwd and check-groups before any package installs, so every package adopts the frozen id (records D2 and D3). The reserved blocks, and why an instance pins no band, are D4 and D23 in docs/decisions.md.

The variants

EACH VARIANT ADDS ONE THING registry.gitlab.com/dunn.dev/cairn/base/<variant> LAYER 3 base-zfs-nvidia NVIDIA open-gpu kernel modules, container toolkit LAYER 2 base-zfs source-built OpenZFS, the cairn-verify binary LAYER 1 · COMPOSED FROM SCRATCH base Fedora bootc minimal, systemd stack, no kernel modules

A ZFS host and a GPU host share one composed base, so a foundation fix reaches both on rebuild.

Every variant publishes a per-commit tag. base and base-zfs-nvidia each earn :stable and :stable-sealed by booting their own sealed artifact; base-zfs:stable moves with base-zfs-nvidia’s.

Kernel modules are compiled from source against the exact kernel the compose installed, never DKMS and never akmods. modules/zfs builds OpenZFS against the matching kernel-devel because upstream ships no Fedora repository, and modules/nvidia does the same for NVIDIA’s open-gpu-kernel-modules. Both builders install into /staged; the consuming layer takes it with one COPY.

A SOURCE-BUILT MODULE INTO A SHIPPED IMAGE THROWAWAY STAGE the kmod builder compile, then sign STAGED TREE /staged signed modules + tools LAYERED IMAGE base-zfs COPY /staged/ / GUARD single-kernel sweep exactly one modules dir GUARD cairn-verify shipped files vs the manifest

The copy cannot reach the compose-built initramfs, and both guards fail the build rather than a host.

Both guards are in the tree: the single-kernel sweep in images/base-zfs/Containerfile, and cairn-verify, which checks the shipped files against a manifest derived at build time from the builders’ own install records, so a stray dnf install cannot clobber a source-built module unseen. Whether a loaded module is trusted (which kernel keyring its signer resolves in) is a separate gate at /architecture/pipeline/.

What the image carries

An image is a public artifact, so it carries only what is safe to publish.

THE LINE AROUND A PUBLIC ARTIFACT baked in, public Secure Boot auto-enroll payloads the kmod-signing certificate composefs, read-only sysroot console kargs, dracut drop-ins a blank machine-id package exclusions never baked private key material runtime encryption keys ZFS pool keys the instance adds sysctl policy a firewall ruleset service users secrets and app config mounts and SELinux rules

The first column is publishable on purpose, the second cannot live in a public artifact, and the third is the host's own.

The Secure Boot auto-enroll payloads sit at /usr/lib/bootc/install/secureboot-keys/auto/. The auto basename is the trigger systemd-boot watches, and bootc’s install path copies that tree verbatim to the ESP. They hold public certificates only, so baking them under the sealed composefs digest gives the boot chain its own enrollment material (D19). The kmod-signing anchor /usr/share/cairn/kmod-signer.der is the same certificate that signs systemd-boot and every UKI, so no second signing identity exists (D29), and its private half lives in a masked CI variable that never reaches an image layer. Who minted them and how to check them is the trust chain; every anchor is at /keys/.

Only the os-release PRETTY_NAME line is rewritten, because a sealed UKI’s .osrel section is what systemd-boot’s menu shows; ID and VERSION_ID stay.

Package exclusions are /etc/dnf/dnf.conf.d/ drop-ins numbered by the layer that added them, so a derived image’s dnf install cannot pull back what a lower layer excluded. cairn ships the nftables package and unit with no ruleset and leaves it disabled, so a host has no firewall until its instance gives it one, a trade I accepted, mitigated by key-only SSH.

/usr/local is part of the read-only image: install software in a Containerfile or container. tun loads at boot. Rootless tun under Fedora 44’s container policy needs setsebool -P container_use_devices on; the denial logs no AVC.

To test for a bootc host on either backend, run this, not grep -w composefs=, which the digest after = defeats:

[ -e /run/ostree-booted ] || grep -qE '(^| )composefs=' /proc/cmdline

In a unit: ConditionPathExists=|/run/ostree-booted and ConditionKernelCommandLine=|composefs.

Annotated at the step

Every operation in every image Containerfile carries a cf:<slug> block: What, Why and Upstream always, Validated where a live check backs the step, and Decision where the step enacts a record. When cairn diverges from upstream extension points, the block says so at the step, as the systemd-nspawn selftest skip does in cf:compose.