contemper

module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: Apache-2.0

README

contemper

CI Go Reference Latest release License Go version Coverage Docs OpenSSF Scorecard

Build bootable VM images from container images.

contemper takes an OCI image you already built, merges in what a given virtualization target needs, and writes out a bootable disk. Authoring a VM image becomes an ordinary container build.

$ <tool> build -t registry.example.com/my-appliance:dev .
$ <tool> push registry.example.com/my-appliance:dev
$ contemper convert --target qemu registry.example.com/my-appliance:dev
$ contemper deploy --to local-qemu my-appliance-dev.aarch64/

<tool> is podman or docker; the two commands above are identical either way. contemper build runs the build (with Docker buildx or podman) and the convert step together and skips the registry; see Getting started for that walkthrough.

Why

Containers arguably became what they are because the Dockerfile made image authoring easy and understandable. VM images are still mostly built with heavier, more bespoke tooling: a separate toolchain with its own base image conventions, build tools and registry habits, even though the content (a filesystem plus a little metadata about how to boot it) is the same kind of thing.

contemper bets that container authoring works just as well for VM images. You write a build file with the tools you already use, push and scan it like any other image, and contemper turns it into a disk. Building a custom VM image should feel no different from building a custom container image.

How it works

  • You author, contemper packages. Your image provides a kernel, an initrd and an init system, and is marked with LABEL io.contemper.ready="true". contemper checks the label before pulling any layers.
  • Two boot modes. By default (UKI) the image installs the kernel, a generic initrd and optionally a kernel command line at fixed paths, and contemper seals them into a Unified Kernel Image it builds. The image is the unit of change: you upgrade by redeploying a new image, with state on volumes. Alternatively, an image can ship its own EFI bootloader under /boot/efi (GRUB, systemd-boot, shim for Secure Boot), declared with LABEL io.contemper.boot="bootloader". The guest then updates its own kernel with its package manager, so it can run as a long-lived VM. contemper does not generate or install the bootloader. See choosing a boot mode.
  • Nothing from your image runs at conversion time. contemper merges layers and reads files; it never executes image content. No container runtime, no privileged builder, no emulation: an arm64 disk builds on an x86-64 host as easily as a native one.
  • The output is a bundle: a UEFI-bootable qcow2 disk (the EFI partition holds either contemper's Unified Kernel Image or the image's /boot/efi tree; the root is a writable ext4 filesystem) plus a contemper.json recording where it came from and the boot mode.
  • Targets add what a platform needs through support images: plain OCI images layered on top, never executed.

How it compares

Tool Authoring input Runs code to build Scope
contemper any OCI image no image → bootable disk
bootc Containerfile on a bootc base yes (ostree) image → disk and in-place updates
d2vm Dockerfile or a Docker image yes (docker build, chroot) image → disk, several formats
Packer provisioner scripts yes (boots an instance) broad; many clouds
mkosi declarative config and packages yes (distro installers) OS images, systemd-centric
distrobuilder YAML definitions yes Incus/LXC images

bootc is the closest in idea: it shares the belief that container authoring suits OS images. d2vm is the closest in workflow. The differences from bootc: bootc starts from its own prescriptive base images, while contemper accepts any base you install a kernel and init system into (Alpine and OpenRC included). bootc gives host-style semantics (/usr read-only, /etc and /var merged forward) and transactional in-place updates of the image. contemper's default (UKI) behaves like a container: a writable root kept across reboots and replaced on redeploy, with persistent data on volumes. Bootloader images can instead run as long-lived VMs that the distro's package manager updates in place. What contemper lacks is an image-based update mechanism of its own, nothing like bootc switch: it stops at producing a disk.

d2vm also turns a Dockerfile or a Docker image into a VM disk, but as a conversion tool: for a supported distribution it installs a kernel, an init system and a bootloader into the image through docker build, then assembles the disk with loop devices, mounts and a chroot, which needs root. contemper takes all of that from the image, runs nothing from it, and needs no root. d2vm covers what contemper doesn't: BIOS boot, a LUKS-encrypted root, build-time settings such as a root password, more disk formats and KubeVirt containerDisks.

Packer and virt-builder, in their common cloud-image workflow, customize an existing image by running code (Packer can also install a full OS from an ISO); mkosi and distrobuilder build from packages through their own config formats. contemper's input is whatever your container tooling already produces. More in docs/comparison.md.

Status

contemper is pre-1.0: the CLI and the bundle format may still change.

Works today:

  • convert for the qemu and incus targets (the latter adds the Incus agent through the default incus-support image), build (with Docker or podman), and deploy --to local-qemu.
  • Two boot modes: a UKI built by contemper, or the image's own bootloader with in-place kernel updates and Secure Boot support.
  • convert and deploy handle several architectures at once.
  • Support images can declare variants, selected by what's actually present in your image (an init system, a first-boot mechanism).
  • Volumes: declare one with VOLUME in your build file, and contemper sizes, formats and mounts it in the guest through a first-boot helper, persisted across redeploys for local-qemu instances.

Planned: publish and deploy for Incus. See the roadmap for the rest.

Documentation

The documentation lives in docs/ and is published at https://contemper-project.github.io/contemper/. Start with Getting started, or read the design for the reasoning behind the choices.

Building and testing

$ go build -o contemper ./cmd/contemper
$ make test   # unit tests
$ make e2e    # build, convert and boot the example image

convert needs e2fsprogs and qemu-img; deploy needs qemu-system-* and UEFI firmware. See host tools.

License

Apache License 2.0, see LICENSE. The embedded systemd-stub binaries are LGPL-2.1-or-later; see NOTICE and internal/uki/stubs/README.md for where they come from and how to verify them.

Directories

Path Synopsis
cmd
contemper command
Command contemper converts a contemper-ready OCI image into a bootable VM disk bundle, and can deploy that bundle locally for testing.
Command contemper converts a contemper-ready OCI image into a bootable VM disk bundle, and can deploy that bundle locally for testing.
hack
distro-entry command
Command distro-entry reads the distribution matrix.
Command distro-entry reads the distribution matrix.
distro-report command
Command distro-report turns the results of a distribution test run into GitHub issues: one issue per matrix entry and architecture, opened when it fails, commented on when it keeps failing, and closed when it passes again.
Command distro-report turns the results of a distribution test run into GitHub issues: one issue per matrix entry and architecture, opened when it fails, commented on when it keeps failing, and closed when it passes again.
volumes-support/buildimg command
Command buildimg builds and pushes the volume-formatting support image and its two init-system variant images (support/volumes-support/{base,openrc,systemd}) as multi-platform (linux/amd64, linux/arm64) indexes, for a given registry prefix and tag - the content is architecture-independent (a POSIX sh script, an OpenRC init script, a systemd unit and its enablement symlink), so each platform's image differs only in its config's declared architecture, not its files.
Command buildimg builds and pushes the volume-formatting support image and its two init-system variant images (support/volumes-support/{base,openrc,systemd}) as multi-platform (linux/amd64, linux/arm64) indexes, for a given registry prefix and tag - the content is architecture-independent (a POSIX sh script, an OpenRC init script, a systemd unit and its enablement symlink), so each platform's image differs only in its config's declared architecture, not its files.
internal
buildinfo
Package buildinfo reports the contemper version, commit and build date.
Package buildinfo reports the contemper version, commit and build date.
bundle
Package bundle writes contemper.json, the manifest that accompanies a bundle's disk file: which image it came from, which target was resolved, and what the image declared (volumes, ports, ...).
Package bundle writes contemper.json, the manifest that accompanies a bundle's disk file: which image it came from, which target was resolved, and what the image declared (volumes, ports, ...).
disk
Package disk builds the on-disk artifacts of a contemper bundle: the ext4 root filesystem (via mkfs.ext4 + debugfs, never by extracting image content to the host filesystem under its real name), the FAT32 ESP (via go-diskfs), the GPT layout tying them together, and the qcow2 conversion.
Package disk builds the on-disk artifacts of a contemper bundle: the ext4 root filesystem (via mkfs.ext4 + debugfs, never by extracting image content to the host filesystem under its real name), the FAT32 ESP (via go-diskfs), the GPT layout tying them together, and the qcow2 conversion.
guestmeta
Package guestmeta renders the convert-time-only facts contemper writes into a bundle's guest filesystem under /etc/contemper/, and the fstab lines it appends for declared volumes.
Package guestmeta renders the convert-time-only facts contemper writes into a bundle's guest filesystem under /etc/contemper/, and the fstab lines it appends for declared volumes.
hostenv
Package hostenv discovers the host tools contemper shells out to (mkfs.ext4, debugfs, e2fsck, qemu-img, qemu-system-*), looking on PATH first and then in well-known Homebrew keg-only locations, and prints an install hint when a tool is missing.
Package hostenv discovers the host tools contemper shells out to (mkfs.ext4, debugfs, e2fsck, qemu-img, qemu-system-*), looking on PATH first and then in well-known Homebrew keg-only locations, and prints an install hint when a tool is missing.
imagebuild
Package imagebuild drives a container build engine - `docker buildx build` or `podman build` - as a subprocess, for `contemper build` to build an image before handing it to the docker-daemon: or containers-storage: source.
Package imagebuild drives a container build engine - `docker buildx build` or `podman build` - as a subprocess, for `contemper build` to build an image before handing it to the docker-daemon: or containers-storage: source.
imgtest
Package imgtest builds synthetic in-process OCI images for tests, so merge/validate/support logic can be exercised without a registry or a real container build.
Package imgtest builds synthetic in-process OCI images for tests, so merge/validate/support logic can be exercised without a registry or a real container build.
limits
Package limits bounds how much content contemper accepts from an image while merging its layers and unpacking archives.
Package limits bounds how much content contemper accepts from an image while merging its layers and unpacking archives.
localqemu
Package localqemu implements the per-instance state `deploy --to local-qemu` keeps between deploys: a state directory, and one qcow2 file per declared volume inside it, created once and reused on later deploys while the root disk itself resets every time (booted with qemu's snapshot=on).
Package localqemu implements the per-instance state `deploy --to local-qemu` keeps between deploys: a state directory, and one qcow2 file per declared volume inside it, created once and reused on later deploys while the root disk itself resets every time (booted with qemu's snapshot=on).
progress
Package progress renders the convert/deploy pipelines' progress output: one icon-and-two-column line per stage (name on the left, status/timing on the right).
Package progress renders the convert/deploy pipelines' progress output: one icon-and-two-column line per stage (name on the left, status/timing on the right).
qemu
Package qemu implements `deploy --to local-qemu`: booting a contemper bundle's disk under a host-discovered qemu-system-* binary with UEFI firmware, snapshot=on so the bundle disk stays pristine.
Package qemu implements `deploy --to local-qemu`: booting a contemper bundle's disk under a host-discovered qemu-system-* binary with UEFI firmware, snapshot=on so the bundle disk stays pristine.
rootfs
Package rootfs builds the merged filesystem view of a source image (plus an optional support-image overlay) as a flattened tar stream on disk, and provides safe, index-based lookup and symlink resolution over it without ever extracting file content to the host filesystem under its real name or ownership.
Package rootfs builds the merged filesystem view of a source image (plus an optional support-image overlay) as a flattened tar stream on disk, and provides safe, index-based lookup and symlink resolution over it without ever extracting file content to the host filesystem under its real name or ownership.
source
Package source resolves a contemper source reference (a registry image, an OCI archive, an OCI layout directory, a docker-archive tarball, or an image already loaded into the local Docker daemon) into a platform-selected v1.Image, and checks the contemper readiness label before any layer content is fetched.
Package source resolves a contemper source reference (a registry image, an OCI archive, an OCI layout directory, a docker-archive tarball, or an image already loaded into the local Docker daemon) into a platform-selected v1.Image, and checks the contemper readiness label before any layer content is fetched.
subprocess
Package subprocess builds *exec.Cmd values for the host tools contemper shells out to (mkfs.ext4, debugfs, e2fsck, qemu-img, qemu-system-*, docker), with consistent behavior when their context is canceled - in particular, when contemper itself is asked to stop by SIGINT or SIGTERM (see cmd/contemper's signal handling).
Package subprocess builds *exec.Cmd values for the host tools contemper shells out to (mkfs.ext4, debugfs, e2fsck, qemu-img, qemu-system-*, docker), with consistent behavior when their context is canceled - in particular, when contemper itself is asked to stop by SIGINT or SIGTERM (see cmd/contemper's signal handling).
support
Package support parses and resolves a support image's declarations (carried as image config labels): its unconditional file requirements, and its branch/variant declarations.
Package support parses and resolves a support image's declarations (carried as image config labels): its unconditional file requirements, and its branch/variant declarations.
target
Package target maps a target name to its canonical spelling and Assembler, and implements the UEFI qcow2 assembler shared by the qemu and incus targets: UKI (or the image's own bootloader), then GPT/ESP/ext4, then qcow2.
Package target maps a target name to its canonical spelling and Assembler, and implements the UEFI qcow2 assembler shared by the qemu and incus targets: UKI (or the image's own bootloader), then GPT/ESP/ext4, then qcow2.
uki
Package uki builds a Unified Kernel Image: a systemd-stub UEFI binary with .osrel, .cmdline, .initrd and .linux sections appended, in that order, the way systemd-ukify lays them out.
Package uki builds a Unified Kernel Image: a systemd-stub UEFI binary with .osrel, .cmdline, .initrd and .linux sections appended, in that order, the way systemd-ukify lays them out.
validate
Package validate checks the merged rootfs against contemper's fixed path contract: a kernel, an initrd and an init binary, plus an optional command line, at fixed locations.
Package validate checks the merged rootfs against contemper's fixed path contract: a kernel, an initrd and an init binary, plus an optional command line, at fixed locations.
volume
Package volume implements contemper's volume declaration model: naming (explicit or derived from the path), size parsing, and turning an image's `VOLUME` paths plus its `io.contemper.volume.*` labels into resolved Spec values.
Package volume implements contemper's volume declaration model: naming (explicit or derived from the path), size parsing, and turning an image's `VOLUME` paths plus its `io.contemper.volume.*` labels into resolved Spec values.
volumehelper
Package volumehelper resolves and loads contemper's automatically merged volume-formatting support image: the same schema/predicate mechanism a user's own --support image uses (see internal/support), applied to a fixed, contemper-supplied image instead of one named on the command line.
Package volumehelper resolves and loads contemper's automatically merged volume-formatting support image: the same schema/predicate mechanism a user's own --support image uses (see internal/support), applied to a fixed, contemper-supplied image instead of one named on the command line.
test
distros
Package distros describes the distribution test matrix: the entries in matrix.json, each naming a test image (test/distros/<containerfile>) and the base image and build arguments it is built with.
Package distros describes the distribution test matrix: the entries in matrix.json, each naming a test image (test/distros/<containerfile>) and the base image and build arguments it is built with.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL