# Show HN: Kern – container and resource runtime in a 1.5 MB binary, no daemon

> Source: <https://github.com/getkern/kern>
> Published: 2026-08-24 18:24:59+00:00

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

**A real, kernel-enforced container in ~3.5 ms, out of one 1.52 MB binary with no daemon.**

**0 RAM at rest** · no daemon, no socket, nothing to start · one static binary, `libc`

its only Rust dependency

```
# install the release binary (static, 1.52 MB, checksum-verified by the script)
curl -fsSL https://raw.githubusercontent.com/getkern/kern/main/install.sh | sh

# a throwaway shell in a real OCI image: rootless, kernel-enforced, a few ms
kern box dev --image alpine -it -- sh
```

No native Windows: use WSL2. Install.

**One binary that manages resources, of which isolation is the first.** That is why there is no
single row for kern in a comparison table: it is a container runtime, a sandbox, a resource slicer
and a stack runner at once, in 1.52 MB with no daemon.

**A real container.** Real OCI images:`pull`

,`build`

from a Dockerfile,`commit`

,`push`

,`save`

/`load`

. A box from an image starts in ~3.5 ms.**A sandbox, always rootless.** User, PID, mount, network, UTS and IPC namespaces, an overlay or read-only root pivoted in, a deny-by-default seccomp allowlist and cgroup v2 limits. One flag,`--security-profile untrusted`

, is the whole hardened bundle.**Resource profiles, not just isolation.** CPU (`vcpu:`

), memory, disk (`vdisk:`

) and devices (`vgpio:`

), declared once in a`kern.toml`

and attached by name.`kern run`

applies the same caps to a process on the host, with no sandbox at all.[docs/RESOURCES.md](/getkern/kern/blob/main/docs/RESOURCES.md)**Stacks, in kern's own format or in Docker's.**`kern compose <file> up`

takes a`kern-compose.toml`

(`[box.NAME]`

tables, with the resource profiles above) or the`docker-compose.yml`

you already have, read as written. One stack to one pod, services reaching each other by name.**The tools around them.**`ps`

,`logs`

,`exec`

,`stats`

,`inspect`

,`wait`

,`top`

(a live TUI),`doctor`

, plus a Python and Node SDK and an MCP server for agents.

Its entire Rust dependency tree is `libc`

: JSON and OCI manifests are parsed by hand, and `pull`

shells out to the `curl`

and `tar`

already on the machine rather than linking a TLS stack. (1.52 MB
is the size-optimized release build; a plain `cargo install`

from source is 1.91 MB.)

-
**Not a hypervisor.** The boundary is the Linux kernel, so a kernel privilege-escalation bug is an escape. Docker and Podman share that condition, which is why gVisor and Firecracker exist.Read with the tagline, that is one line seen from both sides: untrusted and AI-generated code is what kern is FOR, because you chose to run it and own the blast radius (agent tool-calls, CI jobs, build steps, code cells). What it is not for is hostile code from strangers, multi-tenant, on a kernel you serve other tenants from. kern does start rootless always, where Docker's is opt-in.

-
**Not free of the userns trade.** Its isolation is built on an unprivileged user namespace, a fertile source of kernel LPE bugs.[SECURITY.md](/getkern/kern/blob/main/SECURITY.md)states this before any claim. -
**Not a wall around what you mount in.**`-v $HOME:/host`

gives the box your home directory: a mount is a trust decision you make, not a boundary kern enforces.`--net host`

and`--privileged`

are opt-outs by name. (The one path kern refuses to bind is its own runtime registry.) -
**Not a Docker Engine reimplementation.** It speaks Docker's*formats*, not its API: no overlay networks, no plugins, no Swarm. Matrix:[docs/DOCKER-COMPAT.md](/getkern/kern/blob/main/docs/DOCKER-COMPAT.md). -
**Not a Kubernetes runtime.** No CRI. Use containerd or CRI-O. -
**Not shipping GPU slices.** On the[roadmap](/getkern/kern/blob/main/ROADMAP.md), with no GPU code in this edition, so there is nothing here to trust or to attack yet.

What it does not know or does not do yet is in [OPEN_ITEMS.md](/getkern/kern/blob/main/OPEN_ITEMS.md) rather than left for
you to find.

kern needs a Linux kernel with unprivileged user namespaces and cgroup v2. It runs on **Linux, WSL2
and ARM boards** (Raspberry Pi · Jetson · Arduino UNO Q); there is **no native Windows** build, use
WSL2 (kern ships a pre-baked WSL rootfs).

The quickest route is the release binary: one static file, no toolchain, and the script verifies its SHA256 before installing it.

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

It picks `x86_64`

or `aarch64`

for you, installs to `~/.local/bin`

(`/usr/local/bin`

as root, or
`KERN_INSTALL_DIR`

), and refuses to install a download whose checksum does not match. Verifying by
hand instead is two lines:

```
curl -fsSLO https://github.com/getkern/kern/releases/latest/download/kern-x86_64-unknown-linux-musl.tar.gz{,.sha256}
sha256sum -c kern-x86_64-unknown-linux-musl.tar.gz.sha256 && tar xzf kern-x86_64-unknown-linux-musl.tar.gz
```

**From source** is the other route, and the whole dependency tree is one crate (`libc`

), so it is
short: clone, build and install took 36 s on a desktop (i7-14700KF), longer on a small ARM board.

```
# if you do not have Rust yet
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

cargo install --git https://github.com/getkern/kern getkern --locked
```

That puts `kern`

in `~/.cargo/bin`

, which rustup adds to your `PATH`

(open a new shell, or
`source "$HOME/.cargo/env"`

, if `kern`

is not found).

The release also ships an `aarch64`

binary, a Windows `.exe`

shim and a pre-baked WSL rootfs, each
with its own `.sha256`

; the tag is GPG-signed and independently timestamped ([provenance/](/getkern/kern/blob/main/provenance)).

`kern doctor`

tells you whether boxes will run here before you try. Boards, WSL2 and the long form:
[docs/INSTALL.md](/getkern/kern/blob/main/docs/INSTALL.md). Common questions (Docker, bubblewrap, youki, E2B, Windows, the
threat model): [docs/FAQ.md](/getkern/kern/blob/main/docs/FAQ.md).

```
kern box dev --image alpine -it -- sh              # a throwaway shell in a real OCI image
kern run --memory 256M --cpus 0.5 -- ./crunch      # cap a process, no sandbox
kern box svc --image nginx:alpine -d -p 8080:80 \  # a service: published, restarted, health-checked
  --restart --health-cmd 'wget -qO- localhost:80' -- nginx -g 'daemon off;'
kern ps                                            # what is running, with PORTS and HEALTH
kern exec svc -it -- sh                            # shell into it
kern stop svc                                      # its signal, its grace, then the code it exited with
kern top                                           # live TUI: boxes, CPU/RAM, profiles, volumes
kern compose stack.toml up                         # a multi-box stack (examples/) or a compose.yml
kern compose stack.toml down                       # and take it down again
```

Untrusted code, one flag for the bundle:

```
kern box job --image python:3.12-slim --security-profile untrusted --memory 256m \
  -v ./job:/w -- python3 /w/x.py
```

`--security-profile untrusted`

is the seccomp **allowlist** + `--cap-drop ALL`

+ `--read-only`

in one
opt-in flag (spell them out by hand if you prefer); add `--require-limits`

to refuse to start unless the
memory/pids caps are actually enforced. No network unless you ask, dangerous capabilities dropped,
seccomp always on. Ninety runnable examples, each doing one thing: [examples/](/getkern/kern/blob/main/examples).

Every read verb also answers in JSON, so nothing has to parse a table:

```
kern ps --json | jq '.[] | select(.health == "unhealthy") | .name'
kern volume ls --json          # ps · images · stats · inspect · builds · pod ls · config list · diff
```

kern speaks `docker-compose.yml`

. Point it at the stack you already have and `kern compose up`

runs it
with no daemon and no Docker Desktop, the same on Linux, WSL2 and ARM boards.

```
# compose.yaml - a real stack, unchanged
services:
  db:
    image: postgres:alpine
    environment: { POSTGRES_PASSWORD: secret, POSTGRES_DB: app }
  web:
    image: adminer
    ports: ["8080:8080"]
    depends_on: [db]
kern compose compose.yaml up
```

Both official images start, `web`

reaches `db`

by service name, and the port is published to the host.
Warm (images cached) the web tier serves in **~0.3 s**, and the stack costs only what postgres and adminer
actually use (~66 MB here) with **zero daemon** on top, where Docker Desktop is a background VM before your
first container.

Official images that drop to a non-root user (postgres, redis, ...) want `uidmap`

and a `/etc/subuid`

line, and outbound image pulls want `pasta`

; both are one `apt install`

on a dev box, and `kern doctor`

names either if it is missing. This is the local dev loop, not a production orchestrator: no Swarm, no
overlay networks.

Run agent or LLM-generated code from your own program with
, a thin, dependency-free wrapper over the

`kern-sandbox`

`kern`

binary. Every call runs in a fresh isolated box: network off, memory and pid caps, capabilities
dropped, output bounded, and a timeout the binding itself enforces.

```
pip install kern-sandbox        # PyPI   · needs the `kern` binary above, on PATH or $KERN_BIN
npm  install kern-sandbox       # npm    · same
python
from kern_sandbox import run_code

r = run_code("import platform; print(platform.python_version())")
print(r.stdout)          # ran in a fresh box; a timeout / OOM / blocked escape is data on r.fault
```

**Faults are data, not exceptions**: a timeout, OOM-kill or blocked syscall is a field on the result, not a raise. A fresh box per call by default;`Sandbox`

keeps a workspace across calls and a warm`kernel()`

keeps one interpreter for sub-millisecond cells (weaker isolation, by choice).**Rich results without a Jupyter kernel**: the last expression,`display()`

and matplotlib figures come back captured, like a notebook cell.- Ships an
**MCP server**(`kern-mcp`

): a dependency-free stdio server that gives Claude Desktop, Cursor or any MCP client a local code interpreter. Point the client at it:

```
{ "mcpServers": { "kern": { "command": "kern-mcp" } } }
```

Tools: `run_code`

(python/bash/node), `write_file`

, `read_file`

, `list_files`

. Each call is a fresh
network-off box; files persist across calls in a workspace on disk. Setup command, image and the
other options: [bindings/python/README.md](/getkern/kern/blob/main/bindings/python/README.md).

Full API, Python and Node: [bindings/python/README.md](/getkern/kern/blob/main/bindings/python/README.md) ·
[bindings/node/README.md](/getkern/kern/blob/main/bindings/node/README.md).

A slice is declared once in `~/.config/kern/kern.toml`

and attached by name, to a sandboxed box or a
bare process, with the same token.

Three kinds: `vcpu:`

(CPU and memory), `vdisk:`

(a size-capped scratch disk) and `vgpio:`

(device
nodes). Two of them, and the anchors they are carved from:

```
[[cpu]]                     # the host budget a slice is carved from
id    = "cpu:0"
cores = 8.0

[[vcpu]]                    # 1.5 cores and 512 MiB  ->  attach as  vcpu:heavy
name    = "heavy"
backend = "cpu:0"
cpus    = 1.5
memory  = "512m"

[[gpio]]                    # a controller anchor
id = "gpio:0"

[[vgpio]]                   # exactly one device node ->  attach as  vgpio:sensor
name    = "sensor"
backend = "gpio:0"
i2c     = ["/dev/i2c-1"]
kern validate ~/.config/kern/kern.toml       # check it before anything runs
kern box train --image alpine vcpu:heavy vdisk:scratch -- ./train.sh
kern run vcpu:heavy -- ./train.sh            # the same slice, no sandbox
kern box iot --image alpine vgpio:sensor -- ls /dev
```

Profiles compose: several attach to one box, and an explicit flag beats a profile's own value. Every
key is spelled like its CLI flag, so `cpus`

is `--cpus`

and `memory`

is `--memory`

. A backend naming
no declared pool is refused when the config is read, not when the box runs.
[docs/RESOURCES.md](/getkern/kern/blob/main/docs/RESOURCES.md) has the field-by-field schema.

A `vdisk:`

is a RAM-backed tmpfs when kern runs rootless, whatever its backend says, and an
ext4-on-loop image with a real quota when it runs privileged. kern says which one you got, per
profile, rather than letting you assume, and the size cap is enforced either way.

** vgpio: is chip-granular, not per-line.** Asking for

`pins`

binds the whole `/dev/gpiochipN`

, and
that character device exposes every line of that controller. `pins = [17]`

does not restrict the box
to line 17: the kernel has no per-line mount boundary, so the pin list is cooperative metadata rather
than a boundary. Naming a device node, as `i2c`

above does, grants that node and nothing else.| kern | Docker | Podman | |
|---|---|---|---|
| Daemon | no |
yes (`dockerd` + `containerd` ) |
no |
| Rootless | yes, always |
opt-in | yes |
| Cold start, bare box | ~2.3 ms |
~297 ms | ~293 ms |
| Cold start, from an OCI image | ~3.5 ms |
~297 ms | ~293 ms |
| Stop a service (init handles SIGTERM) | ~1.9 ms |
~310 ms | ~380 ms |
| Resident memory, nothing running | 0 |
154 to 160 MB | 0 |
| Footprint | one 1.52 MB binary |
daemon stack | multi-binary install |
| OCI images, pull / build / push | yes | yes | yes |
`docker-compose.yml` |
yes, read as-is | yes | partial |
| Overlay networks, Swarm, CRI | no |
yes | partial |
| GPU | on the roadmap | yes | yes |

Intel i7-14700KF, Linux 7.0.0, the release binary, one script you can run yourself:
`python3 examples/benchmark.py`

. Yours will differ with your CPU, kernel and filesystem.

| kern | bubblewrap | runc | podman | docker | |
|---|---|---|---|---|---|
| Cold start (bare box) | ~2.3 ms |
~2.3 ms | ~18.6 ms | ~293 ms | ~297 ms |
| 200 boxes in parallel | ~0.11 s |
~0.16 s | ~0.35 s | ~44.8 s | ~16.2 s |

Three thousand at once take ~2.2 s, and a live box costs ~0.3 MB of memory.

Two honest notes. **Nobody wins single-shot latency outright**: the floor for `unshare`

+ `exec`

is
1 to 2 ms, so the whole top tier sits inside its own noise, and bubblewrap is a launcher with no
images, caps or lifecycle. The gap that means something is to the *engines*, two orders of magnitude
above.

Method, per-phase breakdown, board numbers and every caveat: ** BENCHMARKS.md**.

Namespaces, a `pivot_root`

, 16 dangerous capabilities dropped before exec, an always-on seccomp
**allowlist** by default (moby's own default filter minus kern's 35 escape syscalls, which stay
hard-killed; a syscall outside the vetted set returns `ENOSYS`

, and the wider denylist is the opt-out
via `KERN_SECCOMP=denylist`

), cgroup v2 limits (`--require-limits`

refuses to start unless they bind),
and a deny-by-default `/dev`

. Where a boundary is cooperative rather than kernel-enforced,
[SECURITY.md](/getkern/kern/blob/main/SECURITY.md) says so and names the bypass.

You do not have to take it on trust: [pentest/](/getkern/kern/blob/main/pentest) holds four adversarial suites that assert
those boundaries against the kernel rather than against kern's own reporting, and they run without a
registry account or a network.

```
sh pentest/run-with-local-registry.sh ./target/release/kern pentest/pentest-ports.sh
```

Report a vulnerability privately via GitHub Security Advisories or [hello@getkern.dev](mailto:hello@getkern.dev).

|

[docs/DOCKER-COMPAT.md](/getkern/kern/blob/main/docs/DOCKER-COMPAT.md)[docs/RESOURCES.md](/getkern/kern/blob/main/docs/RESOURCES.md)·[docs/CONFIG.md](/getkern/kern/blob/main/docs/CONFIG.md)·[docs/STORAGE.md](/getkern/kern/blob/main/docs/STORAGE.md)·[docs/EGRESS.md](/getkern/kern/blob/main/docs/EGRESS.md)`kern.toml`

schema, volumes and egress[docs/THREAT_MODEL.md](/getkern/kern/blob/main/docs/THREAT_MODEL.md)·[SECURITY.md](/getkern/kern/blob/main/SECURITY.md)·[OPEN_ITEMS.md](/getkern/kern/blob/main/OPEN_ITEMS.md)[BENCHMARKS.md](/getkern/kern/blob/main/BENCHMARKS.md)·[EDGE.md](/getkern/kern/blob/main/EDGE.md)[examples/](/getkern/kern/blob/main/examples)·[blog/](/getkern/kern/blob/main/blog)[bindings/python/README.md](/getkern/kern/blob/main/bindings/python/README.md)·[bindings/node/README.md](/getkern/kern/blob/main/bindings/node/README.md)`kern-sandbox`

SDK: embed kern in Python or Node**The core is done. Everything above works today:** 840 Rust, 78 Python and 61 Node tests,
clippy-clean, `cargo-deny`

-clean, on real hardware: Linux, WSL2, Raspberry Pi 5, Jetson Orin Nano,
Arduino UNO Q. **v0.7.0 is the first published release.** The CLI and config surface can still
change, always called out in [CHANGELOG.md](/getkern/kern/blob/main/CHANGELOG.md).

Issues and pull requests are welcome. [CONTRIBUTING.md](/getkern/kern/blob/main/CONTRIBUTING.md) has the workflow and the
gates; contributions are covered by the [CLA](/getkern/kern/blob/main/CLA.md).

Alex, [@realexhub](https://github.com/realexhub). Commits come from
[@getkerndev](https://github.com/getkerndev), the project's commit identity.

**The commits are not signed; the release TAG is.** That is what to verify:
`git verify-tag v0.7.0`

against the key in [provenance/](/getkern/kern/blob/main/provenance), whose fingerprint is in
[SECURITY.md](/getkern/kern/blob/main/SECURITY.md).

Apache-2.0. See [LICENSE](/getkern/kern/blob/main/LICENSE) and [TRADEMARK.md](/getkern/kern/blob/main/TRADEMARK.md).
