Skip to content

henrywang/composefs-os

Repository files navigation

composefs-os

Bootable Linux system images built on composefs-rs. Image-based OS management for personal and small-scale systems — no ostree, no fleet tooling.

What This Is

composefs-os publishes OCI container images that boot directly via a composefs overlay filesystem. Each image ships cbootc, a small embedded tool that handles upgrades, rollbacks, and image switching from within the running system.

It is not:

  • A general-purpose bootc replacement
  • An ostree-compatible tool
  • A fleet management system

See DESIGN.md for rationale and architecture.

Available Images

Image Boot style Status
ghcr.io/henrywang/composefs-os:fedora-44 GRUB (BLS Type 1) Working
ghcr.io/henrywang/composefs-os:fedora-44-uki systemd-boot + UKI (BLS Type 2) Working
ghcr.io/henrywang/composefs-os:fedora-44-uki-sb systemd-boot + UKI + Secure Boot Working
ghcr.io/henrywang/composefs-os:ubuntu-26.04 GRUB (BLS Type 1) Working
ghcr.io/henrywang/composefs-os:ubuntu-26.04-uki systemd-boot + UKI (BLS Type 2) Working
ghcr.io/henrywang/composefs-os:ubuntu-26.04-uki-sb systemd-boot + UKI + Secure Boot Working
ghcr.io/henrywang/composefs-os:arch-latest GRUB (BLS Type 1) Working
ghcr.io/henrywang/composefs-os:arch-latest-uki systemd-boot + UKI (BLS Type 2) Working
ghcr.io/henrywang/composefs-os:arch-latest-uki-sb systemd-boot + UKI + Secure Boot Working

Note: Arch images use a rolling release tag (arch-latest). GRUB + Secure Boot is not available for Arch — Arch does not ship a signed shim or signed GRUB EFI binary in official repositories (shim-signed is AUR-only). Use the -uki-sb variant instead.

Quick Start

# Install to a raw disk image (run from inside the container, needs --privileged)
sudo podman run --rm --privileged \
    -v $(pwd):/output \
    -v /var/lib/containers:/var/lib/containers \
    -v /var/tmp:/var/tmp \
    ghcr.io/henrywang/composefs-os:fedora-44 \
    cbootc install to-disk /output/disk.raw --size 10G

# Boot it
qemu-system-x86_64 -enable-kvm -m 4096 \
    -drive file=disk.raw,if=virtio \
    -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/ovmf/OVMF_CODE.fd \
    -nographic

Secure Boot

Pass --secure-boot to install the pre-signed Fedora shim + GRUB chain instead of running grub2-install. The resulting image passes UEFI Secure Boot enforcement without enrolling any custom keys.

# Install with Secure Boot EFI chain
sudo podman run --rm --privileged \
    -v $(pwd):/output \
    -v /var/lib/containers:/var/lib/containers \
    -v /var/tmp:/var/tmp \
    ghcr.io/henrywang/composefs-os:fedora-44 \
    cbootc install to-disk /output/disk-sb.raw --size 10G --secure-boot

# Boot with OVMF Secure Boot firmware (VARS must be a writable copy)
cp /usr/share/edk2/ovmf/OVMF_VARS.secboot.fd /tmp/OVMF_VARS.secboot.fd
qemu-system-x86_64 -enable-kvm -m 4096 \
    -machine q35,smm=on \
    -global driver=cfi.pflash01,property=secure,value=on \
    -drive file=disk-sb.raw,if=virtio \
    -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd \
    -drive if=pflash,format=raw,file=/tmp/OVMF_VARS.secboot.fd \
    -nographic

The same GRUB base image works for both modes — the EFI chain difference is handled entirely at install time.

UKI (Unified Kernel Image)

The -uki image uses systemd-boot and BLS Type 2 entries: a single .efi file bundles the kernel, initramfs, and composefs= cmdline. The cmdline (including the composefs hash) is embedded at install time by cbootc install to-disk, so there is no separate .conf file and no writable grubenv.

Pass --uki to cbootc install to-disk when using the UKI image:

sudo podman run --rm --privileged \
    -v $(pwd):/output \
    -v /var/lib/containers:/var/lib/containers \
    -v /var/tmp:/var/tmp \
    ghcr.io/henrywang/composefs-os:fedora-44-uki \
    cbootc install to-disk /output/disk-uki.raw --size 10G --uki

# A writable VARS file is needed for EFI variables (random seed, bootctl set-next)
cp /usr/share/edk2/ovmf/OVMF_VARS.fd /tmp/OVMF_VARS.fd
qemu-system-x86_64 -enable-kvm -m 4096 \
    -drive file=disk-uki.raw,if=virtio \
    -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/ovmf/OVMF_CODE.fd \
    -drive if=pflash,format=raw,file=/tmp/OVMF_VARS.fd \
    -nographic

cbootc rollback automatically detects the boot style — it uses bootctl set-next on UKI systems and grub2-editenv on GRUB systems.

UKI + Secure Boot

The -uki-sb image combines UKI with Secure Boot enforcement. At install time cbootc generates a self-signed key pair (or accepts --sb-key/--sb-cert), signs both systemd-boot and the UKI .efi, and installs signed systemd-boot directly as BOOTx64.EFI — no shim required. The firmware verifies the binaries against its Signature Database (db).

The signing cert must be enrolled in the UEFI db once before Secure Boot enforcement will allow booting. For QEMU testing, prep_sb_vars.py does this automatically using virt-fw-vars (dnf install python3-virt-firmware):

# Install
sudo podman run --rm --privileged \
    -v $(pwd):/output \
    -v /var/lib/containers:/var/lib/containers \
    -v /var/tmp:/var/tmp \
    ghcr.io/henrywang/composefs-os:fedora-44-uki-sb \
    cbootc install to-disk /output/disk-uki-sb.raw --size 10G --uki --secure-boot
# disk-uki-sb.raw.sb.cer is written alongside the disk image

# Enroll the cert into a copy of OVMF_VARS
python3 tests/prep_sb_vars.py disk-uki-sb.raw ovmf-vars-uki-sb.fd

# Boot with Secure Boot enforcement
qemu-system-x86_64 -enable-kvm -m 4096 \
    -machine q35,smm=on \
    -global driver=cfi.pflash01,property=secure,value=on \
    -drive file=disk-uki-sb.raw,if=virtio \
    -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd \
    -drive if=pflash,format=raw,file=ovmf-vars-uki-sb.fd \
    -nographic

On bare metal, enroll via the UEFI setup menu (Secure Boot → Key Management → Authorized Signatures → Add) selecting EFI/BOOT/composefs-os-sb.cer from the ESP. cbootc upgrade automatically re-signs new UKIs using the persisted key.

Building a Custom Image

The published base images are a starting point. Add your own packages and configuration in a derived Containerfile:

# Fedora
FROM ghcr.io/henrywang/composefs-os:fedora-44
RUN dnf install -y vim htop && dnf clean all

# Ubuntu
FROM ghcr.io/henrywang/composefs-os:ubuntu-26.04
RUN apt-get install -y vim htop && apt-get clean

# Arch Linux (rolling)
FROM ghcr.io/henrywang/composefs-os:arch-latest
RUN pacman -S --noconfirm --needed vim htop && pacman -Scc --noconfirm

# Use COPY (not RUN echo) for /etc/hostname: buildah bind-mounts a synthetic
# /etc/hostname into every RUN container, so writes via RUN are silently lost.
COPY <<EOF /etc/hostname
myhost
EOF

Use examples/fedora/Containerfile, examples/ubuntu/Containerfile, or examples/arch/Containerfile as full templates.

AWS (EC2) Images

examples/fedora/Containerfile.aws and examples/ubuntu/Containerfile.aws extend the GRUB base images with:

  • cloud-init (EC2 datasource only) — injects SSH keys and hostname from instance metadata on first boot
  • NVMe + Xen drivers rebuilt into the initramfs — required for EBS volumes on current and older EC2 instance types

Prerequisites

A vmimport IAM role must exist in your AWS account (one-time setup): https://docs.aws.amazon.com/vm-import/latest/userguide/required-permissions.html

Build and upload

# 1. Build the base image (skip if already built)
just build-base          # Fedora
just build-base-ubuntu   # Ubuntu

# 2. Build the AWS image layer (cloud-init + NVMe initramfs)
just build-aws-fedora
just build-aws-ubuntu

# 3. Write a raw disk image
just install-disk-aws-fedora   # → disk-aws-fedora.raw
just install-disk-aws-ubuntu   # → disk-aws-ubuntu.raw

# Or run all three steps at once (starting from scratch):
just ci-aws-fedora
just ci-aws-ubuntu

# 4. Upload to S3, import snapshot, and register as AMI
./examples/upload-ami.sh -d disk-aws-fedora.raw -b my-s3-bucket
./examples/upload-ami.sh -d disk-aws-ubuntu.raw -b my-s3-bucket -n my-ubuntu-ami -r us-east-1

upload-ami.sh options:

Flag Description
-d DISK Raw disk image path
-b BUCKET S3 bucket for staging
-n NAME AMI name (default: composefs-os-TIMESTAMP)
-r REGION AWS region (default: CLI config / AWS_DEFAULT_REGION)
-k Keep the S3 object after import

The registered AMI uses UEFI boot mode, ENA networking, and a gp3 root volume.

In-System Management

Once booted, cbootc manages the system:

# Show current deployment status
cbootc status

# Pull the latest image and stage a new boot entry
cbootc upgrade

# Reboot to apply
systemctl reboot

# Roll back to the previous deployment
cbootc rollback
systemctl reboot

# Switch to a different image
cbootc switch docker://ghcr.io/henrywang/composefs-os:fedora-44

The tracked image reference is stored in /var/lib/cbootc/config.toml and survives upgrades. cbootc-update.timer (enabled in the base image) runs cbootc upgrade daily with a randomised delay.

Repository Layout

composefs-os/
  Containerfile.fedora         Builds Fedora 44 base images (--target grub | uki | uki-secureboot)
  Containerfile.ubuntu         Builds Ubuntu 26.04 base images (--target grub | uki | uki-secureboot)
  Containerfile.arch           Builds Arch Linux base images (--target grub | uki | uki-secureboot)
  src/                         cbootc source (Rust)
  units/
    cbootc-update.service      Systemd service for automatic upgrades
    cbootc-update.timer        Systemd timer (daily, randomised delay)
  examples/
    fedora/
      Containerfile            Template for derived Fedora images (local/QEMU)
      Containerfile.aws        Fedora AWS image template (cloud-init + NVMe drivers)
    ubuntu/
      Containerfile            Template for derived Ubuntu 26.04 images (local/QEMU)
      Containerfile.aws        Ubuntu AWS image template (cloud-init + NVMe drivers)
    arch/
      Containerfile            Template for derived Arch Linux images
    upload-ami.sh              Upload a raw disk image to S3 and register as an AMI
  tests/
    e2e.py                     QEMU-based end-to-end test suite
  .github/workflows/
    ci.yml                     Rust build, test, lint
    container.yml              Build and push base images to ghcr.io
    e2e.yml                    End-to-end tests (boots in QEMU)

Known Limitations

/etc conflict resolution on upgrade

cbootc upgrade carries your /etc edits forward by copying the current deployment's overlayfs upper directory into the new deployment's upper directory. This gives the following behaviour:

  • File you edited, image didn't → your version persists ✓
  • File image changed, you didn't → new image version shows through ✓
  • Both you and the image changed the same file → your version wins

The last case is the same default as bootc. If you'd rather an image update win (e.g. after a security fix to a config file you've also customised), update your local copy manually after upgrading.

Tip: for configuration that must be reproducible, bake it into the Containerfile rather than editing the running system. Note that /etc/hostname requires COPY <<EOF instead of RUN echo — buildah bind-mounts a synthetic /etc/hostname into every RUN container so writes via RUN are silently lost.

Rollback

cbootc rollback selects the previous deployment for the next boot. Run systemctl reboot to apply it.

  • GRUB systems: writes next_entry to /boot/grub2/grubenv. If rollback fails to boot, use the GRUB menu to pick the older BLS entry manually from /boot/loader/entries/.
  • UKI/systemd-boot systems: calls bootctl set-next to set the LoaderEntryOneShot EFI variable. If rollback fails to boot, use the systemd-boot menu (hold Space at startup) to pick the older .efi entry.

Old deployment boot files accumulate across upgrades and are not pruned automatically. Remove them manually when disk space is a concern.

x86-64 only

All boot paths (GRUB, UKI, UKI + Secure Boot) are hard-coded to x86_64-efi. aarch64 and other architectures are not supported.

License

MIT — see LICENSE.

About

Bootable Linux system images built on composefs-rs

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors