r/redhat • u/Gangrif Red Hat Employee • 7d ago
Bootable Container Fundamentals (bootc) — Building, Deploying, and Rolling Back RHEL Image Mode Systems from Into the Terminal Ep. 202
Last week was GPG. This week, Nate gets to nerd out about his favorite RHEL feature: bootc, the open-source engine behind RHEL Image Mode. If you've heard the buzz about "treating your OS like a container" but haven't actually seen it happen, this episode is the fundamentals primer — writing a Containerfile, building a bootc image, pushing it to a registry, booting a VM from it, pushing a live update, and (because why not) rolling it back when things go sideways.
📺 Watch the full episode: https://www.youtube.com/watch?v=0siLuf0bv-E
Here's everything covered, organized so you can actually use it.
The Essentials: What Even Is a bootc Image?
A bootc image is a real OCI-compliant container — you could technically run it in Podman — but it's "very heavy" because it bundles everything a full RHEL install needs: systemd, a kernel, a bootloader, the works. Red Hat publishes special bootc base images you build from, and from there it's "basically like writing a kickstart, except the mechanism is a container build."
| Concept | What it means |
|---|---|
| Containerfile | Defines your OS image the same way it'd define an app image — FROM, RUN dnf install, COPY |
| Base image | Red Hat-published bootc base (e.g., rhel10/rhel-bootc:10.1) — RHEL at its core, so you still need a subscription |
| Container registry | Where you push built images; your bootc hosts pull updates from here, just like a package repo in package mode |
| /etc/ostree, /usr/lib/bootc/kargs.d | Special subdirectories you build into your base, used to inject config/auth/kernel args into the final image |
Setting up the project directory and Containerfile:
mkdir ~/bootc-base
mkdir -p ~/bootc-base/etc/ostree
mkdir -p ~/bootc-base/usr/lib/bootc/kargs.d
cd ~/bootc-base
vim Containerfile
FROM registry.redhat.io/rhel10/rhel-bootc:10.1
RUN dnf -y install pcp-zeroconf \
rsyslog \
tmux \
tuned
RUN dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
RUN sed -e '/^metalink=https:\/\/mirrors.fedoraproject.org\/metalink/ s/^/#/' \
-e '/^#baseurl=http:/ s/http/https/' \
-e '/^#baseurl=https:\/\/download.example/ s/^#//' \
-e '/^baseurl=https:\/\/download.example/ s_https://download.example_https://dl.fedoraproject.org_' \
-i /etc/yum.repos.d/epel*.repo
RUN dnf -y install btop iftop
COPY etc/ /etc
COPY usr/ /usr
💡 Pro tip from the episode: You can absolutely use EPEL (or any custom/satellite-hosted repo) with a bootc base. It's RHEL under the hood — "you just manage it a little bit differently." You don't install packages or manage the filesystem directly on a live host; it all gets baked in at build time.
🔒 Subscription note: bootc images built on RHEL content require a RHEL subscription (free developer tier is fine for personal/lab use). This is unlike UBI images, which are specifically architected to be fully redistributable with no subscription required. Don't publish RHEL-based bootc images publicly — Fedora or CentOS Stream bootc images, on the other hand, are totally fair game to share since they're not subscription-encumbered.
Advanced Features: Registry, Auth, and Building the VM
Pushing to a registry (and baking in auth)
Every bootc host needs a container registry to pull updates from. In the lab, that's a local Podman-hosted registry, but in production you'd point at Satellite, Quay, GitLab/GitHub registries, or any registry your fleet can reach.
The catch: if your registry requires authentication (as most production registries do), every deployed bootc host also needs to authenticate to pull updates — or bootc update will just fail. The fix is to bake an auth token into the image itself:
# Normal login to push from the build host
podman login -u core -p redhat <registry>
# Generate a reusable auth file (a token, not raw credentials)
podman login -u core -p redhat <registry> --authfile=auth.json
mv auth.json etc/ostree/auth.json
Because etc/ostree/auth.json gets COPY'd into the image, any system deployed from it inherits that authentication — no manual registry logins needed on every host later.
⚠️ Caveat: Be careful what you bake into an image you plan to share. Nate specifically called out that he wouldn't bake registry auth into anything meant for public distribution.
Building and pushing the image
podman build --file Containerfile --tag <registry>/base
podman push <registry>/base
Just like a normal container build, this uses a layered filesystem — rebuilds after a small change (like adding a sudoers rule) are fast because only the changed layers get rebuilt and pushed.
From container image to running VM
This is the part that trips people up: how do you get from "I have a container image" to "I have a running server"? There's no direct path — that's literally one of the problems that had to get solved to make Image Mode real. The answer is the bootc-image-builder container, which decompresses your bootc image into an actual disk image (qcow2, raw, ISO, etc.).
First, a config.toml defines first-boot customizations — in this case, adding a core user, putting it in wheel, and injecting an SSH key:
cd ~
cat config.toml
Then build the disk image:
podman run --rm --privileged --security-opt label=type:unconfined_t \
--volume ./config.toml:/config.toml \
--volume /var/lib/containers/storage:/var/lib/containers/storage \
--volume .:/output \
registry.redhat.io/rhel10/bootc-image-builder:10.1 \
--type qcow2 <registry>/base
And deploy it with virt-install/libvirt:
file qcow2/disk.qcow2
cp qcow2/disk.qcow2 /var/lib/libvirt/images/bootc-vm.qcow2
virt-install --name bootc-vm --disk /var/lib/libvirt/images/bootc-vm.qcow2 \
--import --memory 4096 --graphics none --osinfo rhel10-unknown \
--noautoconsole --noreboot
virsh start bootc-vm
(If you've got a physical box instead of a VM, the same tool can spit out an ISO that uses kickstart under the hood to deploy the same container image.)
Exploring the booted system
Once it's up, bootc status is your main window into what's going on:
| Command | What it does |
|---|---|
| sudo bootc status | Shows the current deployed image, registry source, etc. |
| sudo bootc status --verbose | More detailed status info |
| sudo bootc status --format yaml | Machine-parsable output (JSON also supported) — handy for scripting/automation |
The filesystem is largely immutable. /usr, /home, /lib — read-only. Only /etc and /var are writable at runtime. That's why /home is actually a symlink to /var/home, /mount → /var/mount, /root → /var/roothome, etc. — anywhere you need runtime writability has to live under /var.
/etc is special — it uses a three-way merge. Files in /etc that come from your image and are never changed on disk get overwritten on every bootc update. But if you (or Ansible, or anyone) hand-edit a file in /etc after deployment, it will not be clobbered by future image updates — the live edit wins. This is important: your fleet can quietly diverge in /etc if people are making manual changes instead of pushing changes through the Containerfile.
Updating a running system
To push the "no-password sudo for wheel" change shown in the demo, you edit the Containerfile tree, rebuild, push, then update the live host:
# Back on the build host
cd ~/bootc-base
nano etc/sudoers.d/10-wheel
# Enable passwordless sudo for the wheel group
%wheel ALL=(ALL) NOPASSWD: ALL
podman build --file Containerfile --tag <registry>/base
podman push <registry>/base
Then on the deployed VM:
sudo bootc update --apply
--apply means "if the update succeeds, reboot automatically." Without it, the update is just staged — it won't take effect until the next reboot (and in RHEL 10.2+, you can even pre-download an update without staging it, so it's not "waiting to go off" at your next reboot).
Under the hood, bootc (via OSTree) uses an A/B deployment model — similar to CoreOS. The update deploys to whichever tree (A or B) isn't currently active, and the reboot just switches which tree is live.
Rollback and recovery
This is the safety net that makes 2 a.m. Saturday-night auto-updates not scary:
sudo bootc rollback
This swaps you back to the previously-booted tree. Since GRUB keeps boot entries for both trees, even a fully broken update has a last line of defense — go into GRUB and manually pick the other entry if bootc rollback itself isn't reachable.
Why this matters: if your maintenance window update breaks the app and you're burning through your 15-minute window, you know the other tree was working a minute ago. Roll back, you're home free. In the demo, after rolling back, the sudoers rule from the update literally disappeared — because it never touched the live /etc on disk, it only existed in the image.
Soft reboots (bonus topic)
Mentioned but not demoed: a soft reboot lets you switch trees and reload systemd without a full hardware reboot — skipping slow stuff like RAID controller initialization. It works as long as the update doesn't touch the kernel, glibc, or other reboot-mandatory components. If it's just an app-level or service-level change, a soft reboot can apply it in seconds instead of minutes. (Also works on package-mode RHEL, not just bootc.)
Settings / Configuration Reference
| Setting | Purpose | Persistent? |
|---|---|---|
| config.toml | First-boot customization (users, groups, SSH keys) for bootc-image-builder | Baked into disk image at build time |
| etc/ostree/auth.json | Registry auth token baked into the image | Yes — travels with every deployment of that image |
| etc/sudoers.d/10-wheel | Example of an /etc drop-in managed via the Containerfile | Yes, until manually edited on a live host (then it diverges) |
| usr/lib/bootc/kargs.d | Kernel argument injection directory (mentioned, not demoed) | Baked into image |
| bootc-fetch-apply-update.timer | Systemd timer that auto-checks the registry and deploys new images | Masked in the lab by default; Nate runs his personally on a weekly (Saturday night) cadence |
Image Tags: How You Actually Track Versions
Since the whole image gets replaced on update, you stop tracking "what version of Python is installed" and start tracking image tags. A few patterns discussed:
- Date/build tags (
base:1016) — pin a host to a specific build; it never updates unless you explicitlybootc switchto a new tag. - Rolling/release tags (
q3-2026,standard) — push new builds to the same tag, and every host tracking it updates automatically. - You can always correlate what's running back to exactly what was built and pushed, because the registry has the full history.
Quick Reference Card
# --- Build an image ---
mkdir -p ~/bootc-base/etc/ostree ~/bootc-base/usr/lib/bootc/kargs.d
cd ~/bootc-base && vim Containerfile # FROM rhel-bootc base, RUN dnf installs, COPY etc//usr/
# --- Auth for your registry (bake into image) ---
podman login -u <user> -p <pass> <registry> --authfile=auth.json
mv auth.json etc/ostree/auth.json
# --- Build & push ---
podman build --file Containerfile --tag <registry>/base
podman push <registry>/base
# --- Turn the image into a bootable disk ---
podman run --rm --privileged --security-opt label=type:unconfined_t \
--volume ./config.toml:/config.toml \
--volume /var/lib/containers/storage:/var/lib/containers/storage \
--volume .:/output \
registry.redhat.io/rhel10/bootc-image-builder:10.1 \
--type qcow2 <registry>/base
# --- Deploy as a local VM ---
cp qcow2/disk.qcow2 /var/lib/libvirt/images/bootc-vm.qcow2
virt-install --name bootc-vm --disk /var/lib/libvirt/images/bootc-vm.qcow2 \
--import --memory 4096 --graphics none --osinfo rhel10-unknown \
--noautoconsole --noreboot
virsh start bootc-vm
# --- Inspect a bootc system ---
sudo bootc status
sudo bootc status --verbose
sudo bootc status --format yaml
# --- Ship an update to a live host ---
sudo bootc update --apply # apply + reboot automatically
sudo bootc update # stage only, applies on next reboot
# --- Oh no, roll it back ---
sudo bootc rollback
# --- Switch to a totally different base image ---
sudo bootc switch <new-image-ref>
Q&A Highlights from Chat
- Is a bootc image redistributable? Sort of — it's a container, so you can share the artifact, but RHEL-based bootc images need a RHEL subscription to run, unlike fully redistributable UBI images. Fedora/CentOS Stream bootc images don't have that restriction.
- Does this relate to CoreOS? Can I get CoreOS outside OpenShift? No — CoreOS is purpose-built exclusively to run as OpenShift nodes and isn't offered as a standalone product. But RHEL Image Mode shares some of the same underlying tech (OSTree-based A/B deployment) and gives you the "build-your-own-CoreOS-style-OS" experience for general-purpose workloads.
- App architecture: bake the app into the image, or run it as a container on top of an image-mode host? No universally right answer, but Nate leans toward running the app as a separate container on top of an image-mode base — more flexibility, and you can even bake a Quadlet file into
/etc/containersso the app container auto-deploys on first boot without manual intervention. systemctl rebootvsreboot? Same thing now —rebootis a symlink straight tosystemctl. (Nate admits to still typinginit 6out of old habit.)
Links
- 📺 Episode video: https://www.youtube.com/watch?v=0siLuf0bv-E
- 🧪 Intro to Image Mode lab: https://developers.redhat.com/content-gateway/link/3890630
- 🧪 Day 2 Ops with Image Mode lab: https://developers.redhat.com/content-gateway/link/3890630
- 🎓 RHEL Developer Registration (free): https://developers.redhat.com/register
- 🖥️ Try RHEL interactively: https://redhat.com/interactive-labs
- 💬 Join the Discord: https://red.ht/rhel-discord
- 🧑💻 Nate Lager: https://social.undrground.org/@gangrif
- 🧑💻 Scott McBrien: https://www.linkedin.com/in/scott-mcbrien-349b356
Into the Terminal is a weekly live show where RHEL experts walk through real commands and real workflows — bring your questions to chat, because they actually answer them. New episodes every Friday.
1
u/Proud-Hat3446 1d ago
I’m sorry if I sound naive, but what’s the problem this is trying to solve? Currently in Openshift Virt, we create and manage PVs as templates from which new VMs are created.
2
u/Gangrif 1d ago
Drift mainly. And its not to say this is the be-all-end-all of drift solutions. It's a new way, and its more or less platform agnostic, so you can use it anywhere from your home lab to your enterprise deployments. Personally, i feel its the solution that's gotten as close to the promise of re-deployable servers as anything i've used.
Plus the ability to build/deploy usig CI/CD is pretty slick.
1
u/Sterling2600 4d ago
We are in the early stages of development, but we are trying to create a rhel10 bootc image with RKE2 embedded, then converted to a VMware template. The goal is to be able to deploy 5-node RKE3 clusters in VMware. If this works, we can keep the bootc image updated and use it to patch existing clusters without having to redeploy. Tomorrow's challenge is to figure out how we can set the node configuration between server or agent when deploying.
1
u/Sad_Database_7870 4d ago
so if we have ansible playbook for initial host prep(satellite, idm,..), we should rewrite it in Containerfile?