Skip to main content
k7d boots every VM through the same daemon and the same containerd shim. The backend is which VMM process owns the KVM fd:
  • native (default) — k7d’s in-process rust-vmm VMM. The VM lives in the daemon’s address space; warm fork is a MAP_PRIVATE CoW of guest memory (~5 ms). This is what every other k7d page describes.
  • firecracker — a stock, sha-pinned upstream Firecracker v1.16.1 process, launched under the stock jailer. Same guest kernel, initramfs, agent, protocol, fork/tree/pool verbs and Docker service; the VMM is the audited upstream binary and sits in its own uid, chroot, pid namespace, cgroup and seccomp filter.
The Firecracker engine ships in k7d 0.6.0 (install.sh --with-k3s registers RuntimeClass k7-fc and the ConfigPath runtime option). The 0.5.0 tarball does not.

Selecting a backend

Three knobs, one result (VmConfig.backend). The first one that yields a backend wins; a contradiction is a loud Create error. Rules:
  • runtimeClassName: k7-fc ⇒ Firecracker, even with no pod annotation.
  • runtimeClassName: k7 ⇒ native unless the pod carries k7d.katakate.org/backend: firecracker (per-pod opt-in).
  • k7d.katakate.org/backend: native on a k7-fc pod is a Create error containing contradicts. The RuntimeClass is never silently overridden.
  • Any other annotation value fails Create — a typo must not boot a pod on the wrong engine.
Gate admission on runtimeClassName (k7 vs k7-fc); k7d does not ship a Kyverno/OPA policy. In K7 the two RuntimeClasses surface as the k7d and k7d-fc backends.

Feature matrix

Unsupported is a named error, never a silent downgrade: a pod that asked for virtiofs or time warp on k7-fc does not reach Running.

How a Firecracker fork works

A native tree forks by mapping the source’s memory MAP_PRIVATE. A Firecracker tree has no such mapping, so a point is:
  1. PUT /snapshot/create on the paused source — a Diff snapshot by default (only pages dirtied since the previous point; K7D_FC_FORK_SNAPSHOT=full writes all of RAM and exists for cross-checking, not as a fallback).
  2. The Diff extents are FICLONERANGEd onto a root-owned base.mem; restore-N.mem is a FICLONE of the result — the point costs its dirty set, the base costs O(1).
  3. Each child is a fresh jailed Firecracker LoadSnapshoted from its own reflink of restore-N.mem (mem_backend: File, clock_realtime), with writable disks reflinked from clones taken at the same paused instant. Every child extent reports FIEMAP_EXTENT_SHARED — CoW is proven, not assumed.
Cluster fork runs that primitive on every member after a parallel PATCH /vm Paused (pause skew p99 ≈ 125 µs across 20 runs). Suspend exports a Full point out of the jail and drops the process; resume re-stages a jail and LoadSnapshots with network_overrides so the member keeps its TAP identity. Measured on an exclusive Hetzner node (2026-09-08, 256 MiB / 1 vCPU busybox guests, 500 ms per-child ceiling asserted in tests): Most of the per-child cost is staging a chroot, leasing a uid, spawning jailer (new namespaces, pivot_root, cgroup) and waiting for its API socket — the warm pool of pre-staged jails is what recovers it. See LATENCY_BUDGETS.md for the native rows.

What the jail buys, and what it does not

Multi-tenant --docker should use k7-fc: the guest side is identical, so it is a RuntimeClass switch, not a rewrite. The VMM jail is the extra boundary; the in-guest docker.sock is still guest root. See Security.

Installing

  • packaging/install.sh --with-k3s registers both k7 and k7-fc (containerd blocks without BinaryName, /etc/k7d/shim-k7-fc.toml, RuntimeClass k7-fc with overhead.podFixed 32Mi / 50m).
  • utils/install-firecracker.sh puts the pinned firecracker and jailer in /usr/local/bin, verifying every sha256 from guest/fc/pins.env. A digest mismatch is fatal. k7d doctor reports the version and sha it finds.
  • Daemon knobs: K7D_FC_CHROOT_BASE (jail root; default /var/lib/k7d/fc, a link onto the reflink XFS that holds the writable disks so they can be hard-linked into a jail), K7D_FC_UID_BASE (first uid of the per-VM lease range; default 60000, 1024 live VMs per daemon, exhaustion is an error rather than a wrap-around), K7D_FC_FORK_SNAPSHOT (diff default, full), K7D_POOL_BACKEND (native | firecracker for the warm pool).
Known issue: K7 observed CRI exec into a k7-fc pod hanging after a k7 pause / k7 resume cycle (readiness probe timeout, K7 PERFORMANCE.md). Native k7 is unaffected.

Reference