Problem
Building bootc base images currently requires rpm-ostree compose, which ties base image creation to the rpm-ostree ecosystem. This creates a chicken-and-egg problem: bootc aims to be the primary interface for bootable containers, but building the base images still requires rpm-ostree.
Related: #215
Analysis: What does rpm-ostree compose actually do?
I analyzed the rpm-ostree compose pipeline (composepost.rs) and the Fedora 42 bootc base image build system (gitlab.com/fedora/bootc/base-images) to identify exactly which transforms are needed.
Key finding: The Fedora bootc images use rpm-ostree compose rootfs (not compose image). This produces a plain directory that gets packaged as a regular OCI image via FROM scratch + COPY. The ostree commit happens at deployment time, not build time.
I inspected the official quay.io/fedora/fedora-bootc:42 image filesystem and mapped every deviation from a plain dnf --installroot to the transform that caused it. The results:
| Transform |
Needed? |
Notes |
| /etc -> /usr/etc rename |
No |
Done at deploy time by ostree-prepare-root |
| /var -> tmpfiles.d conversion |
Yes |
Walk /var, generate tmpfiles.d entries, clean /var |
| Toplevel symlinks |
Yes |
/home->/var/home, /root->/var/roothome, etc. |
| SELinux subs_dist fixups |
No |
selinux-policy RPM already includes all needed entries |
| Kernel/initramfs config |
Yes |
dracut.conf.d files for ostree/bootc modules |
| rpmdb relocation |
Yes |
Move to /usr/share/rpm, create hardlinks |
| systemd preset application |
Yes |
preset-all after clearing /etc/systemd/system |
Plus config injection: prepare-root.conf, kernel install.conf, dnf5 config, useradd HOME fixup, bootc install config, bootupd metadata.
Two transforms from the original rpm-ostree analysis turned out to be unnecessary: the /etc -> /usr/etc rename (handled at deploy time by ostree-prepare-root) and SELinux file_contexts.subs_dist fixups (the selinux-policy RPM already includes all ostree-specific entries like /usr/etc /etc).
Implementation
I implemented these transforms as bootc container finalize-rootfs and bootc container post-chroot-cleanup subcommands. PR: #2100
The bootc container namespace already has precedent for build-time write operations (ukify, export), and its documented purpose is "Operations which can be executed as part of a container build" -- so this seemed like the natural home.
The workflow:
# 1. Install packages
dnf --installroot=/target --releasever=42 -y install <packages>
# 2. Apply transforms
bootc container finalize-rootfs /target
# 3. Chroot operations (dracut, presets, bootupd)
chroot /target dracut --no-hostonly --kver $(ls /target/usr/lib/modules/) \
--force /usr/lib/modules/$(ls /target/usr/lib/modules/)/initramfs.img
chroot /target systemctl preset-all
chroot /target bootupctl backend generate-update-metadata
# 4. Post-chroot cleanup
bootc container post-chroot-cleanup /target
# 5. Package as OCI
buildah from scratch && buildah copy ... && buildah commit
Test Results
Built a Fedora 42 bootc base image using this workflow (no rpm-ostree at any point). Tested on a GCP VM with nested virtualization for KVM.
Build
Packages installed: 225 (via dnf --installroot)
bootc container lint: 12/12 checks passed (1 cosmetic warning)
bootc install to-disk: Installation complete (GPT, XFS root, ostree deploy, GRUB via bootupd)
Boot (QEMU + KVM)
Login prompt: Reached at 41 seconds
systemctl is-system-running: running
Reboot: Clean, second login prompt confirmed
bootc status output from the booted image
Booted image: localhost/fedora-bootc-e2e:42
Digest: sha256:4580cd48c8df617520b6da30cd4115d21e23db12e98dfd52bc21d5b350bdbfac (amd64)
Timestamp: 2026年03月26日T13:52:56Z
ostree admin status
* default 2c7a693a832cdd9c8bd02bfae894f5c19d68f7203e46d8cc0f29cd196883425b.0
origin: <unknown origin type>
Boot log -- ostree prepare-root and switch-root sequence
[ 0.000000] Command line: BOOT_IMAGE=(hd0,gpt3)/boot/ostree/default-.../vmlinuz-6.19.8-100.fc42.x86_64 console=ttyS0,115200n8 root=UUID=b70e56dd-... rw ostree=/ostree/boot.1/default/.../0
Starting ostree-prepare-root.service - OSTree Prepare OS/...
[ OK ] Finished ostree-prepare-root.service - OSTree Prepare OS/.
[ OK ] Reached target initrd-switch-root.target - Switch Root.
Starting initrd-switch-root.service - Switch Root...
[ OK ] Stopped initrd-switch-root.service - Switch Root.
Starting ostree-remount.service - OSTree Remount OS/ Bind Mounts...
[ OK ] Finished ostree-remount.service - OSTree Remount OS/ Bind Mounts.
[ OK ] Listening on systemd-bootctl.socket - Boot Entries Service Socket.
Filesystem comparison: tool-built image vs official quay.io/fedora/fedora-bootc:42
Toplevel symlinks are identical between both images. The differences are:
- Package count: 225 (our minimal) vs 523 (official standard variant). Expected -- we built a minimal set, the official image is the "standard" variant with networking, SSSD, firmware, python, etc.
- Minor permission differences: /boot dr-xr-xr-x vs drwxr-xr-x, /opt group write bit. Cosmetic.
- Structural layout: Identical. Same ostree conventions, same config files, same rpmdb location.
Findings along the way
- bubblewrap is a hidden dependency: bootupd uses bwrap to sandbox bootloader installation. The official Fedora treefile doesn't list it because it comes as a transitive dep of rpm-ostree (the builder, not the target). When building without rpm-ostree, bubblewrap must be explicitly installed.
- dracut conf.d files must exist before dracut runs: The ostree and bootc dracut modules are only included if the conf.d files requesting them are present. If you skip writing these files, the initramfs won't contain ostree-prepare-root and the system won't boot.
- /root symlink breaks dracut: When /root is a symlink to /var/roothome (which doesn't exist during build), dracut-install fails. Workaround: temporarily create a real /root directory before running dracut, restore the symlink after.
- nss-altfiles doesn't create /usr/lib/passwd: The nss-altfiles RPM installs the NSS module but doesn't create the static passwd/group files. rpm-ostree compose generates these from reference files. When building without rpm-ostree, they must be generated from /etc/passwd and /etc/group.
Questions
- Is
bootc container finalize-rootfs the right place for this? Or should it be a separate tool? The bootc container namespace seemed natural given the existing ukify and export precedents.
- Should the dracut/preset/bootupd chroot operations also be handled by the tool (requiring chroot capability), or is the current split (tool + manual chroot) acceptable?
- How does this relate to the composefs work? With composefs enabled, the deployed rootfs is mounted read-only via erofs. The transforms here operate on the build-time rootfs before it becomes an OCI image, so composefs shouldn't affect the approach -- but I'd like to confirm.
|