A learning guide for understanding, building, and deploying Bootable Containers in the context of Red Hat OpenShift and OpenShift Virtualization.
Note: This project uses CentOS Stream 9 bootc images for simplicity (no subscription required). The concepts apply equally to RHEL bootc images.
CI/CD: For building, converting, and deploying these images automatically via GitHub Actions, OpenShift Pipelines, or GitOps, see the CI/CD README.
- Concepts
- Key Components
- Architecture
- Prerequisites
- Demo 1: Build a Web Server
- Demo 2: Regular Container vs Bootable Container
- Demo 3: Deploy to OpenShift Virtualization
- Demo 4: Upgrade and Rollback
- CI/CD Integration
- Updates and Rollbacks
- Best Practices
- References
Bootable Containers (also known as Image Mode) represent a paradigm shift in how operating systems are built, deployed, and managed. Instead of treating the OS as a collection of packages installed and configured imperatively, bootable containers treat the entire operating system as a container image.
┌─────────────────────────────────────────────────────────────────┐
│ Traditional (Package Mode) │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Install OS → Install Packages → Configure → Deploy ││
│ │ (Mutable, Imperative, Drift-prone) ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Image Mode (bootc) │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Containerfile → Build Image → Push → Deploy/Boot ││
│ │ (Immutable, Declarative, Reproducible) ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
| Aspect | Package Mode | Image Mode |
|---|---|---|
| OS Content Model | Individual RPM packages | Versioned OCI OS image |
| Updates | Package-by-package | Atomic image replacement |
| Rollback | Complex, often impossible | Simple, automatic |
| Root Filesystem | Mutable | Immutable (except /etc, /var) |
| Configuration | Runtime, imperative | Build time, declarative |
| Consistency | Drift over time | Guaranteed identical |
| Testing | Test each package combo | Test entire image |
- Unified Workflow: Same tools (Podman, Containerfile, registries) for apps AND operating systems
- Immutability: Root filesystem is read-only, preventing configuration drift
- Atomic Updates: Update the entire OS as one unit, not package by package
- Easy Rollbacks: Boot into previous image version if issues arise
- GitOps Ready: OS configuration can be versioned and managed via Git
- Container-Native: Seamless integration with OpenShift and Kubernetes workflows
The core tool that manages bootable container images on a system:
# Check current booted image
bootc status
# Switch to a new image
bootc switch quay.io/myorg/my-bootc:latest
# Update to latest version of current image
bootc upgrade
# Rollback to previous image
bootc rollbackA containerized tool that converts bootc container images into various disk formats:
| Output Type | Use Case |
|---|---|
qcow2 |
KVM, OpenShift Virtualization, libvirt |
vmdk |
VMware vSphere |
raw |
Bare metal, direct disk write |
iso |
Installation media |
ami |
AWS EC2 |
This project uses CentOS Stream 9 bootc:
# CentOS Stream 9 (used in this project)
quay.io/centos-bootc/centos-bootc:stream9
# Fedora (alternative)
quay.io/fedora/fedora-bootc:41
# RHEL (requires subscription)
registry.redhat.io/rhel9/rhel-bootc:9.6┌─────────────────────────────────────────────────────────────────────┐
│ Build Pipeline │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Containerfile │───▶│ podman build │───▶│ Container Registry │ │
│ │ (OS Config) │ │ │ │ (quay.io, etc.) │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────┐
│ Direct Boot (bootc) │ │ bootc-image-builder │ │ OpenShift Deploy │
│ │ │ │ │ │
│ • Bare metal │ │ • QCOW2 → KVM/OCP-V │ │ • DataVolume │
│ • Existing system │ │ • VMDK → VMware │ │ • VirtualMachine │
│ • bootc switch/upgrade │ │ • ISO → Installation │ │ • Auto-import │
└─────────────────────────┘ └─────────────────────────┘ └─────────────────────┘
All commands in this guide use the ${USERNAME} environment variable. Set it once:
export USERNAME=<your-quay-username># Required: Podman 4.0+
podman --version
# Login to quay.io (or your registry)
podman login quay.iomacOS Users: Building bootc container images works on macOS. However, converting to QCOW2 with
bootc-image-builderrequires a Linux x86_64 system (local VM or remote server). See Demo 3 for details.
- OpenShift 4.14+ with OpenShift Virtualization operator
- Access to create DataVolumes and VirtualMachines
Build a simple Apache httpd web server as a bootable container.
cd demos/webserver
# Remove existing image/manifest if present
podman manifest rm quay.io/${USERNAME}/bootc-webserver:v1.0 2>/dev/null || true
# Create manifest and build for both architectures
podman manifest create quay.io/${USERNAME}/bootc-webserver:v1.0
podman build --platform linux/amd64,linux/arm64 \
--manifest quay.io/${USERNAME}/bootc-webserver:v1.0 .
# Push the manifest
podman manifest push --all quay.io/${USERNAME}/bootc-webserver:v1.0Bootable containers use systemd and are designed to boot as VMs. For a quick local test, run httpd directly:
# Run httpd directly (bypasses systemd)
podman run -d --name webserver-test -p 8080:80 \
quay.io/${USERNAME}/bootc-webserver:v1.0 \
/usr/sbin/httpd -DFOREGROUND
# Test
curl http://localhost:8080
# Cleanup
podman rm -f webserver-testNote: This only tests the web content. For a full test with systemd services, deploy as a VM (see Demo 3).
Now that you've built a bootable container, compare it with a regular container to see the differences.
| Aspect | Regular Container | Bootable Container |
|---|---|---|
| Purpose | Run a single application | Run a full operating system |
| Init (PID 1) | Application process | systemd |
| Kernel | Uses host kernel | Contains its own kernel |
| Size | ~100 MB | ~1.5 GB |
| Can boot as VM | No | Yes |
| Atomic updates | No | Yes (via bootc) |
cd demos/comparison
./compare.shThis script compares:
- Image sizes
- Kernel presence (
/boot/vmlinuz*) - Init system (
/sbin/init) - Kernel modules (
/usr/lib/modules/) - systemd services
- bootc tool availability
See demos/comparison/README.md for individual commands to run step-by-step.
Convert the bootable container to a QCOW2 disk image and deploy it as a VM.
Build the bootc container image and push to your registry. This works on any platform:
cd demos/webserver
# Remove existing manifest if present
podman manifest rm quay.io/${USERNAME}/bootc-webserver:v1.0 2>/dev/null || true
# Create manifest and build for both architectures
podman manifest create quay.io/${USERNAME}/bootc-webserver:v1.0
podman build --platform linux/amd64,linux/arm64 \
--manifest quay.io/${USERNAME}/bootc-webserver:v1.0 .
# Push the manifest (includes both architectures)
podman manifest push --all quay.io/${USERNAME}/bootc-webserver:v1.0Note: Multi-arch builds create a manifest list that works on both x86_64 and ARM64 systems. On Apple Silicon, amd64 builds use emulation (slower but works).
Important:
bootc-image-builderrequires access to host container storage via-v /var/lib/containers/storage:/var/lib/containers/storage. This only works on native Linux - it cannot run reliably on macOS due to Podman Machine virtualization limitations.
Required packages on the Linux system:
# Fedora/CentOS/RHEL
sudo dnf install -y podman osbuild-selinux
# Ubuntu/Debian
sudo apt install -y podman| Package | Purpose |
|---|---|
podman |
Container runtime (4.0+ required) |
osbuild-selinux |
SELinux policies for osbuild (required if SELinux is enforced) |
Run this on a Linux x86_64 system (local VM or remote server):
# Login to registry first
podman login quay.io
export USERNAME=<your-quay-username>
# Pull the bootc image first (bootc-image-builder no longer pulls automatically)
sudo podman pull quay.io/${USERNAME}/bootc-webserver:v1.0
mkdir -p output
sudo podman run \
--rm -it --privileged \
--pull=newer \
--security-opt label=type:unconfined_t \
-v "$PWD/output":/output \
-v /var/lib/containers/storage:/var/lib/containers/storage \
quay.io/centos-bootc/bootc-image-builder:latest \
--type qcow2 \
quay.io/${USERNAME}/bootc-webserver:v1.0
# Result: output/qcow2/disk.qcow2Example terminal output:
[rguske@centos-image-builder ~]$ sudo podman run \
--rm -it --privileged \
--pull=newer \
--security-opt label=type:unconfined_t \
-v "$PWD/output":/output \
-v /var/lib/containers/storage:/var/lib/containers/storage \
quay.io/centos-bootc/bootc-image-builder:latest \
--type qcow2 \
quay.io/${USERNAME}/bootc-webserver:v1.0
[-] Disk image building step
[5 / 5] Pipeline qcow2 [--------------------------------------------------------------------------------------------------------->] 100.00%
[2 / 2] Stage org.osbuild.qemu [------------------------------------------------------------------------------------------------->] 100.00%
Message: Results saved in .
[rguske@centos-image-builder ~]$
Options for Linux x86_64 environment:
| Option | Description |
|---|---|
| Local Linux VM | Run Fedora/CentOS in UTM, VMware Fusion, or Parallels on your Mac |
| Remote Linux server | SSH to any Linux x86_64 server and run the command there |
| GitHub Actions | Use the workflow in cicd/github-actions/ for automated builds |
See bootc-image-builder README for more details.
After converting to QCOW2 on Linux, you have two options:
Option A: Package on the Linux host (recommended)
Create the Containerfile.ocpv on your Linux host:
cat << 'EOF' > Containerfile.ocpv
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest AS builder
ADD --chown=107:107 output/qcow2/disk.qcow2 /disk/
RUN chmod 0440 /disk/*
FROM scratch
COPY --from=builder /disk/* /disk/
LABEL name="bootc-webserver-disk" version="1.0"
EOF
# Build and push
podman build -f Containerfile.ocpv -t quay.io/${USERNAME}/bootc-webserver:v1.0-disk .
podman push quay.io/${USERNAME}/bootc-webserver:v1.0-diskOption B: Copy QCOW2 back to macOS
# On Linux: copy the QCOW2 to your Mac via scp
scp output/qcow2/disk.qcow2 user@your-mac:/path/to/demos/webserver/output/qcow2/
# On macOS: build and push
cd demos/webserver
podman build -f Containerfile.ocpv -t quay.io/${USERNAME}/bootc-webserver:v1.0-disk .
podman push quay.io/${USERNAME}/bootc-webserver:v1.0-diskImportant: Make your Quay.io repository public, or create an image pull secret:
# Option 1: Make repository public in Quay.io UI Go to: quay.io → Repository Settings → Make Public # Option 2: Create pull secret for private registry oc create secret docker-registry quay-pull-secret \ --docker-server=quay.io \ --docker-username=${USERNAME} \ --docker-password=<your-password-or-token> \ -n bootable-containers-demoIf using a pull secret, uncomment
secretRef: quay-pull-secretindatavolume.yaml.
# Go back to project root
cd ../..
# Check available storage classes and set the variable
oc get sc
export STORAGE_CLASS=<your-storage-class>
# Update kustomization.yaml with your values (it ships with rguske /
# kubevirt-odf-replica-two-block as placeholders, see the patches: comment)
sed -i "" "s|rguske|${USERNAME}|g" openshift-virtualization/kustomization.yaml
sed -i "" "s|kubevirt-odf-replica-two-block|${STORAGE_CLASS}|g" openshift-virtualization/kustomization.yaml
# Deploy using kustomize
oc apply -k openshift-virtualization/
# Watch progress
oc get dv -n bootable-containers-demo -w
# Get route URL
oc get route bootc-webserver -n bootable-containers-demo -o jsonpath='{.spec.host}'Once deployed, access the web server via the route URL:
The VM has two users configured:
| User | Password | Configured via |
|---|---|---|
bootc-user |
redhat |
Containerfile (always available) |
rhel |
R3dH4t1! |
cloud-init (requires cloud-init in image) |
Note: The
rheluser is created by cloud-init at first boot. If cloud-init isn't installed in your bootc image, usebootc-userinstead.
Note: The default VM uses 1 CPU core and 2Gi memory. Adjust
virtualmachine.yamlif your cluster has different capacity or if you need more resources.
Demonstrate bootc's atomic upgrade and rollback capabilities by switching between image versions.
- VM running v1.0 from Demo 3
- Both v1.0 and v1.1 images pushed to your registry
v1.0 (red theme, default):
cd demos/webserver
# Remove existing manifest if present
podman manifest rm quay.io/${USERNAME}/bootc-webserver:v1.0 2>/dev/null || true
# Create manifest and build multi-arch
podman manifest create quay.io/${USERNAME}/bootc-webserver:v1.0
podman build --platform linux/amd64,linux/arm64 \
--manifest quay.io/${USERNAME}/bootc-webserver:v1.0 .
podman manifest push --all quay.io/${USERNAME}/bootc-webserver:v1.0v1.1 (purple theme):
# Remove existing manifest if present
podman manifest rm quay.io/${USERNAME}/bootc-webserver:v1.1 2>/dev/null || true
# Create manifest and build multi-arch with theme customization
podman manifest create quay.io/${USERNAME}/bootc-webserver:v1.1
podman build --platform linux/amd64,linux/arm64 \
--build-arg THEME_COLOR="#7b2cbf" \
--build-arg THEME_COLOR_DARK="#5a189a" \
--build-arg THEME_COLOR_LIGHT="#9d4edd" \
--build-arg VERSION="1.1" \
--build-arg SUBTITLE="Version 1.1 - Purple Edition" \
--manifest quay.io/${USERNAME}/bootc-webserver:v1.1 .
podman manifest push --all quay.io/${USERNAME}/bootc-webserver:v1.1Open the web server URL and confirm you see the red theme with v1.0 badge.
Connect to the VM:
virtctl console bootc-webserver -n bootable-containers-demoCheck current bootc status:
sudo bootc status[bootc-user@bootc-webserver ~]$ sudo bootc status
[ 1213.908583] SELinux: Context unconfined_u:object_r:invalid_bootcinstall_testlabel_t:s0 is not valid (left unmapped).
● Booted image: quay.io/rguske/bootc-webserver:v1.0
Digest: sha256:5d100215e05a702733f7ddbcdca54f0d69e3a4437d3c5396dab368b5ca0c8c51 (amd64)
Version: 9 (2026-07-31T22:34:55Z)
export USERNAME=<your-quay-username>sudo bootc switch quay.io/${USERNAME}/bootc-webserver:v1.1[bootc-user@bootc-webserver ~]$ sudo bootc switch quay.io/${USERNAME}/bootc-webserver:v1.1
[ 1381.133563] evm: overlay not supported
layers already present: 66; layers needed: 7 (1.1 GB)
Fetched layers: 1.07 GiB in 4 minutes (4.95 MiB/s)
Deploying: done (15 seconds) Pruned images: 0 (layers: 0, objsize: 9.4 MB)
Queued for next boot: quay.io/rguske/bootc-webserver:v1.1
Version: 9
Digest: sha256:728e8e1d196a4707ab8017948e27e0d73534cee037c4038fd9c05da8a2375a46
Reboot to apply:
sudo systemctl rebootAfter reboot, refresh the browser. You should see:
- Purple theme
- v1.1 badge
- Subtitle: "Version 1.1 - Purple Edition"
Connect back into the VM:
virtctl console bootc-user -n bootable-containers-demoRollback:
sudo bootc rollback
sudo systemctl rebootAfter reboot, refresh the browser. You should see the red theme and v1.0 badge restored.
- Atomic updates: The entire OS updates as one unit
- Instant rollback: Previous version is preserved and ready to boot
- No package drift: Every boot is from a known, tested image
Complete CI/CD pipelines are available in cicd/. See cicd/README.md for details.
| Option | Best For |
|---|---|
| GitHub Actions | GitHub-hosted projects |
| OpenShift Pipelines | OpenShift-native CI/CD |
| GitOps (ArgoCD) | Declarative deployments |
Git Push → Build bootc → Convert to QCOW2 → Package → Deploy VM
On a running bootc system:
bootc status # Check current image
bootc upgrade # Pull and stage new image
systemctl reboot # Apply updatebootc rollback # Switch to previous image
systemctl reboot # Apply rollbackRUN dnf install -y httpd vim-minimal && \
dnf clean all && \
rm -rf /var/cache/dnfLABEL version="1.2.0"
LABEL description="Web server with Apache httpd"podman run --rm -it quay.io/${USERNAME}/bootc-webserver:v1.0 /bin/bash
systemctl list-unit-files --state=enabledCOPY httpd.conf /etc/httpd/conf/httpd.conf
COPY app-config.yaml /etc/myapp/config.yaml
