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 | iexthen kern box dev --image alpine -- sh
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 | 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.
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.
- Development: a fast, daemonless Docker for your inner loop. Spin up a service, a database, or a
clean per-language build box in ~2 ms, run it, throw it away, your host stays untouched and no
dockerdsits in the background eating RAM (familiar-commands.sh · database-box.sh). - AI agents: run each model-generated tool call in a fresh, network-off box, sandbox faults come back as data, not crashes (warm-kernel.py · kern-mcp for Claude Desktop / Cursor · agent-tool-runner.py).
- CI: run each step in a capped, daemonless box, no Docker-in-Docker (ci-in-a-box.sh).
- Edge / ARM: one 1.6 MB binary on a Pi 5 / Jetson where a Docker daemon does not fit (edge-many-services.sh).
- Device access / IoT: hand a workload exactly the device nodes you name (an I2C or SPI bus, a GPIO chip, a sensor) and nothing else, deny-by-default. The closest thing is CDI (the Container Device Interface), but that's a JSON spec you register with a supporting engine; kern's is a line of TOML, engine included, and it works on a bare
kern runtoo (device-isolation.sh). - Untrusted or customer code: execute it isolated and resource-capped, on your own machine, no cloud (code-interpreter.py).
- Serverless / functions: a fresh, throwaway box per request, so one call's crash or timeout stays contained to its own box (per-request-workers.py).
- Data & batch: ETL, scraping, per-file fan-out, each input in its own capped box (data-pipeline.sh · batch-process.sh).
- Build and run OCI images:
kern build/kern box, speaks Docker formats, no daemon.
- ⚡ Daemonless & tiny. No
dockerd-style service. A ~1.6 MB static binary, one Rust dependency (libc); it shells out to the system'scurl/taronly to pull images (running a box needs neither). Cold start ~2 ms vs ~308 ms fordocker run; ~7 MB RSS per box vs an always-on ~186 MB daemon (dockerd+containerd).kern psreads state straight from the kernel. - 👤 Rootless by default. Unprivileged user namespaces: your uid maps to root inside the box,
and only there. Single-uid is the default and is
libc-pure (no helper, smallest id surface);--uid-rangeopts into a full sub-id range (apt,www-data-style drops) via the standardnewuidmap+/etc/subuid. kern says plainly that path is not helper-free. No host privilege is gained either way. - 🧱 Correct by construction. The mount sequence is a typestate: remounting the root read-only
before pivoting into it doesn't compile, so a class of sandbox-escape bug is unrepresentable, not
just untested.
--planprints the exact isolation sequence without running anything. - 🔍 Honest about its boundaries. Filesystem / process / namespace isolation is a real kernel boundary, the right tool for your own or semi-trusted code (CI, dev, edge, your agents' code). For actively hostile multi-tenant code, reach for a microVM. SECURITY.md says exactly when to use which, and marks every guarantee that is cooperative or opt-in.
| 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.
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).
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 loopThree kinds ship today (a GPU slice is on the Roadmap):
vcpu: a hard CPU + memory slice (cgroupcpu.max/memory.max).vdisk: a size-capped scratch at/vdisk/<name>(rootless: a RAM-backed tmpfs that counts against the memory cap, so an 8 GB scratch uses 8 GB of RAM; privileged: ext4-on-loop on real disk).vgpio: exactly the named GPIO / I²C / SPI device nodes, nothing else (a fresh/dev+ an fd-pinned bind). A reusable, deny-by-default device set attached by name is closest to CDI (the Container Device Interface), but CDI is a JSON spec you author and register with an engine that supports it (Podman, containerd, Docker 25+); kern's is one line of TOML, the engine is included, it stays deny-by-default, and it attaches to a barekern runwith no box at all. Docker's own--deviceis per-run and unnamed; systemd'sDeviceAllowis per-unit, not a portable preset.
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 refusedNo daemon, no root: one ~1.6 MB binary.
kern box try --image alpine -- shDeny-by-default: only the peripheral you name crosses the boundary.
kern box iot --image alpine vgpio:sensor -- ./read.py # only /dev/i2c-1 crosses inServerless-style, on your own machine: one throwaway box per call.
echo "$payload" | kern box fn --image python -- handler.pyThe 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'kern build -t app:1 . && kern tag app:1 registry.example/app:1 && kern push registry.example/app:1Daemonless, rootless, and complete: the full container UX plus resource slices, in one binary.
- Run anything, isolated: OCI images from any registry (v2 auth, multi-arch, gzip + zstd) or a
--rootfs; CoW overlay (image immutable, scratch discarded) or--read-only;-itTTY;--initPID-1 reaper. - Governed slices: hard cgroup v2 caps on any
box/run:--memory·--cpus·--cpuset-cpus·--memory-swap-max·--pids-limit·--io-weight·--nice.kern runis the governor with no sandbox. - Data & devices:
-vvolumes (symlink-safe) · named volumes with a--sizequota · network volumes (nfs/smb/sshfs) ·--secret→/run/secrets(RAM,0400) ·vdisk:scratch ·vgpio:device passthrough (deny-by-default) ·--tmpfs. - Network & identity: isolated by default;
--network hostfor outbound;--egress-allow d1,d2(⚠️ experimental) restricts outbound to an allowlist of domains via a kern-run filtering proxy (an agent canpip installbut can't exfiltrate to an arbitrary domain), with honest known gaps documented in docs/EGRESS.md;-prootless publish (loopback unless you ask); in-box--ssh;--podshared-net pods (--no-outbound);--tun;--user. - Least privilege: 13 dangerous caps always dropped (
--cap-add/--cap-drop); an always-on seccomp denylist (kexec, modules, ptrace, mount API,setns, …) that also kills wrong-arch + x86_64 x32-ABI aliases; opt-in Landlock (LSM) write-allowlist (--landlock-rw <path>): the box root is read+exec and writes are confined to the paths you name, a kernel-enforced second boundary the workload can't lift. - Lifecycle, no daemon:
--restart+--health-cmd;cp/pause/attach/exec/rename/update/wait/diff/events;ps/top/stats/logs/inspect/prune/gc/history/recover;compose(readsdocker-compose.ymltoo); reusable[[vcpu]]/[[vgpio]]/[[vdisk]]profiles;kern doctor.
Every flag & command, grouped
Run anything, isolated
- OCI images, any registry:
--image alpinepulls (registry v2, multi-arch → your arch, gzip and zstd layers) and runs. Docker Hub, GHCR, GitLab, quay, Harbor, self-hosted, via the standardWWW-Authenticatechallenge (Bearer or Basic).kern loginstores creds0600and passes them tocurloff-argv. Or bring a rootfs with--rootfs. Pull a foreign arch withkern pull --platform os/arch, then run it. - Governed slices:
kern runcaps a command with no sandbox;--memory/--cpus/--cpuset-cpus(pin) /--memory-swap-max/--pids-limit/--io-weight/--niceset hard cgroup v2 caps on anyboxorrun(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-onlyfor a read-only root. Interactive TTY with-it(raw mode, resize-aware). --init: a built-in PID-1 reaper (no zombies, forwards SIGTERM) without bundlingtini.
Data & devices across the boundary
- Volumes, full:
-v src:dst[:ro](symlink-safe) · named volumes (kern volumeCRUD, with a per-volume--sizequota) · network volumes (nfs:///smb:///sshfs://) mounted rootless via FUSE/GVFS. - Secrets:
--secret NAME=value/NAME=-(stdin) /SRC[:NAME](file) →/run/secrets/NAME(mode0400) 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 freshnosuid,nodevtmpfs (refused over hardened mounts).
Networking & identity
- Modes: isolated loopback-only by default (
--network none);--network host(=--net) for outbound;--hostname;--tunexposes/dev/net/tunfor WireGuard / userspace VPNs. - Port publishing:
-p [ip:]host:boxfrom a rootless forwarder; binds127.0.0.1by default,0.0.0.0only if you ask. - In-box SSH:
--ssh 2222runs a throwawaysshd(auto keypair or--ssh-key), published. - Pods:
kern pod create+--pod <name>: a shared-network pod where boxes reach each other by name (--no-outboundto 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/ALLdrops more,--cap-add CAPkeeps 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 nestedkern box(docker-in-docker): re-allows exactlyunshare/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:
--restartsupervises a detached box;--health-cmd+--health-interval/-retries/-start-period/-timeout/-actionprobe it;kern psshows 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:
-ddetached;ps/top(TUI) /stats/logs/inspect/stop/kill/killall/prune/gc/history/recover. - Compose:
kern compose stack.toml(or adocker-compose.yml) brings up a multi-box stack in dependency order;kern up/downfor the file in this dir. - Diagnostics:
kern doctor(will boxes run here?),info,bench, shellcompletions. - Resource profiles: reusable
[[vcpu]]/[[vgpio]]/[[vdisk]]in~/.config/kern/kern.toml, attached by prefix (kern run vcpu:heavy vgpio:leds -- ./train.sh); managed withkern 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.
🐧 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 | shServed 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 | iexkern 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 -- shNo 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 --releaseA 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 -- shHard 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 -- makeDetached, 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 -fkern 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)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'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)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 imageWarm 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.)
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_msPython, 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).
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.
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 |
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.
✅ Use kern when you want:
- a fast, daemonless sandbox for your own or agent/LLM-generated code (fresh box per call, embeddable from Rust/Python);
- CI / build boxes, or a throwaway dev environment, without a background daemon;
- containers on edge / ARM boards where a Docker daemon is too heavy or absent (Pi, Jetson, Android-kernel);
- resource slices beyond containers:
vcpu:/vdisk:/vgpio:(GPU on the roadmap).
🔀 Reach for something else when you need:
- a hard boundary against actively hostile multi-tenant code → a microVM (Firecracker) or gVisor. kern's FS/PID/namespace/cgroup isolation is a real kernel boundary for your own or semi-trusted code, not a VM;
- the Docker Engine API / Docker Desktop workflow, or a true CLI drop-in → Docker / Podman;
- Kubernetes CRI integration → containerd / CRI-O;
- a low-level OCI runtime to slot under containerd/podman (the runc layer) → crun, youki (also Rust), or runc. kern isn't a runc-replacement; it's the whole daemonless UX (pull, build, run, compose) in one binary, not a runtime another engine drives.
kern states every boundary that is cooperative or opt-in plainly in SECURITY.md; being honest about the edges is the point.
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.
- Namespaces.
unshareinto a fresh user + PID + UTS + IPC namespace (and, by default, an isolated loopback-only net namespace;--netshares the host's, opt-in, flagged in the status panel). A single-uid map makes your uid root inside the box only;--uid-rangeopts into a full sub-id range. - Root filesystem. An overlay by default (image = read-only lower, a private upper takes
writes);
--read-onlyremounts 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-rootfsswaps the overlay for a direct bind: faster on a slow overlayfs, at the cost of a mutable shared source.) - Devices, volumes & secrets. A fresh
/devwith the safe nodes (+ /dev/net/tunon--tun);-vvolumes 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. - Lockdown. A clean env (no host secrets leak in), capabilities stripped to least-privilege, an
optional
--userdrop, an always-on seccomp denylist (incl. wrong-arch + x32), and cgroup caps: hardMemoryMax/CPUQuota/TasksMaxwhen 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.
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.
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.
kern trades breadth for a small, honest core. What it needs, and what it deliberately does not do:
Requires:
- A Linux kernel with unprivileged user namespaces + cgroup v2. On Windows it runs under WSL2; there is no native macOS/Windows port (Roadmap).
- Hard
--memory/--cpus/--pidscaps need a delegated cgroup (a systemd user manager, or root); without one they degrade to best-effort and kern says so. Microsoft's default WSL2 kernel and a stock Raspberry Pi OS don't delegate thememorycontroller, so--memoryis accepted-but-unenforced there (same as Docker/Podman) until you enable it: on WSL, addcgroup_enable=memory cgroup_memory=1tokernelCommandLineunder[wsl2]in%UserProfile%\.wslconfig, thenwsl --shutdown; on Raspberry Pi OS, addcgroup_enable=memory cgroup_memory=1to/boot/firmware/cmdline.txt, then reboot. newuidmap+/etc/subuidfor a full uid range (--uid-range,--ssh); a single-uid box works without them.
Deliberately not here:
- Not a microVM, not for hostile multi-tenancy. kern is a kernel-boundary sandbox (namespaces + an always-on seccomp denylist) for your own or semi-trusted code. A kernel vulnerability is not contained; for actively hostile tenants reach for a microVM (Firecracker) or gVisor. The threat model says so plainly.
- No overlay / software-defined networking (a box gets an isolated netns, or the host's; a pod shares one) and no Docker plugin ecosystem.
kern execcaps are inherited only where kern can join the box's cgroup (root, or a delegatedkern.slice); on a rootless per-box-scope host the exec'd command runs outside the box's--memory/--pids(namespaces + seccomp still isolate it), and kern warns.- GPU slices are on the Roadmap, not shipped.
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.
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.
- GPU slices. A workload gets a slice of a GPU, not the whole device. It lands incrementally,
each stage useful on its own and each opt-in (
--no-gpustays the default): first safe access + visibility (device passthrough, driver-gated, sysfs/procfs masked; per-box VRAM + utilisation inkern stats), then a cooperative per-box VRAM cap via a userspace driver shim (NVIDIA/CUDA first, honest trust model: for first-party / noisy-neighbour isolation, not a hard boundary against a hostile tenant), then time-sliced compute + AMD (HIP) / Vulkan. A cross-vendor GPU merge pool stays an optional plugin, not core. - More governed resources. The same profile model could extend to other cgroup or kernel-real resources (I/O bandwidth, network shaping) as they prove useful.
- Snapshot / warm-start (CRIU). Same-host checkpoint and restore of a warm box for subsecond restarts. Feasible but gated: rootless CRIU needs a capability and suspending the seccomp filter, so it would be an explicit opt-in mode, not the default, and only for same-host, non-GPU boxes. Not committed.
- macOS. There is no native port: a daemonless kernel + cgroup sandbox has no macOS equivalent, so a native macOS kern is a non-goal. The only path considered is a thin shim driving a Linux VM (the same shape as WSL2 on Windows), with the same honest caveat, the win would be "no Docker Desktop", not "no VM". Not committed.
- A microVM (
--vm) mode. An obvious question, given the threat model: to run actively hostile code. It would be a different tool with a different threat model: kern today is a kernel-boundary sandbox, not a microVM, and says so plainly. If it ever shipped it would be an explicit, separate mode, never a silent change to what kern is. Not committed. - 1.0, freeze: CLI + config under semver, threat model + architecture finalised.
See ARCHITECTURE.md for the design.
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.
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).
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.