Bootc Image Configuration#
Configuration in Bluefin-based bootc systems (Bluefin, Dakota, finpilot, Bluefin LTS) is embedded in the OCI image at build time — not applied post-install. The deployed root filesystem is read-only; the three writable tiers are:
| Tier | Path | Mutability | Upgrade behavior |
|---|---|---|---|
| Image defaults | /usr/etc | Read-only (image) | Reset on image update |
| Machine overrides | /etc | Mutable | 3-way merged on upgrade |
| User overrides | ~/.config, ~/.local | Mutable | Persists per-user |
/var is not 3-way merged — it uses Docker VOLUME semantics. Use systemd-tmpfiles.d to pre-create directories under /var. Prefer /usr over /etc where software supports both, and use drop-in directories (e.g., /etc/sudoers.d/) to minimize state drift.
See the bootc filesystem docs for authoritative details.
Multi-Stage OCI Build Pattern#
All Bluefin-based images use a context-stage multi-stage Containerfile that separates concerns cleanly across layers. The finpilot Containerfile is the canonical template:
ctx (scratch stage)
├── /build ← local build scripts
├── /custom ← local custom files
├── /oci/common ← imported from ghcr.io/projectbluefin/common
└── /oci/brew ← imported from ghcr.io/ublue-os/brew
base image (e.g. quay.io/fedora-ostree-desktops/silverblue:44)
└── RUN /ctx/build/00-image-info.sh
└── RUN /ctx/build/10-overlay.sh
└── RUN /ctx/build/20-packages-and-services.sh
└── RUN /ctx/build/90-cleanup.sh
└── RUN bootc container lint --fatal-warnings
Build scripts run in numeric order with ctx bind-mounted read-only:
| Build Phase | Script | Purpose |
|---|---|---|
| Identity | 00-image-info.sh | Generates /usr/share/ublue-os/image-info.json, patches os-release |
| Overlay | 10-overlay.sh | Copies common runtime layer, brew integration, custom declarations; enables systemd units |
| Packages | 20-packages-and-services.sh | Installs RPMs/COPR packages, enables services — kept separate so overlay edits don't invalidate the package layer |
| Cleanup | 90-cleanup.sh | Removes .repo files and build artifacts so repos (non-functional at runtime) don't litter the image |
| Lint | inline | bootc container lint --fatal-warnings enforces image correctness |
Separating 20-packages-and-services.sh from the overlay phase is intentional — filesystem overlay changes should not bust the expensive package layer cache.
Key identity ARG values set at build time: IMAGE_NAME, IMAGE_VENDOR, UBLUE_IMAGE_TAG, BASE_IMAGE_NAME, VERSION. Volatile metadata (SHA_HEAD_SHORT, IMAGE_CREATED) is declared late so new commits don't invalidate package/overlay layers.
Supported base images:
| Base | Description |
|---|---|
quay.io/fedora-ostree-desktops/silverblue | Fedora, GNOME desktop |
quay.io/fedora-ostree-desktops/base-main | Fedora, no desktop |
quay.io/centos-bootc/centos-bootc:stream10 | CentOS-based (Bluefin LTS) |
quay.io/hummingbird-community/bootc-os | Hummingbird-based, minimal |
system_files Overlay Layers (projectbluefin/common)#
projectbluefin/common organizes all shared configuration as three filesystem overlays merged at build time; later layers win on conflicts:
| Layer | Applied to | Contains |
|---|---|---|
shared/ | All variants (Bluefin, Dakota, knuckle, forks) | systemd units, udev rules, shell utilities, OEM hooks |
bluefin/ | GNOME desktop variants only | dconf defaults, GNOME Shell extensions, branding, ujust recipes |
nvidia/ | NVIDIA GPU variants | GPU-specific runtime configuration |
The Containerfile also copies in build-time generated artifacts: ujust completions, YubiKey U2F / game-device udev rules (SHA256-verified), and the umotd binary.
To audit local configuration drift after deployment, run:
ujust check-local-overrides
This diffs /usr/etc against /etc to surface machine-local divergence.
Declarative Kernel Arguments#
Kernel arguments are declared as TOML files embedded in /usr/lib/bootc/kargs.d/ inside the image — replacing the legacy grubby runtime approach:
# /usr/lib/bootc/kargs.d/framework-intel.toml
kargs = ["module_blacklist=hid_sensor_hub"]
For install-time machine-specific injection (serial consoles, extra mounts, networking), pass --karg to bootc install:
bootc install to-disk --karg console=ttyS0,115200 /dev/sda
bootc install to-existing-root --karg="systemd.mount-extra=UUID=:/data:xfs:defaults"
For UKI setups, use signed UKI addons to provide machine-specific kargs without invalidating the signed image.
Cleanup of legacy grubby kargs is handled in the OEM hooks — see system-setup.hooks.d/10-framework.sh in projectbluefin/common.
First-Boot Tooling Integration#
Several systemd units embedded in the image manage first-boot activation:
| Unit | Trigger | Scope | Purpose |
|---|---|---|---|
ublue-system-setup.service | After rpm-ostreed.service | system | Runs OEM/hardware setup hooks as root (oneshot) |
flatpak-preinstall.service | After network-online.target | system | Installs Flatpaks declared in /etc/flatpak/preinstall.d/ (retries 3×, 30s apart) |
brew-setup.service | First boot | system | Extracts the pre-staged Homebrew tarball |
brew-preinstall.service | Every login, after brew available | user | Content-addressed Brewfile sync — only runs brew bundle when Brewfiles have changed |
rechunker-group-fix.service | Before systemd-sysusers | system | Fixes group/shadow file issues for legacy rechunker compatibility |
OEM Hardware Hooks#
Hooks in system-setup.hooks.d/ fire via ublue-system-setup.service and detect hardware via DMI strings:
10-framework.sh— Framework laptops: applies audio jack (AMD) and suspend (Ryzen 7040) fixes; cleans up legacygrubbykargs11-asus.sh— ASUS hardware: enablesasusd.service
Hooks exit immediately on non-matching hardware, so runtime cost is negligible.
Package Manager Pre-Staging#
Neither Homebrew nor Flatpak is invoked during the OCI image build. Instead:
- Homebrew: The
ghcr.io/ublue-os/brewOCI layer is copied in at build time as a tarball + systemd units.brew-setup.serviceextracts it on first boot. Custom Brewfiles go incustom/brew/*.Brewfileand are staged to/usr/share/ublue-os/homebrew/at build time. - Flatpak:
.preinstallINI files fromcustom/flatpaks/are staged to/usr/share/flatpak/preinstall.d/at build time.flatpak-preinstall.serviceinstalls them after network comes online.
Flatpaks are not reinstalled during bootc switch — only on first boot with network access.
ISO-Embedded Configuration#
Titanoboa (Bluefin ISO Builder)#
Titanoboa implements the Container-native ISO contract v0.1.0 — all ISO customizations must be embedded in the container image itself, not applied by the ISO builder.
Required files in the image:
| Path | Content |
|---|---|
/usr/lib/bootc-image-builder/iso.yaml | ISO label, GRUB2 boot entries, kernel args |
/usr/lib/modules/*/vmlinuz | Kernel |
/usr/lib/modules/*/initramfs.img | Initramfs |
/boot/efi/EFI/$VENDOR/ | EFI binaries (shim, grub) |
/usr/lib/grub/i386-pc | GRUB2 modules |
iso.yaml defines the GRUB2 menu including kernel arguments inline in the linux: field. Titanoboa is consumed as a GitHub Action — see the titanoboa README for integration details.
Dakota ISO#
The Dakota ISO build embeds installer configuration in /etc/bootc-installer/ at container build time:
| File | Purpose |
|---|---|
images.json | Locks the installer image catalog to Dakota; sets bootloader: "systemd", composefs: true, needs_user_creation: false |
recipe.json | Distro branding, tour slides, imgref (base), local_imgref (offline VFS store ref) |
The OCI payload image is embedded as VFS containers-storage inside the squashfs — no network pull is needed at install time. driver = "vfs" is set in /etc/containers/storage.conf because composefs targets require it.
The live environment is built in three stages:
live/Containerfile— pulls Dakota NVIDIA image, configures GDM autologin, installs Flatpaks, drops in installer configscripts/build-live-squashfs.sh— squashes to one layer, imports into VFS containers-storage, runsmksquashfslive/src/build-iso.sh— assembles final ISO with squashfs and boot files
Key invariant: The VFS store at
var/lib/containers/storageinside the squashfs must be populated withdakota-nvidia:stablebefore ISO upload — an empty store causes install failure even with network access.
Customizing with finpilot#
projectbluefin/finpilot is the official template for building custom Bluefin-derived images. Fork it and edit the following:
| What to customize | Where |
|---|---|
| Base image | FROM line in Containerfile |
| Image identity | ARG IMAGE_NAME, IMAGE_VENDOR, UBLUE_IMAGE_TAG |
| RPM packages | build/20-packages-and-services.sh |
| COPR packages | build/20-packages-and-services.sh via copr_install_isolated |
| CLI tools (Homebrew) | custom/brew/*.Brewfile |
| GUI apps (Flatpak) | custom/flatpaks/*.preinstall |
| Additional systemd units | build/10-overlay.sh → systemctl enable … |
| ujust recipes | custom/just/ |
Key constraint: Clean up .repo files added during build — DNF repositories are non-functional at runtime, and bootc container lint --fatal-warnings will catch leftover repo artifacts.
For a package-adding example with COPR, see how 20-packages-and-services.sh installs uupd via copr_install_isolated.
Key References#
| Resource | Link |
|---|---|
| finpilot template | projectbluefin/finpilot |
| finpilot Containerfile | Containerfile |
| common system_files | projectbluefin/common — system_files/ |
| Declarative kargs example | framework-intel.toml |
| Titanoboa ISO builder | ublue-os/titanoboa |
| Dakota ISO pipeline | projectbluefin/dakota-iso |
| bootc filesystem docs | bootc-dev.github.io/bootc/filesystem.html |
| bootc install docs | bootc-dev.github.io/bootc/bootc-install.html |
| bootc image building guidance | bootc-dev.github.io/bootc/building/guidance.html |
| Bluefin Administration Guide | docs.projectbluefin.io/administration |