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.
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.
- 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 withLABEL 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/efitree; the root is a writable ext4 filesystem) plus acontemper.jsonrecording 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.
| 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.
contemper is pre-1.0: the CLI and the bundle format may still change.
Works today:
convertfor the qemu and incus targets (the latter adds the Incus agent through the defaultincus-supportimage),build(with Docker or podman), anddeploy --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.
convertanddeployhandle 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
VOLUMEin your build file, and contemper sizes, formats and mounts it in the guest through a first-boot helper, persisted across redeploys forlocal-qemuinstances.
Planned: publish and deploy for Incus. See the
roadmap for the rest.
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.
$ go build -o contemper ./cmd/contemper
$ make test # unit tests
$ make e2e # build, convert and boot the example imageconvert needs e2fsprogs and qemu-img; deploy needs qemu-system-*
and UEFI firmware. See host tools.
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.