This document is fully generated by Opus 4.6 with OpenCode.
A new-user comparison of getting a "hello world" Apache httpd system running in QEMU using bootc and Kairos, written March 2026.
Kairos v4.0.1 / Hadron v0.0.4; bootc from current main branch.
Start from nothing. End with an immutable Linux VM booting in QEMU, serving a static "Hello World" page over HTTP on port 80. Compare how easy each project makes this.
bootc takes a standard OCI container image and makes it bootable. You
write a Containerfile the same way you would for any application
container, but the base image includes a Linux kernel and systemd.
Services start via systemctl enable, not CMD.
1. Write a Containerfile
FROM quay.io/fedora/fedora-bootc:41
RUN dnf -y install httpd && dnf clean all
RUN systemctl enable httpd
## Note: /var/www/html/ content won't update on `bootc upgrade` — fine for
## a hello-world demo but in production put content under /usr/share/httpd/
RUN echo '<h1>Hello World from bootc!</h1>' > /var/www/html/index.html
# Let us log in during testing
RUN echo 'root:password' | chpasswd
LABEL containers.bootc 1That's it. The dnf install / systemctl enable pattern is the same
one sysadmins have used for decades; the Containerfile is just the
delivery mechanism.
2. Build
podman build -t localhost/my-httpd-bootc .3. Create a bootable disk image
truncate -s 10G disk.raw
sudo podman run --rm --privileged --pid=host \
--security-opt label=type:unconfined_t \
-v /dev:/dev \
-v /var/lib/containers:/var/lib/containers \
-v .:/output \
localhost/my-httpd-bootc \
bootc install to-disk --generic-image --via-loopback /output/disk.rawAlternatively, use bootc-image-builder to produce a qcow2 directly:
sudo podman run --rm --privileged \
-v /var/lib/containers/storage:/var/lib/containers/storage \
-v ./output:/output \
quay.io/centos-bootc/bootc-image-builder:latest \
--type qcow2 \
localhost/my-httpd-bootc4. Boot in QEMU
qemu-system-x86_64 -m 2048 -enable-kvm -cpu host \
-drive file=disk.raw,format=raw,if=virtio \
-nic user,hostfwd=tcp::8080-:80 \
-nographic -serial mon:stdio5. Test
curl http://localhost:8080
# <h1>Hello World from bootc!</h1>You need to understand three things: Containerfiles, bootc install to-disk (or bootc-image-builder), and basic QEMU flags. The only
"new" concept beyond normal container building is that
CMD, ENTRYPOINT, and EXPOSE are ignored at boot, and
environment variables from ENV are not propagated to systemd
services — everything runs via systemd.
Kairos is a CNCF sandbox project that turns Linux distros into immutable, container-based operating systems optimized for Kubernetes at the edge. It supports multiple "flavors" (Alpine, Fedora, Ubuntu, and the new default "Hadron"), with built-in k3s/k0s, P2P mesh networking, cloud-config-driven provisioning, and fleet management.
Getting Apache hello-world running is more involved because the project optimizes for a different workflow.
The default Kairos quickstart uses Hadron, a minimal base with no
apt, dnf, or zypper. To install Apache you must either:
- Download a statically-linked httpd binary and
COPYit in, or - Use a non-Hadron flavor (Ubuntu, openSUSE) via BYOI, which adds its own complexity since official prebuilt artifacts are no longer published for these flavors.
This is the closest equivalent to the bootc workflow.
1. Write a Dockerfile
FROM quay.io/kairos/kairos-init:v0.6.8 AS kairos-init
# Use an openSUSE base that has a package manager
FROM opensuse/leap:15.6 AS base
# "Kairosify" the system
RUN --mount=type=bind,from=kairos-init,src=/kairos-init,dst=/kairos-init \
eval /kairos-init -l debug -s install --model generic --version "0.1.0" && \
eval /kairos-init -l debug -s init --model generic --version "0.1.0"
# Install Apache
RUN zypper -n install apache2 && zypper clean -a
RUN systemctl enable apache2
RUN echo '<h1>Hello World from Kairos!</h1>' > /srv/www/htdocs/index.html2. Build and push
docker build -t my-kairos-httpd:0.1.0 .
docker tag my-kairos-httpd:0.1.0 ttl.sh/my-kairos-httpd:0.1.0
docker push ttl.sh/my-kairos-httpd:0.1.03. Generate an ISO with AuroraBoot
docker run --rm \
-v $PWD/build:/tmp/auroraboot \
-v /var/run/docker.sock:/var/run/docker.sock \
quay.io/kairos/auroraboot:latest \
--set container_image=docker://ttl.sh/my-kairos-httpd:0.1.0 \
--set "disable_http_server=true" \
--set "disable_netboot=true" \
--set "state_dir=/tmp/auroraboot"4. Write a cloud-config
#cloud-config
install:
device: "auto"
reboot: false
poweroff: true
auto: true
users:
- name: kairos
passwd: kairos
groups: [admin]
stages:
boot:
- name: "Repart image"
layout:
device:
label: COS_PERSISTENT
expand_partition:
size: 0
commands:
- |
[[ "$(echo "$(df -h | grep COS_PERSISTENT)" | awk '{print $5}' \
| tr -d '%')" -ne 100 ]] \
&& resize2fs /dev/disk/by-label/COS_PERSISTENT5. Create a cloud-init ISO sidecar
cd build
touch meta-data
cp ../cloud-config.yaml user-data
mkisofs -output ci.iso -volid cidata -joliet -rock user-data meta-data6. Create a raw disk and run the installer in QEMU (phase 1: install)
truncate -s 20G disk.raw
qemu-system-x86_64 -m 8192 -smp cores=2 \
-nographic -cpu host -enable-kvm \
-serial mon:stdio \
-drive if=virtio,media=disk,file=disk.raw \
-drive format=raw,media=cdrom,readonly=on,file=kairos.iso \
-drive format=raw,media=cdrom,readonly=on,file=ci.iso \
-boot dWait for installation to complete and the VM to power off.
7. Boot from disk (phase 2: run)
qemu-system-x86_64 -m 4096 -smp cores=2 \
-nographic -cpu host -enable-kvm \
-serial mon:stdio \
-drive if=virtio,media=disk,file=disk.raw \
-nic user,hostfwd=tcp::8080-:808. Test
curl http://localhost:8080If you stick with Hadron, you need to download a static binary:
FROM quay.io/kairos/kairos-init:v0.6.8 AS kairos-init
FROM ghcr.io/kairos-io/hadron-toolchain:v0.0.1-beta2 AS httpd-build
# Download a static httpd (e.g., busybox httpd or similar)
RUN curl -L -o /busybox \
https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox
RUN chmod +x /busybox
FROM ghcr.io/kairos-io/hadron:v0.0.1-beta2 AS base
ARG VERSION=0.1.0
RUN --mount=type=bind,from=kairos-init,src=/kairos-init,dst=/kairos-init \
eval /kairos-init -l debug -s install --model generic \
--provider k3s --version "${VERSION}" && \
eval /kairos-init -l debug -s init --model generic \
--provider k3s --version "${VERSION}"
COPY --from=httpd-build /busybox /usr/bin/busyboxThen you still need a systemd unit to run busybox httpd -f -p 80 -h /srv/www, the hello-world HTML dropped via cloud-config, and steps
3-8 from Path A. There is no shortcut for the two-phase install.
You need to understand: Dockerfiles, kairos-init (kairosification),
AuroraBoot, cloud-config (yip, not standard cloud-init), mkisofs
for the sidecar ISO, two-phase QEMU boot (install then run), Kairos
partitioning (OEM, recovery, state, persistent), and the difference
between flavors. That is roughly 3x the conceptual surface area.
| Dimension | bootc | Kairos |
|---|---|---|
| Goal | Make OCI images bootable | Immutable edge OS platform with Kubernetes |
| Base image | Standard distro (Fedora, CentOS) with kernel + systemd | Hadron (no pkg mgr) or BYOI flavor |
| Package install | dnf install httpd |
Depends on flavor; Hadron needs static binaries |
| Service management | systemctl enable in Containerfile |
Same, plus cloud-config stages |
| Disk image creation | bootc install to-disk --via-loopback (one step) |
AuroraBoot ISO + two-phase QEMU install |
| QEMU workflow | Single boot from raw disk | Boot ISO to install, then reboot from disk |
| Time to first curl | ~15 minutes | ~45-60 minutes |
| Configuration | Containerfile only | Containerfile + cloud-config YAML |
| New concepts | 1-2 beyond normal containers | 5-8 beyond normal containers |
| Kubernetes built-in | No (bring your own) | Yes (k3s/k0s included) |
| Fleet management | No | Yes (P2P, QR code, operator) |
| Atomic upgrades | bootc upgrade |
kairos-agent upgrade |
| Rollback | ostree-based A/B; bootc rollback |
Active/passive/recovery partitions |
| Immutable rootfs | ostree: /usr immutable, /etc 3-way merge, /var writable |
Partition-based: squashfs rootfs, /oem + /usr/local writable |
| Official QEMU docs | Not documented | Reference page with build script |
| Hello-world example | None in repo | None focused on httpd |
What went well:
- The mental model is dead simple: "it's a container, but it boots." If you know Containerfiles, you know 90% of what you need.
dnf install httpd && systemctl enable httpdis exactly what a sysadmin expects. No new abstractions.- One
podman runcommand produces a bootable disk. No intermediate ISOs, no two-phase install. - The docs are accurate, if reference-heavy rather than tutorial-oriented.
What tripped me up:
- The README points to ADOPTERS.md rather than a getting-started guide.
The actual user docs live at
docs.fedoraproject.org/en-US/bootc/, a separate site not linked prominently. - Zero example Containerfiles in the repo. You synthesize the workflow
from several doc pages (
bootc-install.md,building/guidance.md,building/bootc-runtime.md,building/users-and-groups.md). - No QEMU documentation. After creating
disk.raw, you're on your own for theqemu-system-x86_64invocation. - Forgetting to set a root password or SSH key means you boot into a system you can't log into. The docs warn about this but it's easy to miss.
- The
--via-loopbackflow requires root (sudo podman run --privileged). /varsemantics are subtle: content placed in/var/www/html/during the build is present on first install but won't update on subsequentbootc upgradeoperations. For content that should track the image, use/usr/share/httpd/with a symlink.- SELinux labels:
chcondoesn't work in container builds. Usesemanage fcontextinstead.
What went well:
- Polished landing page and clear quickstart guide at kairos.io.
- The "Build Raw images with QEMU" reference page gives you a complete shell script.
- Cloud-config is powerful — set up users, networking, k3s, partitions, and run arbitrary commands at different boot stages from a single YAML file.
- Built-in Kubernetes is genuinely convenient if that's your goal.
After install,
kubectl get nodesjust works. - Multiple install paths (QR code, WebUI, netboot, P2P) are impressive for fleet scenarios.
- Config validation:
kairos validate ./cloud-config.yamlcatches errors before deployment.
What tripped me up:
- The quickstart uses VirtualBox, not QEMU. QEMU instructions are in a separate reference page and assume familiarity with the Kairos installation model.
- Hadron (the default flavor) has no package manager. Installing Apache — a completely standard Linux service — requires either a static binary or switching to a BYOI flavor. This is a jarring surprise.
- BYOI flavors (Ubuntu, openSUSE) no longer ship prebuilt artifacts.
You build everything yourself with
kairos-initand AuroraBoot. Some docs still reference old artifact names. - The "kairosify" step (
kairos-init) is opaque. It installs the Kairos agent, immucore, dracut modules, and GRUB config into your image. If it fails, debugging is difficult. - Cloud-config uses
yip, notcloud-init. The syntax is similar but not identical. If you've used cloud-init before, the differences will bite you (e.g.,stages:blocks, noruncmdat top level in the standard way). - Two-phase QEMU installation (boot ISO to install, then boot from disk) is inherently more complex than bootc's single-step approach.
- The cloud-init sidecar ISO (
mkisofs -volid cidata) is an extra step that's easy to get wrong. - AuroraBoot flag naming (
artifact_version,release_version,flavor,flavor_release,repository) is confusing — it's hard to know which combination is needed. - Documentation defaults to the "Next" (development) version with a warning banner. Getting to stable docs requires clicking through.
- Always set a root password or SSH key in your Containerfile for testing. Remove it before production.
- Use
--generic-imagewithbootc install to-disk. Without it, the image may be tied to the build host's identity. (Note:--generic-imageis auto-enabled when using--via-loopback, but it's good practice to be explicit.) - Content in
/var/won't update on upgrades. Put web content under/usr/share/httpd/and symlink, or use a bind mount. - SELinux labels matter. Use
semanage fcontextin the Containerfile (notchcon, which doesn't persist in container builds). - The filesystem type must be known. If your base image doesn't
set a default, pass
--filesystem xfs(or ext4/btrfs) tobootc install to-disk. - For qcow2 output, use
bootc-image-builder --type qcow2rather than raw disk + manual conversion. - Port forwarding in QEMU: use
-nic user,hostfwd=tcp::8080-:80to expose guest port 80 on host port 8080.
- Start with a BYOI flavor if you need standard packages. Hadron is optimized for Kubernetes appliances, not general-purpose servers.
- Test cloud-config in a container first:
docker run -ti -v $PWD:/test \ --entrypoint /usr/bin/kairos-agent --rm \ quay.io/kairos/hadron:v0.0.4-core-amd64-generic-v4.0.1 \ run-stage --cloud-init-paths /test boot - Set
install.poweroff: trueandinstall.auto: truein cloud-config for unattended QEMU installs. Without these, the installer waits for interactive input. - Cloud-config goes in a sidecar ISO, not baked into the main ISO
(unless you rebuild with AuroraBoot). Use
mkisofs -volid cidata. - Eject the ISO after installation. In QEMU, remove the
-driveflags for the ISO and ci.iso on the second boot. - Kairos uses
yip, not cloud-init. Don't expect full cloud-init compatibility. Check the configuration reference. - If you don't need Kubernetes, omit
--providerfromkairos-initto get a "core" image without k3s/k0s. - The config file lives at
/oem/90_custom.yamlafter installation. SSH in and edit for testing (changes apply on reboot). - Port forwarding: not documented in Kairos' QEMU guide. Use
-nic user,hostfwd=tcp::8080-:80the same as any QEMU setup.
bootc is part of the Fedora/Red Hat ecosystem, with base images from CentOS Stream and Fedora. It is developed in Rust, uses ostree under the hood, and is on track for broader adoption in the RHEL/CentOS family.
Kairos is a CNCF Sandbox project, backed commercially by Spectro Cloud (who also develops the Hadron base and the Palette edge management platform). It has an active community and a regular release cadence.
Choose bootc if:
- You want the simplest path from "container image" to "bootable VM."
- Your team already knows Containerfiles and Linux administration.
- You don't need built-in Kubernetes or fleet management.
- You want standard distro package managers (
dnf,apt). - Your use case is servers, VMs, or cloud instances.
Choose Kairos if:
- You need integrated Kubernetes (k3s/k0s) out of the box.
- You're deploying fleets of edge devices and need P2P provisioning, QR code installs, or a management operator.
- You want active/passive/recovery boot modes with encrypted partitions.
- You need a built-in declarative config system (cloud-config/yip).
- You're willing to invest in learning a more complex system for more powerful fleet-scale features.
For the specific task of "get Apache serving hello-world in QEMU," bootc is substantially simpler. The workflow is: write a Containerfile, build, create a disk image, boot. Four steps, one new concept beyond standard containers.
Kairos requires at least twice as many steps and introduces several new abstractions (kairosification, cloud-config stages, AuroraBoot, two-phase install). Its strength is in the platform it provides: integrated Kubernetes, fleet management, and multiple provisioning methods. But those capabilities come with complexity that's hard to justify for a simple "run Apache in a VM" task.
Both projects share the fundamental insight that container images are a great delivery mechanism for operating systems. They differ in where they draw the line between "just make it boot" (bootc) and "make it a complete edge platform" (Kairos).
Assisted-by: OpenCode (Claude Opus 4)