Skip to content

Getting started

This walks through the example image in the repository: an Alpine appliance with OpenRC that boots to a serial login. Two commands get you there: contemper build turns the example's Containerfile into a bundle, and contemper deploy --to local-qemu boots it under QEMU.

Install

contemper is a single Go binary plus a handful of host tools it discovers rather than bundles (see Host tools for what each one is for). Pick whichever of these gets it and those tools onto your machine with the least fuss:

$ brew install contemper-project/tap/contemper

This installs e2fsprogs and qemu automatically (convert needs both; qemu also carries the UEFI firmware deploy --to local-qemu needs), and removes the quarantine attribute from the (unsigned) binary so macOS will run it.

Download the .deb matching your architecture from the releases page:

$ sudo apt install ./contemper_0.1.0_amd64.deb

apt installs e2fsprogs and qemu-utils automatically, which is everything convert needs. For deploy --to local-qemu, add the emulator and UEFI firmware for the bundles you boot:

$ sudo apt install qemu-system-x86 ovmf            # amd64 bundles
$ sudo apt install qemu-system-arm qemu-efi-aarch64  # arm64 bundles

Download the .rpm matching your architecture from the releases page:

$ sudo dnf install ./contemper-0.1.0-1.x86_64.rpm

dnf installs e2fsprogs and qemu-img automatically, which is everything convert needs. For deploy --to local-qemu, add the emulator for the bundles you boot; each pulls in its UEFI firmware:

$ sudo dnf install qemu-system-x86-core      # amd64 bundles
$ sudo dnf install qemu-system-aarch64-core  # arm64 bundles

Works on any Linux distribution: download the tarball for your platform from the releases page and put the contemper binary it contains on your PATH:

$ tar -xzf contemper_0.1.0_linux_amd64.tar.gz contemper

Then install the host tools yourself. convert always needs e2fsprogs and a qemu-img binary; deploy --to local-qemu additionally needs a system emulator and UEFI firmware for the architecture you're deploying (not necessarily your host's own — see Host tools for cross-architecture deploys):

  • macOS (Homebrew): brew install e2fsprogs qemu
  • Debian/Ubuntu: sudo apt install e2fsprogs qemu-utils to convert; add qemu-system-x86 ovmf (amd64 bundles) or qemu-system-arm qemu-efi-aarch64 (arm64 bundles) for deploy --to local-qemu
  • Fedora/RHEL: sudo dnf install e2fsprogs qemu-img to convert; add qemu-system-x86-core (amd64 bundles, pulls in edk2-ovmf) or qemu-system-aarch64-core (arm64 bundles, pulls in edk2-aarch64) for deploy --to local-qemu
  • Other distributions: install your package manager's equivalents of e2fsprogs and qemu-img, plus a system emulator and UEFI firmware if you'll use deploy --to local-qemu; see Host tools for exactly what each is for.
$ go install github.com/contemper-project/contemper/cmd/contemper@latest

This still needs the host tools listed under "Other Linux / manual" above; only the binary itself comes from Go. contemper version reports whatever version go install resolved (a pseudo-version if there is no tagged release yet) rather than the exact commit and build date a downloaded binary embeds.

Building the example below with contemper build needs Docker with buildx or podman. Build by hand works with either engine without it.

Shell completion

The Homebrew cask and the .deb/.rpm packages install bash, zsh and fish completions for you automatically. If you're running the bare binary from a tarball, set it up yourself:

$ mkdir -p ~/.local/share/bash-completion/completions
$ contemper completion bash > ~/.local/share/bash-completion/completions/contemper

or source it directly from your shell profile: source <(contemper completion bash).

$ mkdir -p ~/.zfunc
$ contemper completion zsh > ~/.zfunc/_contemper

Then add fpath=(~/.zfunc $fpath) to ~/.zshrc, before the line that runs compinit. Any other directory on $fpath works too.

$ mkdir -p ~/.config/fish/completions
$ contemper completion fish > ~/.config/fish/completions/contemper.fish

See contemper completion <shell> --help for shell-specific details.

Verifying downloads

Every release archive and package carries a signed build provenance attestation, so you can check that what you downloaded was actually built by this project's release workflow, straight from that commit and workflow run:

$ gh attestation verify contemper_0.1.0_linux_amd64.tar.gz --repo contemper-project/contemper

(with the archive, .deb or .rpm name you downloaded).

The release also carries the attestations as Sigstore bundles (checksums.txt.sigstore.json for checksums.txt, and contemper_<version>_provenance.sigstore.json for every archive and package), so you can verify offline after downloading the bundle next to the file:

$ gh attestation verify contemper_0.1.0_linux_amd64.tar.gz \
    --bundle contemper_0.1.0_provenance.sigstore.json --repo contemper-project/contemper

That's on top of the usual checksum check against the release's checksums.txt, which lists every archive and package and is itself attested the same way.

Build and boot the example

The example lives in examples/alpine/Containerfile. It installs a kernel, generates a generic initrd, writes a kernel command line and enables OpenRC, all at the paths contemper expects.

contemper build runs docker buildx build --load (or podman build, see --engine) on it and converts the result in one step. It picks up the Containerfile on its own when the directory has no Dockerfile:

$ contemper build --target qemu -o _out examples/alpine
_out/alpine-dev.aarch64

The image is tagged after the directory (alpine:dev), and the bundle is named after the tag and the architecture (x86_64 on an x86-64 host). A bundle is a directory holding a UEFI-bootable qcow2 disk and a contemper.json manifest. The build's progress, the engine's own output included, goes to stderr; stdout carries only the bundle's path.

Boot it:

$ contemper deploy --to local-qemu _out/alpine-dev.aarch64

This boots the disk under QEMU (hardware-accelerated where the host allows it) and streams the serial console to your terminal. The disk is booted with a throwaway overlay, so the bundle itself stays unchanged. Log in as root with no password.

To use the boot as a test, have contemper wait for a line on the serial console and exit once it appears. The example prints contemper-boot-ok when OpenRC finishes starting. Since build prints only the bundle's path, the two commands also chain:

$ contemper deploy --to local-qemu "$(contemper build --target qemu -o _out examples/alpine)" \
    --expect contemper-boot-ok --timeout 180s

contemper build uses Docker with buildx when it is usable and podman otherwise; --engine docker or --engine podman picks one. See source references for the docker-daemon: and containers-storage: sources it converts through.

Build by hand

Prefer this if you use podman, or want an OCI archive to keep or push rather than loading the image straight into Docker's local image store. These are the steps build runs for you: build and save the image with your container engine, then convert the archive.

$ podman build -t contemper-example:dev examples/alpine
$ podman save --format oci-archive -o example.tar contemper-example:dev
$ docker build -f examples/alpine/Containerfile -t contemper-example:dev examples/alpine
$ docker save -o example.tar contemper-example:dev

Docker doesn't look for a file named Containerfile on its own, so pass -f explicitly. Docker 25 and later write docker save output as an OCI layout, which the oci-archive: prefix below reads directly; with an older Docker, read the same file with docker-archive: in place of oci-archive:.

$ contemper convert --target qemu oci-archive:example.tar -o _out

contemper checks the image is marked ready, merges its layers, checks the kernel, initrd and command line are in place, and assembles a UEFI-bootable qcow2 disk. convert prints the bundle's path on stdout, and its progress on stderr, the same way build does. Boot it the same way as above:

$ contemper deploy --to local-qemu _out/contemper-example-dev.aarch64/ \
    --expect contemper-boot-ok --timeout 180s

That is exactly what make e2e (hack/e2e.sh) and the CI boot test do.

Next steps

Read Authoring an image to adapt this to your own image, starting from any base and any init system. examples/debian is the same walkthrough with systemd as init instead of OpenRC, if that's closer to what you're starting from, and examples/archlinux does the same on Arch Linux (amd64 only).