runtimeClassName: k7 run inside k7d microVMs instead of plain containers, and pods with runtimeClassName: k7-fc run the same guest under a stock Firecracker jail (see Backends). Four steps register the runtime on a k3s node.
The release installer does all four steps for you:
sudo ./install.sh --with-k3s. The manual steps below are for understanding what it does, or for non-k3s containerd setups.1
Install and start the daemon
The shim is a thin client: sandbox The guest pair is required: the daemon is the only process that decides which guest a VM boots, and without
Create calls the daemon over /run/k7d/k7d.sock and fails loudly if it is not running.K7D_KERNEL / K7D_INITRD it refuses to create a VM rather than guessing. LimitNOFILE matters as soon as you fork clusters — the 1024 soft default runs out fast.2
Install the shim binary
io.containerd.k7.v1 → containerd-shim-k7-v1).3
Register the runtime in containerd
Add to Two things matter here:
/var/lib/rancher/k3s/agent/etc/containerd/config-v3.toml.tmpl:pod_annotationsforwards thek7d.katakate.org/*pod annotations (cluster-id,fork-vm-index, …) into the sandbox OCI spec so the shim can see them — required for cluster-mode pods deployed via kubectl.- Do not add
BinaryNameunder[options]— the option value leaks into the cgroup path and breaks shim startup.
io.kubernetes.cri.runtime-handler, so the shim cannot tell the two handlers apart any other way):4
Restart k3s and create the RuntimeClass
k7-fc, add a RuntimeClass with the measured PodOverhead so kubelet accounts for the out-of-process VMM:kubectl get runtimeclass k7 k7-fc — handlers k7 and k7-fc.Using it
kubectl logs, kubectl exec (including -it with PTY, resize, Ctrl-C, detach), pod IPs, Services, DNS, ConfigMaps/Secrets, emptyDir/hostPath/PVC volumes, memory/CPU limits, init containers, natural exit / restartPolicy, and multi-vCPU guests all work — see the feature matrix for the full support table and what’s still to come (hostNetwork, NetworkPolicy, IPv6, cross-node fork). Swap in runtimeClassName: k7-fc for the Firecracker engine; everything above works there too except hostPath / virtiofs volumes and time warp, which fail Create with a named error.
The pod’s securityContext is honoured inside the VM: seccomp profiles, maskedPaths / readonlyPaths, device cgroups, capabilities and noNewPrivileges are passed through to the guest runc unchanged — see Security.
CPU limits and vCPUs: the shim derives the guest’s vCPU count from the pod’s CPU limit (
cpu: "2" boots a 2-vCPU guest), and the limit is additionally enforced on the host via a CFS quota on the VM’s vCPU threads. Fork and snapshot preserve multi-vCPU state.Docker inside the pod
dockerd (27.5.1, with Compose and Buildx on PATH) whose overlay2 graph lives on a dedicated per-VM virtio-blk scratch disk; the binaries come from a read-only, host-enforced payload slot at /run/k7d/docker-tools. Nothing is added to the pod spec and no container is privileged. Because it is a guest service rather than a CRI sidecar, a warm fork keeps dockerd and its running containers (the child’s graph disk is a reflink). Any value other than "true", or docker-disk without docker, fails Create. docker.sock is guest root — read Docker as a guest service before enabling it for untrusted workloads, and prefer k7-fc for multi-tenant use. K7 exposes this as k7 create --docker.
Sizing writable storage
A container’s writes go to an overlay upper on the sandbox’s scratch disk — a sparse ext4 image on the host, attached as a read-write virtio-blk device — not to guest RAM. Writable capacity is therefore independent of the pod’s memory limit, and filling it gives the writing process an ordinaryENOSPC rather than guest-wide memory pressure.
Size it per pod (default 8 GiB):
name=size list over container names:
ENOSPC while siblings keep writing. Containers you don’t name go on sharing the pod’s scratch disk.
Per-container scratch is opt-in for two reasons: each image spends one of the VM’s virtio-MMIO device slots, and a pod that uses it is served by a fork rather than from the warm pool. A warm fork gets a reflink-CoW copy of every image, so a fork’s writes — and its usage numbers — are isolated from its parent and its siblings.
resources.limits.ephemeral-storage never reaches the runtime and these annotations are the only sizing input. Usage is measured inside the guest and surfaced in the shim log, not in kubectl top.
Persistent data disks
Scratch dies with the pod. For state that must outlive the VM — a storage node’s replica directory, a database’s data dir — ask for named disks:id:size:guest-mount entry attaches an ext4 image data/<id>.img on the daemon’s reflink filesystem, created and formatted on first use and reused as-is on every later attach. Sandbox teardown leaves it in place; only the daemon’s delete_data_disk op removes it. A size that disagrees with an existing image is an error, not a resize. A warm fork gets a reflink-CoW copy, so a fork’s writes diverge from the source’s without a byte copy. Like per-container scratch, every disk spends one virtio-blk device slot, which is why it is opt-in.
Annotation reference
All values fail loud on a malformed entry — a pod that asked for a disk and silently did not get one would lose its data with the VM, which is the failure the annotations exist to prevent.
Because VMs outlive shims
The daemon owns the VMs, so a containerd restart or shim crash does not kill your workloads: a freshly respawned shim resolves its sandbox back to the still-running VM via the daemon’sreattach_sandbox operation, which also verifies the guest agent still answers before handing the VM back.
The daemon also exposes pause_vm / resume_vm for a sandbox VM: vCPU threads are stopped and device workers parked while guest memory, devices, and the vsock CID stay put. This is what K7’s k7 pause uses against the k7d backend.