hull
Container Runtime

~3 MB. No daemon.
7 isolation layers.

Hull is a daemonless Linux container runtime written in Zig. A single static-musl binary — no dockerd, no containerd, no shim. Each hull run forks the workload and exits. Namespaces, cgroups v2, seccomp-bpf, Landlock, pivot_root, veth bridge — all in one call.

linux/aarch64·install.sh (auto-detect)·checksums·No daemon. No root required.
This page is served by a hull container — Next.js static export inside busybox httpd, isolated with 7 security layers, orchestrated by Mentat.
~3 MB
static-musl binary
7
isolation layers
0
daemons
10
CLI commands
6
base images
6
seccomp profiles

Downloads

One binary.
No installer. No service.

Drop the binary anywhere on PATH and run. No background service, no systemd unit, no privileged group required for --rootless.

PlatformAssetDownload
Linux · x86_64hull-x86_64Download
Linux · aarch64hull-aarch64Get
Auto-detect (install.sh)install.shGet
ChecksumsSHA256SUMSGet
Signature · x86_64hull-x86_64.minisigGet
Signature · aarch64hull-aarch64.minisigGet
Public keyminisign.pubGet
Static-musl binaries, no glibc dependency. Source on GitHub: github.com/rvielma/hull.
Verify the signature

Every binary published here is signed with minisign. Two checks: SHA256 to detect a corrupted download, and the signature to confirm the bits came from the Hull project, not a MITM.

# 1. Download the binary, signature, checksum file, and public key
curl -fsSL https://hull.getmentat.run/releases/hull-x86_64 -o hull
curl -fsSL https://hull.getmentat.run/releases/hull-x86_64.minisig -o hull-x86_64.minisig
curl -fsSL https://hull.getmentat.run/releases/SHA256SUMS -o SHA256SUMS

# 2. Compare SHA256
sha256sum -c SHA256SUMS --ignore-missing
# hull-x86_64: OK

# 3. Verify the minisign signature against the project public key
minisign -Vm hull -P RWQLMHs2c5iiqJf3r7KPdMKdIOgDalrXqzEKI7ijt0DROB2ywop8pbxr
# Signature and comment signature verified

# Only after both checks pass:
chmod +x hull && sudo mv hull /usr/local/bin/

Public key: RWQLMHs2c5iiqJf3r7KPdMKdIOgDalrXqzEKI7ijt0DROB2ywop8pbxr (also at /releases/minisign.pub). If minisign -V fails, the binary is not what the Hull project signed — do not run it.


Validated stacks

Real apps, default profile.
No seccomp loosening required.

Each row was validated end-to-end on a clean GCP VM (Ubuntu 24.04) without Mentat, without Docker: download the binary, hull pull, hull run, hit the service, response received. No syscalls disabled, no privileged container — all 7 isolation layers active.

StackWhat was testedProfileStatus
Static frontendVue, Vite, React SPA, Next.js export — same pattern as the page you're reading (busybox httpd + dist/)default
Next.js 16 SSRforce-dynamic page + /api/hello route, per-request server rendering, distinct timestamps verifieddefault
.NET 8 ASP.NETself-contained publish (PublishSingleFile + InvariantGlobalization), Kestrel on 0.0.0.0:5050, /weatherforecast returns JSONdefault
PostgreSQL 16Hull-native image (debootstrap, no docker-entrypoint, no gosu suid), SELECT/INSERT/CREATE TABLE via psqldefault
Redis 7Hull-native image, PING/SET/GET/INCR/DBSIZE via redis-clidefault
Hull-native images
Built with debootstrap + apt + setpriv. No docker-entrypoint.sh, no gosu suid. UID switching via Linux capabilities + setresuid (post-docker pattern).
Default seccomp covers
Privilege drop (capset, setres*), SysV IPC (postgres shared buffers), vectored I/O (preadv/pwritev), io_uring (Node libuv), NUMA queries, modern *at variants, scheduler policy. ptrace + bpf still blocked.
Rootless on Ubuntu 24.04
Works with a one-line AppArmor profile (flags=(unconfined) {userns,}).hull pull sets ownership to $SUDO_USER so subsequent --rootless runs can pivot_root.

Quickstart

From zero to running
in three commands.

1. Install
curl -fsSL https://hull.getmentat.run/install.sh | sh

# or manually:
curl -fsSL https://hull.getmentat.run/releases/hull-x86_64 -o /usr/local/bin/hull && chmod +x /usr/local/bin/hull
hull version
2. Write a manifest (myapp.json)
{
  "name": "myapp",
  "rootfs": "/var/lib/hull/rootfs/myapp",
  "argv": ["/app/server", "--port", "8080"],
  "env": ["PORT=8080", "NODE_ENV=production"],
  "profile": "default",
  "network": "bridge",
  "hostname": "myapp",
  "limits": { "memory_mb": 256, "cpu": 1.0, "pids": 128 }
}
3. Run it
hull run myapp.json

hull ps
# NAME     PID       UPTIME      ARGV
# myapp    42351     12          /app/server

hull exec myapp /bin/hostname
# myapp

hull inspect myapp
# Container: myapp
#   status:    running
#   pid:       42351
#   Cgroup:    memory 48/256M, cpu 1.0, pids 3/128
#   Namespaces: pid:[4026560304] net:[4026560239] mnt:[...] ...

hull stop myapp

CLI Reference

Ten commands.
Zero configuration files.

hull run [--rootless] <manifest>Start a container from a JSON manifest
hull psList running containers with PID, uptime, argv
hull stop <name>Graceful shutdown (SIGTERM)
hull kill <name>Immediate kill (SIGKILL)
hull exec <name> <cmd...>Run a command inside a running container
hull logs <name>Print captured stdout/stderr
hull inspect <name>Show cgroup, namespaces, mount points
hull pull <name>:<tag>Download image from registry
hull push <rootfs> [--name <n>] [--tag <t>]Upload rootfs to registry
hull versionPrint version string
Exit codes: 0 success, 1 usage, 2 runtime, 3 manifest, 127 exec failed. On seccomp violation (SIGSYS), hull reads dmesg and prints the blocked syscall number.

Security Model

Seven layers.
Each independent.

Failure of one layer does not disable the others. A workload that escapes seccomp still hits Landlock. A workload that bypasses Landlock still sees an isolated PID tree and empty /proc.

1
User namespace (NEWUSER)
Process thinks it's uid 0; host sees an unprivileged uid. Enabled by --rootless. Parent writes uid_map/gid_map via fork-pipe dance.
2
PID namespace (NEWPID)
Isolated PID tree. Workload is PID 1. Cannot see or signal any host process. Bridge mode uses double-fork to preserve isolation.
3
Network namespace (NEWNET)
Three modes: none (loopback only — no route out), host (shared stack), bridge (veth pair + hull0 bridge + nftables masquerade).
4
Mount namespace (NEWNS) + pivot_root
pivot_root (not chroot) into dedicated rootfs. Host filesystem completely invisible. /proc remounted for NEWPID view.
5
cgroups v2
Hard limits on CPU (cpu.max), memory (memory.max), and PIDs (pids.max). Kernel-enforced. Container cannot fork-bomb or exhaust host RAM.
6
Landlock LSM
Filesystem allowlist: rootfs read+exec, /tmp read+write, everything else denied. Even uid 0 inside the container cannot bypass it. Graceful skip on kernels < 5.13.
7
seccomp-bpf
Syscall allowlist per workload profile. KILL_PROCESS on violation (not Docker's EPERM). Installed right before execve. On kill, hull reads dmesg and reports the blocked syscall.

Seccomp Profiles

Curated syscall allowlists
per workload type.

default122 syscalls
Rust musl, Zig, Go, shell scripts
I/O + net + process management + shell pipelines. The baseline for single-binary servers.
webappdefault + 3 syscalls
Node.js 22+ servers, Next.js SSR, modern userspace runtimes
Default plus the io_uring trio (io_uring_setup/enter/register). Opt-in because of recent CVEs — bulk workloads stay on the tighter default.
node32 syscalls
Node.js, Deno, Bun (libuv)
Tight: epoll, eventfd, signalfd, timerfd — the libuv core. No legacy syscalls, no file creation beyond openat.
dotnet36 syscalls
.NET 8/9 (CoreCLR, NativeAOT)
select/pselect6 + signalfd4 + memfd_create (JIT code staging) + tgkill (pthread signals). Minimal.
beam177 syscalls
Elixir, Erlang, Phoenix (BEAM VM)
Default + 55 extras: timerfd, signalfd, inotify, memfd_create, epoll_create, legacy file ops (mkdir/unlink/chmod/chown).
javapermissive syscalls
OpenJDK 8/11/17/21, Mirth Connect, install4j-packaged JVM apps
Curated full table — JVM workloads exercise a wide surface (signal-coordinated GC, JNI, async I/O, JIT, hsperfdata IPC). Per-syscall iteration is O(N) failures, so this profile permits the table; seccomp still blocks anything outside it.

Blocked in all profiles: ptrace, process_vm_readv, bpf, add_key/keyctl, userfaultfd, kexec_load, init_module. x86_64 and aarch64 supported.


Image Registry

Pull. Run.
No Docker required.

Hull images are flat rootfs tarballs stored in an S3-compatible backend (OxideStore). No layers, no manifest lists, no platform indices. One tarball per image, one command to pull.

Pull and run
# Pull an image from the hull registry
hull pull hull.getmentat.run/node:22-slim
hull pull hull.getmentat.run/postgres:16
hull pull hull.getmentat.run/redis:7

# Run it
hull run --manifest <(cat <<EOF
{
  "name": "mydb",
  "rootfs": "/var/lib/hull/rootfs/postgres-16",
  "argv": ["postgres", "-D", "/var/lib/postgresql/data"],
  "profile": "default",
  "network": "bridge",
  "limits": { "memory_mb": 512 }
}
EOF
)
Flat rootfs
No layers, no dedup, no graph driver. A tarball is extracted once and cached. Rebuilds are fast because there's nothing to diff.
OxideStore backend
S3-compatible object storage running on Mentat. Images stored locally — no external registry dependency. Push/pull over HTTPS.
Docker import
Convert existing Docker/OCI images to hull rootfs. Flatten layers, strip metadata, generate manifest template. Use what you already have.

Networking

Three network modes.

"network": "none"
Loopback only. No route to the internet, no DNS. For pure-compute workloads and build sandboxes where network access is a liability.
"network": "host"
Share the host's network stack. Full internet, host loopback, all ports. Zero overhead. For services that bind to host ports directly.
"network": "bridge"
veth pair per container attached to hull0 (10.88.0.0/24). Auto IP allocation via lease files. nftables masquerade + iptables FORWARD. Containers reach internet via host's default route.
Bridge mode verified output
$ nsenter -t <pid> -n ip -br addr
lo               UNKNOWN        127.0.0.1/8
eth0@if36548     UP             10.88.0.2/24

$ nsenter -t <pid> -n ping -c 3 8.8.8.8
3 packets transmitted, 3 received, 0% packet loss
rtt min/avg/max = 0.256/0.389/0.523 ms

Why the name

Hull means
two things in English.

And both fit what the runtime does.

Noun — a ship's hull
The outer structure that holds the cargo, protects against the sea, and separates inside from outside.
The ship doesn't know what it carries. The cargo doesn't know it's on a ship.
hull → contains → protects → isolates
Verb — to hull
To strip away the husk or pod from grains, nuts, and seeds.
The act of separating the inside from the outside — the same boundary the runtime draws between a process and its host.
Why it was the right name
Dockerstevedoremoves containers
Hullship's hullCONTAINS and PROTECTS them

A process inside a hull container doesn't know it is contained. The host doesn't know what runs inside beyond what hull explicitly allows. That's exactly the metaphor of a ship's hull — a hermetic separation between interior and exterior. And practically: four letters, easy to say in any language, the same phonetic energy as Docker, no ambiguity.


Comparison

Hull vs Docker vs runc

HullDockerrunc
Binary size~3 MB~200 MB (dockerd+containerd+runc+shim)~10 MB
Daemon requiredNoYes (dockerd + containerd)No (but needs caller)
Root requiredNo (--rootless)Yes (rootless experimental)No (rootless)
Registry / pullhull pull redis:7 (built-in)docker pull (Docker Hub)No (needs external tool)
Push imageshull push /rootfs --name myappdocker pushNo
Exec into containerhull exec <name> <cmd>docker execNo (needs nsenter manually)
Replicas + LBmt scale myapp 3 (auto round-robin)docker swarm / composeNo
seccomp defaultKILL_PROCESS + audit (prints blocked syscall)EPERM (silent fail)KILL_PROCESS (if configured)
Landlock LSMYes (ABI v1, fs allowlist)NoNo
Workload profiles6 curated (default/webapp/node/dotnet/beam/java)1 genericCustom JSON
Networkingnone / host / bridge (veth + nftables)bridge / host / overlay / macvlanhost only (needs CNI)
PID isolation (bridge)Yes (double-fork NEWPID)YesYes
Image formatFlat rootfs tarball (hull pull/push)OCI layersOCI bundle
Base images6 (node, postgres, redis, python, busybox)Docker Hub (millions)None
State storageJSON files on disk (~/.hull/state/)containerd + ttrpcJSON files (state.json)
DependenciesNone (static Zig binary)containerd, runc, snapshotterlibseccomp (optional)

Manifest Spec

One JSON file.
Three required fields.

Required

namestringContainer name (1-64 chars)
rootfsstringPath to rootfs dir or .tar.gz
argvstring[]Command + arguments

Optional (all have defaults)

envstring[]Environment variables
profilestringSeccomp profile (default)
networkstringnone / host / bridge
hostnamestringContainer hostname
cwdstringWorking directory (/)
limits.*numbermemory_mb, cpu, pids
mounts[]objectBind mounts (host, container, readonly)
bridge.*objectname, subnet, ip, mtu