An OCI-compliant container runtime for balenaOS hostapp extensions. It
implements the OCI runtime spec interface (create, start, kill,
delete, state) but instead of running long-lived processes, it executes
overlay-based extensions that apply filesystem changes to the host and exit
immediately.
The runtime is invoked by containerd as a shim — it is not called directly by users.
# Build the binary (statically linked, no CGO)
make build
# Run unit tests
make test
# Run static analysis
make vetE2E tests require the binary to be built first:
make build && go test -v ./e2e/The runtime follows the standard OCI container lifecycle:
create— Readsconfig.json, validates extension labels, runs thehooks/createhook, spawns a proxy process, and writes OCI statestart— Runs thehooks/starthook, signals the proxy to exit cleanly (SIGUSR1), and transitions the container tostoppedkill— Sends a signal to the proxy processdelete— Runs thehooks/deletehook and removes runtime statestate— Returns OCI state JSON to stdout
Unlike traditional runtimes, extensions don't run persistent processes. They
apply overlay filesystem changes during their hooks and then exit. The start
command intentionally transitions the container to stopped — this is by
design.
The runtime spawns a proxy subprocess (balena-extension-runtime proxy)
during create to give containerd a real PID to track between create and
start. The proxy blocks on signals:
- SIGUSR1 — "start complete", exit cleanly (container shows "Exited (0)")
- SIGTERM/SIGINT — killed, exit cleanly
Extensions are identified by OCI annotations (image labels):
| Label | Required | Description |
|---|---|---|
io.balena.image.class |
yes | Must be overlay |
io.balena.image.requires-reboot |
no | Whether the host must reboot after install |
io.balena.image.kernel-version |
no | Kernel ABI version (M.m.p) for userspace compatibility |
io.balena.image.kernel-abi-id |
no | Kernel binary interface identifier for module/eBPF compatibility |
Extensions can ship executable scripts at <rootfs>/hooks/{create,start,delete}.
Hooks receive the following environment variables:
EXTENSION_ROOTFS— absolute path to the extension rootfsEXTENSION_IMAGE_*— allio.balena.image.*annotations converted to env vars (e.g.,io.balena.image.kernel-abi-idbecomesEXTENSION_IMAGE_KERNEL_ABI_ID)
OCI state is persisted as JSON under
$XDG_RUNTIME_DIR/balena-extension-runtime/<container-id>/state.json.
Writes use atomic rename for crash safety.
- Go 1.22+
- Linux (uses syscall signals and process management)