Revised 2026-09-27. This supersedes the decision of 2024-12-01, which chose crun as the primary container runtime with runc as an automatic fallback. That decision was never what shipped; the review that produced this revision is #85.
nexcage is one binary on a Proxmox VE host, called from two directions (OVERVIEW.md): a person or a script manages LXC containers with it, and a container engine — podman, containerd, CRI-O, a kubelet — drives it with the OCI runtime-spec command line. The 2024 decision was taken before the first of those existed as the centre of the project, and it assumed nexcage would call a runtime binary. Two questions need an answer that the code agrees with: which backend a container goes to by default, and how that is decided for a container whose caller never says.
The Proxmox LXC backend is the default. It turns commands into pct
and Proxmox API calls, across every node of the cluster, and is what a
plain build contains. Nothing selects it; everything not routed elsewhere
lands there.
The crun backend is libcrun, linked in. For an OCI bundle — what an
engine hands over — nexcage does the container work through the vendored
libcrun (deps/crun, a fork), not by executing a crun binary. It is an
opt-in build (-Denable-backend-crun=true) and ships as the -crun
release binary. It is the backend an engine’s host is configured for.
Routing is a name lookup, in the configuration, first match wins.
runtime.routing is a list of { "pattern", "runtime" }; a pattern is a
glob unless it starts with ^ or ends with $, in which case it is a
regular expression. * is the catch-all. --runtime overrides it for one
command. Since 0.17.0 it decides only where a new container goes; every
later command goes to the backend that has the container
(ADR-005,
#372). An engine never
passes --runtime, so a host that runs containers for one is configured
with a single rule:
{ "runtime": { "routing": [ { "pattern": "*", "runtime": "crun" } ] } }
container_config.routing and container_config.default_runtime are also
read, for older files; when both lists are present the container_config
one replaces the other wholesale. container_config.crun_name_patterns, a
glob list routing to crun that predates routing, is removed in this
revision: a glob under routing is the same matcher, so each entry had a
one-line equivalent. A file that still carries it gets a warning naming
the replacement — since 0.17.0 it refuses the file with that message, as
any key nexcage does not read does
(#371).
There is no fallback between backends. The 2024 design had
auto_fallback_enabled: try the primary, use the other if it is
unavailable. A container is one thing on one backend — an LXC container
and a libcrun container are not interchangeable — so a command routed to a
backend that is compiled out fails with UnsupportedOperation and says
how to build it, rather than quietly making a different kind of
container.
There is no runc backend. src/backends/runc shelled out to a runc
binary for create/start/kill/delete; no workflow ever built it and it was
never run, and a second OCI backend would implement the same interface
libcrun already answers. It was removed in 0.13.0
(#88). A routing rule
that still names runc routes to crun with a warning — the container it
describes is an OCI container either way — and --runtime runc is refused
with the replacement named.
There is no VM backend. src/backends/proxmox-vm was compiled out by
default and built by no workflow; the router refused every operation
routed to it without calling the driver, and the driver imported a module
the build did not define. It was removed in 0.14.0
(#306). Unlike runc,
nothing can stand in for it — the default backend would make a container
where a VM was asked for — so a configuration naming vm (or proxmox,
which was mapped to it) is refused as a whole, and --runtime vm exits 2
saying why. qm can come back with a test behind it.
Positive:
ctr, containerd’s CRI, CRI-O and a kubelet, with a Kubernetes pod
scheduled onto it through a RuntimeClass
(KUBERNETES_INTEGRATION.md).Config.getRoutedRuntime) with one source of
rules, and the simulator proves both directions of a rule.Negative:
features structs was first written against crun 1.23.1, without the
trailing field 1.24 added, and that segfaulted features once; in CI,
scripts/check_features_abi.sh now compares the field order in the vendored
header against the order the Zig mirror was written for, which the script
lists itself (it does not read the mirror, so the two have to be changed
together by hand). A crun bump is a deliberate change, not a version number
(#227).-crun binary needs libjson-c5, libseccomp2, libcap2 and
libsystemd0 on the host, installed with apt where missing; releases
before 0.13.0 needed libyajl2 instead, which a Proxmox VE 9 install lacks.| Then | Now | Why |
|---|---|---|
| crun primary, runc fallback | Proxmox LXC default; crun for engines; no runc | nexcage became a Proxmox-first runtime; the OCI half is driven by engines, and libcrun answers them |
call the crun binary |
link libcrun | one process, libcrun’s own errors, no PATH dependence |
| automatic fallback | none | a container is one thing on one backend |
crun_name_patterns |
runtime.routing |
one matcher, one list |
deps/crunLast updated: 2026-09-27 (revision); original 2024-12-01