contemper

module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 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. With Docker, contemper build runs the build 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 installs a kernel, a generic initrd and an init system at fixed paths (a kernel command line there too, optionally, for parameters beyond what contemper sets itself), and is marked with LABEL io.contemper.ready="true". contemper checks the label before pulling any layers.
  • 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 (a Unified Kernel Image on the EFI partition, a writable ext4 root) plus a contemper.json recording where it came from.
  • 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
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

The closest is bootc, which shares the idea that container authoring suits OS images. The differences: 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's point is transactional in-place updates, while contemper stops at producing a disk. And bootc gives host-style semantics (/usr read-only, /etc and /var merged forward), where contemper's VMs behave like containers: a writable root kept across reboots and replaced on redeploy, with persistent data on volumes.

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 target, build (with Docker; podman support is planned), and deploy --to local-qemu. Images can also bring their own bootloader and update their kernel in place. Support images can declare variants, selected by what's actually present in your image (an init system, a first-boot mechanism). Volumes are supported too: 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: a publish adapter through your usual container tooling, then an Incus target and provider. 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.
e2e-variants/buildimg command
Command buildimg pushes a small synthetic OCI image straight to a registry, for hack/e2e-variants.sh: a dummy support image (or one of its variants) built from a handful of files under hack/e2e-variants/files/ plus a set of manifest annotations.
Command buildimg pushes a small synthetic OCI image straight to a registry, for hack/e2e-variants.sh: a dummy support image (or one of its variants) built from a handful of files under hack/e2e-variants/files/ plus a set of manifest annotations.
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.
buildx
Package buildx drives `docker buildx build` as a subprocess, for `contemper build` to build an image before handing it to the docker-daemon: source.
Package buildx drives `docker buildx build` as a subprocess, for `contemper build` to build an image before handing it to the docker-daemon: source.
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.
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 annotations: its unconditional file requirements, and its branch/variant declarations.
Package support parses and resolves a support image's annotations: 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