Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Building bootc base images without rpm-ostree compose #2101

andrewdunndev started this conversation in Ideas
Discussion options

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

  1. 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.
  2. 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?
  3. 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.
You must be logged in to vote

Replies: 1 comment

Comment options

Update: validated end-to-end on Fedora 42

Following up with results from building and deploying this approach in production CI.

What we built

A FROM-scratch Fedora 42 bootc base image using dnf --installroot with the transforms from PR #2100 encoded as Containerfile RUN commands. 255 packages, 886 MB (vs 523 packages, ~2.1 GB for the upstream standard tier). No rpm-ostree at any point in the build.

The transforms are the same ones documented in the PR description: toplevel symlinks, /var tmpfiles.d generation, nss-altfiles /usr/lib/passwd, dracut conf.d + initramfs, systemctl presets, bootupd metadata, rpmdb relocation, ostree prepare-root.conf.

Validation

Tested on GCP (n2-standard-8, Fedora 42, nested KVM) across 11 categories:

Category Result
Boot + reboot (5 cycles) PASS
Journal persistence PASS (required adding Storage=persistent -- upstream minimal/minimal-plus tiers don't include this)
User management (useradd, nss-altfiles runtime, sudo) PASS
bootc switch + rollback (against live registry) PASS
Rootless podman (service user, linger, subuid/subgid) PASS
Console access (tty1 + ttyS0) PASS
Locale/timezone PASS
SELinux (targeted, enforcing, zero AVCs) PASS
Essential CLI tools PASS
tmpfiles.d completeness PASS
ZFS + NVIDIA kernel module layers (multi-stage build, unmodified) PASS

Findings

  1. Persistent journal is missing from the minimal and minimal-plus tiers. Storage=persistent in journald.conf is only in the standard tier. Anyone building from minimal gets volatile journal -- logs lost on reboot. We add it explicitly.

  2. bubblewrap is a hidden dependency. bootupd uses bwrap for sandboxing. It's normally a transitive dep of rpm-ostree (the builder), but since we skip rpm-ostree, it must be explicitly installed.

  3. The transforms work without the PR. We encoded them as shell commands in the Containerfile. When/if finalize-rootfs or a separate base-imagectl tool lands, the Containerfile simplifies, but it's not blocking.

  4. CI on rootless podman requires --cap-add=SYS_ADMIN on buildah build. The chroot operations (dracut, bootupd) need mount(2). buildah's chroot isolation denies it even in privileged containers. buildah build --cap-add=SYS_ADMIN passes the capability through to RUN sub-processes.

  5. Multi-stage kernel module builds layer on top unchanged. ZFS (source build) and NVIDIA (source build from open-gpu-kernel-modules) both work on the FROM-scratch base with zero modifications to the downstream Containerfiles.

Re: cgwalters' review on PR #2100

Understood that bootc shouldn't own these transforms. The lint --fix approach for generic pieces (tmpfiles.d) makes sense. For the rest, a separate base-imagectl tool (or just documented shell commands) works. The seven transforms are well-defined and stable -- they haven't changed between the PR submission and this production deployment.

Blog post with the full technical details: https://andrew.dunn.dev/writing/building-bootc-from-scratch/

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Ideas
Labels
None yet
1 participant

AltStyle によって変換されたページ (->オリジナル) /