~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.
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.
| Platform | Asset | Download |
| Linux · x86_64 | hull-x86_64 | Download |
| Linux · aarch64 | hull-aarch64 | Get |
| Auto-detect (install.sh) | install.sh | Get |
| Checksums | SHA256SUMS | Get |
| Signature · x86_64 | hull-x86_64.minisig | Get |
| Signature · aarch64 | hull-aarch64.minisig | Get |
| Public key | minisign.pub | Get |
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.
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.
| Stack | What was tested | Profile | Status |
| Static frontend | Vue, Vite, React SPA, Next.js export — same pattern as the page you're reading (busybox httpd + dist/) | default | ● |
| Next.js 16 SSR | force-dynamic page + /api/hello route, per-request server rendering, distinct timestamps verified | default | ● |
| .NET 8 ASP.NET | self-contained publish (PublishSingleFile + InvariantGlobalization), Kestrel on 0.0.0.0:5050, /weatherforecast returns JSON | default | ● |
| PostgreSQL 16 | Hull-native image (debootstrap, no docker-entrypoint, no gosu suid), SELECT/INSERT/CREATE TABLE via psql | default | ● |
| Redis 7 | Hull-native image, PING/SET/GET/INCR/DBSIZE via redis-cli | default | ● |
debootstrap + apt + setpriv. No docker-entrypoint.sh, no gosu suid. UID switching via Linux capabilities + setresuid (post-docker pattern).flags=(unconfined) {userns,}).hull pull sets ownership to $SUDO_USER so subsequent --rootless runs can pivot_root.From zero to running
in three commands.
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{
"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 }
}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 myappTen commands.
Zero configuration files.
| hull run [--rootless] <manifest> | Start a container from a JSON manifest |
| hull ps | List 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 version | Print version string |
0 success, 1 usage, 2 runtime, 3 manifest, 127 exec failed. On seccomp violation (SIGSYS), hull reads dmesg and prints the blocked syscall number.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.
Curated syscall allowlists
per workload type.
default122 syscallswebappdefault + 3 syscallsnode32 syscallsdotnet36 syscallsbeam177 syscallsjavapermissive syscallsBlocked in all profiles: ptrace, process_vm_readv, bpf, add_key/keyctl, userfaultfd, kexec_load, init_module. x86_64 and aarch64 supported.
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 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
)Three network modes.
"network": "none""network": "host""network": "bridge"$ 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 msHull means
two things in English.
And both fit what the runtime does.
Hull→ship's hull→CONTAINS 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.
Hull vs Docker vs runc
| Hull | Docker | runc | |
| Binary size | ~3 MB | ~200 MB (dockerd+containerd+runc+shim) | ~10 MB |
| Daemon required | No | Yes (dockerd + containerd) | No (but needs caller) |
| Root required | No (--rootless) | Yes (rootless experimental) | No (rootless) |
| Registry / pull | hull pull redis:7 (built-in) | docker pull (Docker Hub) | No (needs external tool) |
| Push images | hull push /rootfs --name myapp | docker push | No |
| Exec into container | hull exec <name> <cmd> | docker exec | No (needs nsenter manually) |
| Replicas + LB | mt scale myapp 3 (auto round-robin) | docker swarm / compose | No |
| seccomp default | KILL_PROCESS + audit (prints blocked syscall) | EPERM (silent fail) | KILL_PROCESS (if configured) |
| Landlock LSM | Yes (ABI v1, fs allowlist) | No | No |
| Workload profiles | 6 curated (default/webapp/node/dotnet/beam/java) | 1 generic | Custom JSON |
| Networking | none / host / bridge (veth + nftables) | bridge / host / overlay / macvlan | host only (needs CNI) |
| PID isolation (bridge) | Yes (double-fork NEWPID) | Yes | Yes |
| Image format | Flat rootfs tarball (hull pull/push) | OCI layers | OCI bundle |
| Base images | 6 (node, postgres, redis, python, busybox) | Docker Hub (millions) | None |
| State storage | JSON files on disk (~/.hull/state/) | containerd + ttrpc | JSON files (state.json) |
| Dependencies | None (static Zig binary) | containerd, runc, snapshotter | libseccomp (optional) |
One JSON file.
Three required fields.
Required
| name | string | Container name (1-64 chars) |
| rootfs | string | Path to rootfs dir or .tar.gz |
| argv | string[] | Command + arguments |
Optional (all have defaults)
| env | string[] | Environment variables |
| profile | string | Seccomp profile (default) |
| network | string | none / host / bridge |
| hostname | string | Container hostname |
| cwd | string | Working directory (/) |
| limits.* | number | memory_mb, cpu, pids |
| mounts[] | object | Bind mounts (host, container, readonly) |
| bridge.* | object | name, subnet, ip, mtu |