kern

A fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and AI-generated code. stable release

~1.9 ms to a rootless, kernel-enforced box, vs ~308 ms for docker run. Daemonless, no root, no runtime to install. Runs on Linux, Windows (via WSL2), and ARM boards, Raspberry Pi, NVIDIA Jetson, Arduino UNO Q, the same ~1.6 MB binary everywhere.

$ curl -fsSL https://raw.githubusercontent.com/getkern/kern/main/install.sh | sh

Linux · Windows (WSL2) · ARM boards (Pi / Jetson / Arduino UNO Q), checksum-verified static binary, no toolchain.

PS> irm https://raw.githubusercontent.com/getkern/kern/main/install.ps1 | iex

Windows, one line in PowerShell: sets up WSL2 + kern automatically, no Docker Desktop. Inspect first: install.sh · install.ps1.

$ scp kern <board>: && ssh <board> kern box edge --image alpine -- sh

ARM boards (Raspberry Pi, NVIDIA Jetson, Arduino UNO Q), one ~1.6 MB static binary you scp and run: no daemon, no package. Or run the one-liner above on the board (it auto-detects aarch64). Runs where a Docker daemon is too heavy or absent, see EDGE.md.


kern

A fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and AI-generated code.

It's a daemonless container for your inner loop: a real, kernel-enforced box that starts in ~2 ms,
from one ~1.6 MB rootless binary, with no dockerd sitting in the background. The same box also runs
untrusted or agent-generated code, isolated. Embed it from Python, Node or Rust, or run it from the CLI.

Isolation is just the first resource kern manages this way: the same model also slices CPU (vcpu:),
memory, disk (vdisk:) and devices (vgpio:) per process, with or without a full box. The container is
one case of a smaller idea.

Runs everywhere Linux does: bare Linux, Windows (via WSL2), and ARM boards (Raspberry Pi, NVIDIA
Jetson, Arduino UNO Q), where a 186 MB Docker daemon is a poor fit (on the Pi 5 tested here, no engine
was installed at all).

~2 ms cold start (vs ~308 ms docker run) · ~1.6 MB static binary · 0 RAM at rest · rootless

🐧 Linux & ARM boards

curl -fsSL https://raw.githubusercontent.com/getkern/kern/main/install.sh | sh

🪟 Windows (via WSL2)

irm https://raw.githubusercontent.com/getkern/kern/main/install.ps1 | iex

then kern box dev --image alpine -- sh

CI
License: Apache-2.0
Platforms
Release

Terminal demo: a kern.toml defines reusable vcpu/vdisk/vgpio (device) profiles; 'kern box train --image alpine vcpu:heavy vdisk:scratch' attaches a 4-vCPU, 8 GB, 2 GB-scratch rootless isolated slice in a few ms (docker run takes ~308 ms); 'kern run vcpu:heavy -- ffmpeg' caps a heavy transcode with no sandbox; 'kern box iot --image alpine vgpio:sensor' exposes only /dev/i2c-1 and nothing else; piping a request into 'kern box fn --image python' runs it in a fresh isolated box per request (serverless style); 'kern compose stack.toml up' brings up a multi-box stack; 'kern top' is the live TUI for boxes, profiles and volumes: CPU, memory, disk and devices, sliced per box, in one ~1.6 MB static binary, no daemon.

Demo timings on an Intel i7-14700KF (20-core / 28-thread x86_64, Linux 7.0, systemd-user, cgroup delegated): the ~2.7 ms is a capped box start (the vcpu:heavy vdisk:scratch slice), a bare box is ~2 ms; a full kern box lifecycle (fork, isolate, run, tear down), not just kern's ~1.24 ms of setup. The comparison table under Performance is a separate Linux 6.17 desktop; your hardware and cgroup delegation differ, so measure your own. See Benchmarks for methodology and on-device board numbers.

Install · Quickstart · Docker compat · When to use · Embed (Rust / Python / Node) · How it works · Config & profiles · Storage · Benchmarks · Security


kern runs Linux workloads in real, kernel-enforced sandboxes: user + PID + mount + network +
UTS + IPC namespaces, an overlay or read-only root pivoted in, an always-on seccomp filter, and
cgroup limits. It pulls OCI images, builds them, runs them, and gets out of the way. No background
daemon, one short-lived process per box, started in single-digit milliseconds.

It's built around one idea, virtual resources, exposed as two verbs: box wraps a process
in a full isolated slice; run caps a resource on a process you launch yourself. Isolation is just
the first resource; the same model virtualizes CPU (vcpu:), memory, disk (vdisk:) and GPIO
devices (vgpio:)
today, with GPU on the roadmap. On the box side that's a full daemonless
container UX (OCI pull and build, overlay, volumes, secrets, in-box SSH, cp/pause/attach,
ps/exec/logs, compose, health, tag/push, save/load) in ~1.6 MB.

kern box dev --image alpine -- sh        # a throwaway, isolated Alpine shell, in a few ms

…or embed it, a fresh isolated box per call, for untrusted or agent-generated code (cloud-code-interpreter
territory, but local and ~1.6 MB: no cloud, no account, no VM):

import kern_sandbox as kern                     # pip install kern-sandbox
r = kern.run_code("print(sum(range(100)))")   # network OFF, hard caps, a timeout the binding enforces
print(r.stdout, r.success)                     # → a fresh, discarded-after box

kern vs Docker vs Podman

kern Docker Podman
Daemon no yes (dockerd + containerd) no
Rootless yes opt-in (rootless mode) yes
Cold start (a bare box) ~2 ms ~308 ms ~155 ms
Footprint one 1.6 MB static binary ~186 MB daemon stack multi-binary install
OCI images (pull / build) yes yes yes
Resource caps without a full box yes (kern run) no no

Startup numbers are from a labeled benchmark on one machine, measure your own. kern
covers a smaller surface than Docker (no overlay networks, no swarm) and spends it on a fast, rootless
core: single-digit-millisecond starts from one ~1.6 MB binary, no daemon. It is a kernel-boundary
sandbox for your own or semi-trusted code; for actively hostile multi-tenant code, a microVM is the
right tool, and SECURITY.md says exactly where the line is.

What you'd use it for

One move under all of these: a fresh, isolated, resource-capped box in ~2 ms, wherever you would
otherwise reach for a container, a cloud sandbox, or root.

Why kern

The model: two verbs

Verb Question it answers What it does Status
kern box "Isolate this workload, and slice its resources." Its own namespaces, overlay/read-only fs, private process tree, seccomp (the container), plus the same resource slices (--memory, --cpus, vcpu:, vdisk:, vgpio:). ✅ works now
kern run "Just slice resources, no sandbox." Run a command against a CPU / memory quota with no isolation: the lean governor on its own. (A GPU slice is on the roadmap.) ✅ works now

Both take resource slices; the difference is the sandbox. box = isolation + slices; run =
slices without the sandbox. They compose (run inside box). Both ship today.

Virtual resources

One model: a box gets only what you slice for it; a bare run adds the same caps to a process that
otherwise still sees the host. Every shipping cap is a real cgroup v2 or kernel control; devices are
deny-by-default. GPU slices are on the Roadmap.

Resource Flag / profile What the box gets Enforcement
CPU --cpus · --cpuset-cpus · --nice · vcpu: Fractional CPU-time quota, core pinning, priority cgroup cpu.max / cpuset, hard
Memory --memory · --memory-swap-max Hard RAM ceiling (+ swap allowance) cgroup memory.max, hard¹
Disk vdisk: · --size (named volumes) Size-capped scratch at /vdisk/<name> rootless: a RAM-backed tmpfs (counts against the box's memory cap); privileged: ext4-on-loop quota on real disk
Devices vgpio: Only the named GPIO/I²C/SPI/LED nodes, nothing else fresh /dev + fd-pinned bind + capability deny-list (raw-mem/disk/kvm refused)
PIDs --pids-limit Fork-bomb ceiling cgroup pids.max, hard
Block I/O --io-weight I/O bandwidth weight cgroup io
GPU (roadmap) A slice of a GPU (VRAM cap, then time-slice) 🚧 cooperative governor, first-party / noisy-neighbour, not a hard boundary

¹ On the default WSL2 kernel the memory controller isn't delegated: kern warns and shows the one-line
.wslconfig fix; enforced natively on Linux. Profiles (vcpu:/vdisk:/vgpio:) are reusable presets in
~/.config/kern/kern.toml, see docs/CONFIG.md. Author them with kern probe (list the
host resources you can slice), kern examples (print a sample kern.toml), and kern validate (check one).

Profiles

Define a named slice of resources once in ~/.config/kern/kern.toml (optional, the flags work without
it) and attach it to any run by a token, instead of retyping --cpus 2 --memory 1g --device /dev/i2c-1
on every call. The same profile attaches to kern box (isolated) and kern run (no sandbox), on any
Linux, with no daemon (hard cgroup caps still need a delegated cgroup; kern warns and falls back to
best-effort if there is not one):

kern box tool --image python vcpu:agent vgpio:sensor -- ./tool.py   # the same tokens on every call in a loop

Three kinds ship today (a GPU slice is on the Roadmap):

Two layers: declare the host resource (the physical layer), then carve named virtual profiles from it.
Every profile must name a backend so it is never ambiguously attached to nothing: a declared
[[cpu]]/[[gpio]]/[[disk]] id, or a reserved keyword: host (the whole host CPU, or the
host's own device nodes) or ram (a RAM-backed vdisk). The physical block itself stays optional:
use backend = "host" / "ram" without declaring one. Run kern examples for the full reference.

[[cpu]]                        # host CPU budget (physical)
id = "cpu:0"
cores = 8.0
[[vcpu]]                       # a named CPU + memory slice of it  ->  attach with  vcpu:agent
name = "agent"
backend = "cpu:0"              # REQUIRED: a [[cpu]] id above, or "host" for the whole host CPU
cpus = 2
memory = "1g"

[[disk]]                       # a host path for scratch volumes (physical)
id = "data"
path = "/var/lib/kern/volumes"
[[vdisk]]                      # a size-capped scratch on it  ->  vdisk:scratch
name = "scratch"
backend = "data"               # REQUIRED: a [[disk]] id above, or "ram" for a RAM-backed tmpfs
size = "8g"                    # a real ext4 quota only when privileged; rootless it is a RAM tmpfs
                               # charged to --memory, so size it under the box's memory cap

[[gpio]]                       # a named controller anchor; `kern config setup` writes one per host
id = "gpio:0"
[[vgpio]]                      # the grant: deny-by-default, ONLY these nodes cross in  ->  vgpio:sensor
name = "sensor"
backend = "gpio:0"             # REQUIRED: the [[gpio]] id above (like vcpu -> cpu:0, vdisk -> data)
i2c = ["/dev/i2c-1"]           # expose just I2C bus 1; every other /dev node is refused

What you can do in one line

An isolated container, zero setup

No daemon, no root: one ~1.6 MB binary.

kern box try --image alpine -- sh

Exactly one device, nothing else

Deny-by-default: only the peripheral you name crosses the boundary.

kern box iot --image alpine vgpio:sensor -- ./read.py   # only /dev/i2c-1 crosses in

A fresh sandbox per request

Serverless-style, on your own machine: one throwaway box per call.

echo "$payload" | kern box fn --image python -- handler.py

Where a Docker daemon is too heavy

The same box on a Pi or an Android-kernel board: just copy the one binary.

scp kern pi:  &&  ssh pi 'kern box edge --image alpine -- ./agent'

Build, tag, push, all daemonless

kern build -t app:1 . && kern tag app:1 registry.example/app:1 && kern push registry.example/app:1

Features

Daemonless, rootless, and complete: the full container UX plus resource slices, in one binary.

Every flag & command, grouped

Run anything, isolated

  • OCI images, any registry: --image alpine pulls (registry v2, multi-arch → your arch,
    gzip and zstd layers) and runs. Docker Hub, GHCR, GitLab, quay, Harbor, self-hosted, via the
    standard WWW-Authenticate challenge (Bearer or Basic). kern login stores creds 0600 and
    passes them to curl off-argv. Or bring a rootfs with --rootfs. Pull a foreign arch with
    kern pull --platform os/arch, then run it.
  • Governed slices: kern run caps a command with no sandbox; --memory / --cpus /
    --cpuset-cpus (pin) / --memory-swap-max / --pids-limit / --io-weight / --nice set hard
    cgroup v2 caps on any box or run (kern warns if a controller isn't delegated).
  • Writable by default: a copy-on-write overlay; the image stays immutable, scratch is discarded
    on exit. --read-only for a read-only root. Interactive TTY with -it (raw mode, resize-aware).
  • --init: a built-in PID-1 reaper (no zombies, forwards SIGTERM) without bundling tini.

Data & devices across the boundary

  • Volumes, full: -v src:dst[:ro] (symlink-safe) · named volumes (kern volume CRUD, with
    a per-volume --size quota) · network volumes (nfs:// / smb:// / sshfs://) mounted
    rootless via FUSE/GVFS.
  • Secrets: --secret NAME=value / NAME=- (stdin) / SRC[:NAME] (file) → /run/secrets/NAME
    (mode 0400) on a RAM tmpfs, never in the image or env.
  • vDisk (vdisk:): a size-capped scratch at /vdisk/<name>: RAM tmpfs rootless, or an
    ext4-on-loop image (persistent, real quota) when privileged.
  • vGPIO (vgpio:): expose only the listed GPIO/I²C/SPI/LED peripherals into a box
    (deny-by-default for the rest), for edge/IoT.
  • --tmpfs PATH[:size]: a fresh nosuid,nodev tmpfs (refused over hardened mounts).

Networking & identity

  • Modes: isolated loopback-only by default (--network none); --network host (= --net)
    for outbound; --hostname; --tun exposes /dev/net/tun for WireGuard / userspace VPNs.
  • Port publishing: -p [ip:]host:box from a rootless forwarder; binds 127.0.0.1 by default,
    0.0.0.0 only if you ask.
  • In-box SSH: --ssh 2222 runs a throwaway sshd (auto keypair or --ssh-key), published.
  • Pods: kern pod create + --pod <name>: a shared-network pod where boxes reach each other
    by name (--no-outbound to deny internet egress).
  • --user UID[:GID]: drop to a specific uid/gid (fails closed if unmapped).

Least privilege, configurable

  • Capabilities: 13 dangerous caps always dropped; --cap-drop CAP/ALL drops more,
    --cap-add CAP keeps one (still bounded by userns + seccomp).
  • Seccomp: an always-on denylist (kexec, kernel modules, ptrace, the mount API, setns,
    syslog, …); wrong-arch and x86_64 x32-ABI syscalls are killed, closing the alias bypass.
  • --privileged: opt-in, relaxes seccomp for a nested kern box (docker-in-docker): re-allows
    exactly unshare/setns/mount/umount2/pivot_root, keeps kexec/modules/bpf/io_uring/keyring
    blocked (stronger than Docker's --privileged), rootless-only. See SECURITY.md.

Lifecycle & operations, no daemon

  • Stay-up & health: --restart supervises a detached box; --health-cmd +
    --health-interval/-retries/-start-period/-timeout/-action probe it; kern ps shows
    HEALTH + PORTS.
  • Box ops: kern cp (symlink-confined, CVE-2019-14271-safe), pause/unpause (freezer),
    attach (live output), exec (join a running box).
  • Observe & manage: -d detached; ps / top (TUI) / stats / logs / inspect /
    stop / kill / killall / prune / gc / history / recover.
  • Compose: kern compose stack.toml (or a docker-compose.yml) brings up a multi-box stack
    in dependency order; kern up/down for the file in this dir.
  • Diagnostics: kern doctor (will boxes run here?), info, bench, shell completions.
  • Resource profiles: reusable [[vcpu]] / [[vgpio]] / [[vdisk]] in ~/.config/kern/kern.toml,
    attached by prefix (kern run vcpu:heavy vgpio:leds -- ./train.sh); managed with kern config.

Built-in hardening. User+PID+net+UTS+IPC+mount namespaces, self-pivot root, nosuid,nodev box
root, always-on seccomp, least-privilege caps, hard cgroup caps (via systemd-run where present);
every pulled blob sha256-verified and every layer vetted in-process (no ../absolute/device escapes,
decompression- & inode-bomb caps) before an isolated no-follow merge. Where a guarantee is cooperative
or opt-in (vGPIO/vDisk trust scope, network volumes), SECURITY.md says so.

Install

🐧 Linux & ARM boards (Raspberry Pi · Jetson · Arduino UNO Q). One line; auto-detects x86-64 / aarch64:

curl -fsSL https://raw.githubusercontent.com/getkern/kern/main/install.sh | sh

Served from github.com (read the script first if you like). It downloads the release binary for
your arch and verifies the sha256 before installing. No Rust toolchain required. (getkern.dev/install.sh
is a short alias.)

🪟 Windows. One line in PowerShell (no Docker Desktop, no Ubuntu):

irm https://raw.githubusercontent.com/getkern/kern/main/install.ps1 | iex

kern runs inside WSL2, a real Linux kernel, so the isolation (namespaces + seccomp) and --cpus
cap work for real. One honest exception: --memory needs a one-line WSL kernel tweak the default
omits (kern warns and shows it; the fix and the same note for Raspberry Pi OS live under
Requirements & limitations). On a native Linux host --memory is enforced
out of the box. The installer ensures the WSL2 engine (self-elevating for the one reboot it may need, then
resuming on its own), imports kern's own pre-baked distro (a tiny Alpine + kern, no Ubuntu, no
manual steps), drops the kern.exe shim on your PATH, and verifies end-to-end. Every download is
sha256-checked. After it finishes: kern box dev --image alpine -it -- sh. Honest caveat: kern runs
inside the WSL2 kernel, so it doesn't shed the VM weight native Linux does; the win is "no Docker
Desktop", not "no VM".

📦 Offline / air-gapped (a board or locked-down server with no internet). kern is a single
~1.6 MB static binary, so copying that one file is the install:

scp kern pi@raspberrypi:~/          # then:  ssh pi@raspberrypi kern box dev --image alpine -- sh

No daemon, no package, nothing to install on the target, which is why it runs where Docker can't
(see EDGE.md).

Download + verify by hand, or build from source
# Straight from GitHub Releases, check the checksum yourself (aarch64: swap x86_64 → aarch64):
curl -fsSL https://github.com/getkern/kern/releases/latest/download/kern-x86_64-unknown-linux-musl.tar.gz \
  | tar xz && install -m 755 kern ~/.local/bin/kern   # a matching .tar.gz.sha256 ships next to it

# Or build it:
git clone https://github.com/getkern/kern && cd kern && cargo build --release

Quickstart

Start here

A sandboxed shell from any OCI image. The image stays read-only; your writes go to a scratch overlay that vanishes on exit.

kern box dev --image alpine -it -- sh

Cap what it can use

Hard memory + CPU limits (cgroup v2). kern run is the leanest path: a quota on a host command, no sandbox.

kern run --memory 256M --cpus 0.5 -- ./crunch-numbers
kern box build --image alpine --memory 512M --cpus 1.5 -v "$PWD:/src" -w /src --net -- make

Run it as a service

Detached, a published port, restarts if it dies, health-checked, without a daemon.

kern box svc --image alpine -d -p 8080:80 --restart \
  --health-cmd 'wget -qO- localhost:80' -- httpd -f

See & control what's running

kern ps                   # running boxes, with PORTS + HEALTH
kern top                  # live TUI: boxes, CPU/RAM, profiles
kern exec svc -it -- sh   # shell into a running box
kern logs svc -f          # its output, followed live (--tail N for the last N)
kern diff svc             # files changed vs the image (C = changed, D = deleted)
kern update svc --memory 1g   # retune caps live; rename/wait/events too
kern stop svc             # stop it   (kern stop --all for everything)

Hand it a secret

Never baked into the image or env; delivered on a pipe, readable only inside the box. The network is
isolated by default, so --egress-allow opens exactly the one host it may reach (and wget is
Alpine's busybox built-in, no extra install):

printf "$DB_TOKEN" | kern box job --image alpine --secret TOKEN=- --cap-drop ALL \
  --egress-allow api.example.com \
  -- sh -c 'wget -qO- --header="Authorization: Bearer $(cat /run/secrets/TOKEN)" https://api.example.com/v1/me'

Before you rely on it

kern doctor               # will boxes even run on this host? preflight it
kern compose stack.toml   # bring up a stack in dependency order (TOML or compose.yml)

Build & publish images

kern builds OCI images from a Dockerfile without a daemon: each RUN is a real kern box, each
step a content-addressed layer, reused on an unchanged rebuild.

kern build -t app:1 -f Dockerfile .          # FROM RUN COPY ADD ENV WORKDIR USER CMD ENTRYPOINT SHELL …
kern build -t app:1 --build-arg VER=9 .       # build args; multi-stage (FROM … AS b; COPY --from=b)
kern save app:1 -o app.tar                    # export a docker-load-compatible image tar …
kern load -i app.tar                          # … and import one (docker save format)
kern tag app:1 registry.example/app:1         # give a cached image a second name
kern commit devbox warmenv:1                  # snapshot a running box's fs into a reusable image
kern login registry.example                   # (private) creds stored 0600
kern push registry.example/app:1              # publish as a single-layer OCI image

Warm start (kern commit). Bake an expensive one-time setup (apt/pip installs, a warmed cache,
compiled artifacts) into a local image once, then start the next box from it instantly. It reads the
box's kernel-merged overlay through /proc/<pid1>/root, so whiteouts are already resolved, and skips
every nested mount, so a -v volume or a secret is never baked into the image. It's docker commit,
daemonless. A filesystem snapshot, not live memory: processes restart fresh (write state to disk if you
need it back).

kern parses real-world Dockerfiles as-is (comments inside \ continuations, SHELL, BuildKit
RUN --mount/ADD <url> with --checksum/--chmod, COPY <<heredoc, FROM scratch, # escape
and BOM) and honours .dockerignore (also .kernignore), so a COPY . /app won't bake your
.git, .env or secrets into the image. Multi-stage builds run each stage in its own box and
confine COPY --from=<stage> to that stage's filesystem (a hostile source path or symlink can't read
the host). Layers pull as gzip or zstd. push normalizes ownership and strips setuid/setgid, so an
untrusted base can't smuggle a privilege-bit into what you publish. (build/push are the newest
surface, see Project status.)

Embed it

Run a sandboxed command straight from your program: a fresh isolated box per call (untrusted code,
agent tools, per-request workers), a structured result back, including rich mime-typed results
(charts, tables, the last expression) the way a Jupyter cell returns them, but local and daemonless.

Rust, the kern-isolation crate:

use kern_isolation::Sandbox;

let out = Sandbox::builder()
    .rootfs("/var/lib/kern/rootfs/alpine")
    .no_network()                    // isolated loopback-only netns
    .memory_limit_bytes(256 << 20)   // cgroup cap
    .timeout_ms(5_000)               // SIGKILL a runaway
    .build()?
    .run("python3", &["handler.py"])?;

assert!(out.success());              // + out.stdout / .stderr / .exit_code / .wall_ms

Python, the kern_sandbox package on PyPI (pip install kern-sandbox), for running semi-trusted or agent-generated code with fast local isolation:

import kern_sandbox as kern     # kernel-boundary isolation, not a microVM

# one-shot: throwaway box, network OFF, hard caps, a mandatory timeout the binding enforces
r = kern.run_code("print(sum(range(100)))")
print(r.stdout, r.success)

# restrict the network to an allowlist: an agent can pip-install but cannot exfiltrate elsewhere
r = kern.run_code("import urllib.request as u; ...",
                  egress_allow=["pypi.org", "files.pythonhosted.org"])

# rich results, the "code interpreter" pattern (no Jupyter kernel): the last expression, any
# display(), and every matplotlib figure are auto-captured as mime-typed result.results
with kern.Sandbox(setup="pip install matplotlib pandas", timeout_s=60) as s:
    r = s.run_code("import matplotlib; matplotlib.use('Agg')\n"
                   "import matplotlib.pyplot as plt; plt.plot([1, 4, 9])")
    png = next((x.png for x in r.results if x.png), None)   # chart PNG bytes, no savefig; send to the model
    r = s.run_code("import pandas as pd; pd.DataFrame({'a': [1, 2]})")
    r.results[0].html                      # the DataFrame as an HTML table (also .text)

Node / TypeScript, the kern-sandbox package on npm (npm install kern-sandbox), the same model for
the other half of the agent ecosystem (LangChain JS, the Vercel AI SDK), with types in the box:

import { runCode, withSandbox } from "kern-sandbox";

// one-shot: throwaway box, network OFF, hard caps, a timeout the binding enforces
const r = await runCode("print(sum(range(100)))");
console.log(r.stdout, r.success);

// a session: files persist across calls; run agent-generated JS or Python
await withSandbox({ memoryMb: 512, timeoutS: 30 }, async (s) => {
  await s.writeFile("data.csv", csvBytes);
  const out = await s.runCode("import pandas as pd; print(pd.read_csv('data.csv').shape)");
});

Warm kernel (sub-millisecond cells). For a REPL/notebook or an agent's tool loop, open a persistent
warm interpreter with Sandbox.kernel() (Python and Node): in-memory state persists across cells and the
per-cell cost drops from a full interpreter boot (about 10 ms) to sub-millisecond (about 300x, ~25k
cells/s on the Benchmarks host), with the same rich results, still network-off and
resource-capped.

MCP server for Claude Desktop / Cursor / Windsurf. The Python package also ships kern-mcp, a
dependency-free MCP stdio server that hands any MCP client a local, network-off code interpreter
backed by kern: run_code (python/bash/node), write_file, read_file, list_files, with charts
returned as image blocks. Point your client at the kern-mcp command (from
pip install kern-sandbox); set KERN_MCP_KERNEL=1 to route
run_code through the warm kernel. See bindings/python.

Safe by default: every relaxing argument (network, extra mounts) says so, and the binding owns
the timeout, so a timeout fault is a fact, not a guess. Both bindings use the installed kern (PATH
or KERN_BIN); see bindings/python (PyPI),
bindings/node (npm), and the
kern-isolation crate (git/path, not yet crates.io).

Platforms

Linux, multi-architecture. Prebuilt static (musl) binaries for linux-x86_64 and
linux-aarch64: one ~1.6 MB file, no Rust deps beyond libc (the pull path shells out to system
curl/tar).

Platform Arch Status
x86_64 Linux x86_64 ✅ primary + automated CI
Windows 10/11 (via WSL2) x86_64 ✅ CI-built shim + distro (install.ps1)
NVIDIA Jetson (L4T) aarch64 ✅ manually validated
Raspberry Pi 5 aarch64 ✅ manually validated
Arduino UNO Q (Android kernel, Debian userland) aarch64 ✅ manually validated

kern needs a Linux kernel with unprivileged user namespaces + cgroup v2, and a Linux
userland
. The kernel flavor doesn't matter: kern runs even on an Android kernel with a Linux
userland (the Arduino UNO Q). On Windows, WSL2 is that Linux kernel, and the one-line PowerShell
installer sets up WSL2 and drops in a pre-baked kern distro, so isolation and --cpus are enforced
for real (--memory needs a one-line WSL kernel tweak the default omits, see
Requirements & limitations; enforced natively on real Linux). Honest
caveat: you're inside the WSL2 VM, so it's "no Docker Desktop", not "no VM". kern does not run on stock Android-the-OS (Bionic, SELinux, userns off). Daemonless is a big
win on RAM-constrained boards (0 resident vs ~186 MB), see EDGE.md. ARM CI is tracked
in the issues.

Docker compatibility

kern speaks Docker's formats, so your existing images and stacks just work, but it does not
reimplement the Docker Engine API. It's a lightweight alternative, not a drop-in clone.

From your Docker setup kern
OCI images (Docker Hub, GHCR, quay, Harbor, self-hosted) ✅ pull & run: multi-arch, WWW-Authenticate v2 auth, gzip + zstd
docker-compose.yml kern compose reads real-world files as-is: depends_on (+ service_healthy/_completed conditions), healthcheck, deploy.resources.limits, YAML anchors/merge (<<: *x), extends, ${VAR:-default} interpolation, network aliases
Dockerfile build kern build: all common instructions, multi-stage, COPY --from=… (a build stage or an external image), COPY globs (*.txt, src/*, [ab].conf), BuildKit heredocs, ADD <url> (+ --checksum/--chmod), COPY --chmod (recursive, Docker-parity), FROM scratch, SHELL, # escape/BOM, --build-arg, layer cache, and honours .dockerignore. Daemonless: each RUN is a real box
.dockerignore (also .kernignore) ✅ excluded from the build context: keeps .git/secrets out of the image (last-match-wins, ! re-include, **)
docker save / load archives kern save / kern load: export/import an image tar, docker load-compatible
tag / push to a registry kern tag / kern push
Image management (docker images / rmi / search) kern images (list cached), kern rmi (remove, frees unshared layers), kern search (Docker Hub)
docker commit (container → image) kern commit <box> <image>: snapshots the box's filesystem to a reusable image (warm start); skips volumes/secrets
Docker Engine API / docker.sock ❌: tools that attach to the socket (Docker Desktop, some IDE/CI plugins) won't connect
Swarm ❌: use compose / --pod

Everyday docker commands

Most container-lifecycle verbs you type daily have a 1:1 kern equivalent (same name where it makes sense):

docker … kern … Notes
run / create box one verb; -d detaches, -it for a PTY
exec exec joins the box's namespaces
ps ps -q, --filter name=/status=/id=, --format '{{.Field}}', --json
logs logs --tail N, -f/--follow (bounded read, cheap on GB-size logs)
stop / kill stop / kill SIGKILL the box's process group
pause / unpause pause / unpause cgroup v2 freezer
attach attach Ctrl-C detaches, box keeps running
cp cp host↔box, symlinks can't escape the box root
inspect inspect --json
stats stats per-box CPU / memory
top (box processes) exec <box> ps plus kern top, the live TUI for every box
rename rename in place, pid unchanged
update update live cgroup caps, no restart (needs a delegated cgroup)
wait wait prints the exit code (137 after stop)
diff diff overlay-upper changes: C changed/added, D deleted
events events poll-based stream (start/die/rename); daemonless, best-effort
commit commit box → reusable image (warm start)
start (a stopped container) (none) by design: kern boxes are ephemeral - a stopped box isn't resumed, you launch a fresh one

Out of scope by design (daemonless): swarm / service / stack / node / context / plugin / trust / network CRUD / checkpoint, and anything that attaches to docker.sock.

When to use kern (and when not)

✅ Use kern when you want:

🔀 Reach for something else when you need:

kern states every boundary that is cooperative or opt-in plainly in SECURITY.md; being honest about the edges is the point.

How it works

   kern  ·  one static binary, no daemon        box · run · compose · exec · pull · top …
     │
     ▼
   runtime  (kern-isolation)
     ├─ namespaces   user · pid · net · mnt · uts · ipc
     ├─ rootfs       OCI overlay → pivot_root   (typestate: Mounted → OldRootReady → ReadOnly)
     ├─ devices      fresh /dev · vgpio passthrough · -v volumes (symlink-safe)
     ├─ cgroups v2   MemoryMax · CPUQuota · TasksMax
     ├─ seccomp      always-on denylist (+ wrong-arch / x32)
     └─ supervisor   fork → PID 1 → reap → exec / stats / stop
     │
     ▼
   images  (kern-oci)   registry v2 · sha256 per blob · in-process tar vetting

A kern box is one short-lived process tree: no daemon, no shared state.

  1. Namespaces. unshare into a fresh user + PID + UTS + IPC namespace (and, by default, an
    isolated loopback-only net namespace; --net shares the host's, opt-in, flagged in the status
    panel). A single-uid map makes your uid root inside the box only; --uid-range opts into a full
    sub-id range.
  2. Root filesystem. An overlay by default (image = read-only lower, a private upper takes
    writes); --read-only remounts it read-only after a self-pivot (pivot_root(".", ".")), which
    works even where a bind remount-RO is denied (some Android-kernel boards). Nothing is written into
    the rootfs, so many boxes share one read-only rootfs concurrently. (--bind-rootfs swaps the
    overlay for a direct bind: faster on a slow overlayfs, at the cost of a mutable shared source.)
  3. Devices, volumes & secrets. A fresh /dev with the safe nodes (+ /dev/net/tun on --tun);
    -v volumes bound in with targets resolved symlink-safely, confined to the new root; secrets
    on a RAM /run/secrets (0400); vdisk:/vgpio: mounting exactly their declared disk/peripherals.
  4. Lockdown. A clean env (no host secrets leak in), capabilities stripped to least-privilege, an
    optional --user drop, an always-on seccomp denylist (incl. wrong-arch + x32), and cgroup caps:
    hard MemoryMax/CPUQuota/TasksMax when a systemd user manager is present.

The whole mount sequence flows through a typestate (Rootfs<Mounted> → OldRootReady → ReadOnly):
the read-only remount is only reachable after the pivot, so getting the order wrong is a compile
error. The same sequence drives --plan.

OCI images pull with curl + tar (registry v2, WWW-Authenticate auth, multi-arch, gzip/zstd),
each blob sha256-verified, each layer vetted in-process from its raw tar headers (absolute/..
paths, device nodes, escaping hardlink/symlink targets, decompression- & inode-bomb caps) before it
extracts into isolated staging and merges no-follow, closed by parsing the layer, not by trusting
the host tar, so it holds on GNU tar and BusyBox tar alike. Every layer is HTTPS-fetched and sha256-verified; credentials
travel off-argv. See ARCHITECTURE.md.

Verify the claims in the source: namespaces + the mount pipeline
· the always-on seccomp denylist
· cgroup caps (memory/pids)
· OCI pull + in-process tar vetting.

Performance

One isolated /bin/true, warm image cache, one box per run. The x86_64 table below was measured on a
20-core / 28-thread, Linux 6.17, NVMe, systemd-user desktop (exact per-runtime commands in
BENCHMARKS.md). kern's figure is informally reproduced on the machine the hero and
demo numbers come from, an Intel i7-14700KF, Linux 7.0, same class, where a P-core-pinned
/bin/true box lands in the low-single-digit-ms range (an E-core is slightly higher). Your
numbers vary with hardware and load; board rows are from on-device runs.

host kernel kern bubblewrap crun runc podman docker
x86_64 desktop v6.17 1.9 ms 2.6 ms 5.2 ms 12.2 ms 155 ms 308 ms
Jetson Orin Nano v5.15-tegra 3.6 ms 5.6 ms 32 ms 472 ms
Raspberry Pi 5 v6.6-rpi 2.1 ms
Arduino UNO Q v6.16 Android 9.9 ms 14.9 ms 76 ms 858 ms

✗ = not installed on the boards tested. On the Pi 5, kern is the only runtime
present at all
: one ~1.6 MB static binary just works where the others are each a setup step (Docker
alone is a ~186 MB daemon stack).

kern is the fastest sandbox here at ~1.9 ms (ahead of bubblewrap). Its own box setup is ~1 ms
(the KERN_TIMING phases: unshare + overlay + /dev + pivot + seccomp, each sub-ms); the rest is
process start + teardown. Adding a hard cgroup cap costs about +1 ms when the cgroup is already
delegated (a systemd user session writes memory.max directly, ~2.7 ms total, this is the common
desktop case and what the demo shows). Where kern must create the delegated scope itself it shells out
to systemd-run, which brings it to ~5.5 ms, but most of that is external systemd-run + D-Bus
scope creation, not kern
(systemd-run --user --scope -- true alone is ~4 ms); KERN_NO_SCOPE opts
out (back to ~1.9 ms, best-effort in-process cgroup). The
top tier is all within a few ms: nobody wins single-shot latency outright. The real gap is to the
engines: ~80-160× faster than podman (~155 ms) / Docker (~308 ms), which round-trip a daemon every run, yet
kern alone ships a full daemonless container UX in ~1.6 MB. Beyond one start: ~500 boxes/s, ~7 MB
RSS/box, 0 resident (Docker keeps ~186 MB resident before you run anything).

† On the Arduino's Android kernel an overlayfs mount is ~31 ms (a kernel quirk, sub-ms elsewhere),
so the default overlay box is ~33 ms there; --bind-rootfs starts in 9.9 ms, ahead of bubblewrap.

Reproduce with examples/benchmark.py (auto-detects the runtimes you
have). kern does less than Docker (no overlay networks yet, see Roadmap); this compares
the run path.

Real-world examples

Runnable, live-verified scripts in examples/:

Scenario Example
A guided tour: a tool, your code, resource caps, untrusted code, a service showcase.sh
Try to break out: an adversarial isolation battery + 50 boxes at once hardening.sh
Safely vet an untrusted curl … sh install script (no net, no host access) safe-install-script.sh
Per-job pipeline: read-only input → isolated processing → output data-pipeline.sh
Build/test a repo in a clean box (laptop or on-device) ci-in-a-box.sh
Many isolated services on a small board (few MB vs a 186 MB daemon) edge-many-services.sh
Head-to-head timing: kern vs docker run compare-vs-docker.sh

…plus governed runs, port-published services, compose stacks and more, see
examples/README.md.

Requirements & limitations

kern trades breadth for a small, honest core. What it needs, and what it deliberately does not do:

Requires:

Deliberately not here:

Project status

0.6.14. Everything in Features works today and is tested (490 Rust, 61 Python and 50
Node tests; clippy-clean, cargo-deny-clean, adversarially reviewed slice by slice); the isolation is
real. kern trades Docker's breadth (overlay networks, a plugin ecosystem) for a small, fast core that
starts in ~2 ms from one ~1.6 MB binary. Versioned under semver: each release is the official
build of that version, and pre-1.0 means only that the CLI and config surface can still change between
minor versions, always called out in CHANGELOG.md.

Recently: an explicit-backend resource-profile schema; kern exec joins the box's cgroup so an
exec'd command inherits its --memory/--pids caps (with an honest warning where a rootless per-box
scope can't be joined); a warm kernel and an MCP server (kern-mcp) for the Python/Node
bindings; kern commit warm-start snapshots; an --egress-allow allowlist and --landlock-rw; and
daemonless image build / tag / push with a Dockerfile/compose parser checked against a
real docker build. Full per-release detail in CHANGELOG.md.

Deliberately not here yet: the headline GPU slices (on the Roadmap) and Docker-style
overlay networking.

Roadmap

kern starts as a small, fast sandbox/OCI runtime and grows deliberately: the resources it governs are
driven by what proves useful. These are directions under consideration, not commitments or dates,
and some may never ship if they would change what kern is. Recently shipped work is under
Project status, not here.

See ARCHITECTURE.md for the design.

Security

kern isolates with Linux namespaces + seccomp + a self-pivoted root (a copy-on-write overlay by
default: the image stays immutable, writes land in a private upper discarded on exit, and --read-only
locks the whole root), a kernel-level boundary, deny-by-default on devices, with the host's sysfs/procfs
masked, and an opt-in Landlock (LSM) layer on top (plus an experimental egress allowlist). That's
strong for first-party and noisy-neighbour workloads. For adversarial, multi-tenant untrusted code where you want a
hardware-virtualization boundary, a microVM/VM adds a layer kern doesn't: a deliberate trade for
~2 ms starts and a ~1.6 MB footprint.

The full threat model, per-feature notes, and the honest "kern vs a microVM, when to use what"
guidance live in SECURITY.md. Found a vulnerability? Report it privately via GitHub
Security Advisories ("Report a vulnerability" on the repo); please don't open a public issue.

Contributing

Issues and PRs welcome, see CONTRIBUTING.md. Contributions are covered by a
lightweight CLA; the project follows a Code of Conduct. Security
reports: follow SECURITY.md (don't open a public issue).

License

Apache-2.0, permissive, with an explicit patent grant. See NOTICE. The dependency
tree is deliberately tiny (libc only on the Rust side; standard-library-only on the Python side) and
copyleft-free; cargo deny check licenses is clean.

Trademark. The code is free under Apache-2.0: fork it, embed it, run it, no strings.
But "kern" is a trademark of the project: please don't use the name for a fork, a
modified build, or a competing product/service without permission, see TRADEMARK.md.
Open code, protected name, the same split Rust and Firefox use.