Editions
Based on Fedora 45 COSMIC + Hyprland plus two headless appliances Image-based Fedora — transactional updates, identical on every machine, one-command rollback.Build it yourself
Based on Fedora 45 Clone the repo and run four commands. Every image is a chain of bootc layers, each an OCI image built from its parent.How an image is assembled
Each layer is an OCI image built FROM its parent, so a shared layer is built once and reused by everything below it. The appliance branch skips the desktop entirely. The gold leaves are the published images; intermediates are build infrastructure.
Clone it, then four commands — plain bash and podman
- Get the source.
$ git clone https://github.com/MiguelCarino/Custom-Images && cd Custom-ImagesNeeds git, bash and podman. Fedora has all three. - See the catalog.
$ ./build.sh listEvery image with its DE, description and pin reason. - Generate Containerfiles.
$ ./build.sh generate allChain-aware Containerfiles undergenerated/. - Build the images.
$ ./build.sh build all # --push to publishEach shared layer built once, in dependency order. - Make an ISO.
$ ./build.sh iso carino-gamingbootc-image-builder (needssudo) →output/IMAGE/install.iso.
Classic (non-atomic) images — experimental
./build.sh blueprint IMAGE flattens an image's whole layer chain into an osbuild blueprint for the traditional Workstation backend. It is an export, not a build path — nothing here depsolves or builds it, so you hand it to composer-cli yourself:
$ ./build.sh blueprint carino-general
$ composer-cli blueprints push generated/carino-general/blueprint.toml
$ composer-cli compose start carino-general image-installerA blueprint is a lossy copy, never an equivalent. COPR packages need the repo configured host-side or the depsolve fails; Flatpaks, static files and post-install scripts are dropped; and a service whose unit ships only in the image's own file tree is omitted from enabled, because osbuild runs systemctl enable during the build and a missing unit fails it. Every omission is listed in the TOML header and warned about on the terminal.
Why only COSMIC and Hyprland?
Because every desktop shipped is a desktop that has to be verified against every purpose, and the combinations nobody tests are the ones that break. One session, built once, means the integration work — tearing control, per-output VRR, no file indexer, no automounter — is done once and inherited by all six desktop images.
Both sessions come from the same layer and the same greeter: pick COSMIC at login for pointer-driven use, or Hyprland when you want tiling. Purposes still declare which session they are pinned to, so adding a second desktop later stays a one-line change rather than a rewrite.
The telephony image has no desktop — why?
A PBX is an appliance, not a workstation: you reach it through its web UI and over SSH, and nobody sits in front of it. So carino-pbx pins the headless session, which branches off base and never touches desktop-common — no Wayland, no PipeWire, no portals, no Flatpak, no browser. Fewer packages to patch on the one machine in the building that answers the phones.
One caveat stated plainly: FreePBX itself is not part of the image. It writes to /var, and a bootc image copies /var into the machine once at install and never again — so baking it in would freeze it at the version you first installed from. The image ships Asterisk, MariaDB, Apache and the PHP 8.2 runtime FreePBX requires as RPMs that do update transactionally, and FreePBX installs itself on first boot, then updates with fwconsole ma upgradeall. bootc rollback covers the OS underneath it; it does not roll back a FreePBX module upgrade. Fedora 45 testing is pending: Fedora 45 ships Asterisk 23.5.0 and no older stream, while FreePBX 17's published support stops at Asterisk 22, so that pairing — and the first-boot install itself — is unverified until it has run on a booted machine.
An offline appliance — why is that an image?
carino-offline is the intranet keystone: the one box a LAN with no working WAN still gets names, time, trust, packages and content from. It pins the same headless session carino-pbx does, because it is "an appliance that has to answer on the LAN's own ports before anything else on the network is up: DNS, NTP and HTTP are its interfaces and SSH is its console". The plan this box is one piece of is written up at offline.carino.systems, and that page points back here.
It is an image rather than a Carino Setup package list because five of its decisions are system integration a script run after installation cannot own. Port 53 has to be taken from systemd-resolved, which is installed and preset-enabled on this base, so it is masked rather than disabled — and /etc/resolv.conf, a symlink into resolved's run directory, has to be re-owned in the same breath or the box has no resolver at all. An internal CA needs an anchor directory a site root actually lands in, with update-ca-trust extract run where it takes effect. dnf has to be pointable at a LAN mirror without deadlocking the box's own tooling on a hostname that may not resolve. And chrony has to keep serving time with nothing upstream reachable, which takes one directive whose absence is silent for weeks. Nothing in this image has been booted yet — the package set was resolved against Fedora 45 and those system facts were read inside the built headless layer, but no ISO has been installed.
Why atomic, and what do updates look like?
The value of a purpose-built system is the integration — kernel args, service masking, session configuration — and on a mutable system that integration drifts. With bootc the whole OS is one versioned image: every install is identical, updates are transactional, and a bad one is a bootc rollback away.
$ sudo bootc upgrade # staged, applied on reboot
$ sudo bootc switch <ref> # move between purposes
$ sudo bootc rollback # back to the previous deploymentAdd your own purpose
Create one file, config/purposes/<name>.conf — plain bash, sourced by the build system. Four fields are required, the rest are optional:
PURPOSE="mything" # must match the filename
DE="cosmic-hyprland" # or "headless" for an appliance
PIN_REASON="…" # one line: why this session
DESCRIPTION="…" # one line: what it is for
PACKAGES="pkg @group" COPRS="owner/project" KARGS="…"
REPO_RPMS="https://…/x-release.rpm" # non-COPR third-party repos
SERVICES_ENABLE="a.service" SERVICES_MASK="b.service"
FLATPAKS="org.app.Id" POST_SCRIPT="post.sh"The catalog is derived by scanning config/purposes/*.conf, so ./build.sh list picks it up immediately. Everything that is "just a package list" for an already-installed system lives in Carino Setup instead.