~3 MB. Sin daemon.
7 capas de aislamiento.
Hull es un runtime de contenedores Linux sin daemon escrito en Zig. Un unico binario static-musl — sin dockerd, sin containerd, sin shim. Cada hull run ejecuta el workload y termina. Namespaces, cgroups v2, seccomp-bpf, Landlock, pivot_root, veth bridge — todo en una sola llamada.
Un binario.
Sin instalador. Sin servicio.
Coloca el binario en cualquier directorio del PATH y ejecuta. Sin servicio en background, sin unit de systemd, sin grupo privilegiado para --rootless.
| Plataforma | Asset | Descargar |
| Linux · x86_64 | hull-x86_64 | Descargar |
| Linux · aarch64 | hull-aarch64 | Obtener |
| Auto-deteccion (install.sh) | install.sh | Obtener |
| Checksums | SHA256SUMS | Obtener |
| Firma · x86_64 | hull-x86_64.minisig | Obtener |
| Firma · aarch64 | hull-aarch64.minisig | Obtener |
| Llave publica | minisign.pub | Obtener |
Cada binario publicado aqui esta firmado con minisign. Dos chequeos: SHA256 para detectar corrupcion en la descarga, y la firma para confirmar que los bits vienen del proyecto Hull, no de un MITM.
# 1. Descarga binario, firma, checksum, y llave publica 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. Compara SHA256 sha256sum -c SHA256SUMS --ignore-missing # hull-x86_64: OK # 3. Verifica firma minisign contra la llave publica del proyecto minisign -Vm hull -P RWQLMHs2c5iiqJf3r7KPdMKdIOgDalrXqzEKI7ijt0DROB2ywop8pbxr # Signature and comment signature verified # Solo despues de que ambos checks pasan: chmod +x hull && sudo mv hull /usr/local/bin/
Llave publica: RWQLMHs2c5iiqJf3r7KPdMKdIOgDalrXqzEKI7ijt0DROB2ywop8pbxr (tambien en /releases/minisign.pub). Si minisign -V falla, el binario NO es el que firmo el proyecto Hull — no lo ejecutes.
Apps reales, perfil por defecto.
Sin aflojar seccomp.
Cada fila se valido end-to-end en una VM GCP limpia (Ubuntu 24.04) sin Mentat, sin Docker: descargar binario, hull pull, hull run, request al servicio, respuesta recibida. Cero syscalls deshabilitadas, cero contenedor privilegiado — las 7 capas de aislamiento activas.
| Stack | Que se probo | Perfil | Estado |
| Frontend estatico | Vue, Vite, React SPA, Next.js export — mismo patron que esta pagina (busybox httpd + dist/) | default | ● |
| Next.js 16 SSR | pagina force-dynamic + ruta /api/hello, server-rendering por request, timestamps distintos verificados | default | ● |
| .NET 8 ASP.NET | self-contained publish (PublishSingleFile + InvariantGlobalization), Kestrel en 0.0.0.0:5050, /weatherforecast retorna JSON | default | ● |
| PostgreSQL 16 | Imagen Hull-native (debootstrap, sin docker-entrypoint, sin gosu suid), SELECT/INSERT/CREATE TABLE via psql | default | ● |
| Redis 7 | Imagen Hull-native, PING/SET/GET/INCR/DBSIZE via redis-cli | default | ● |
debootstrap + apt + setpriv. Sin docker-entrypoint.sh, sin gosu suid. Drop de UID con Linux capabilities + setresuid (patron post-docker).flags=(unconfined) {userns,}).hull pull ajusta ownership a $SUDO_USER para que --rootless pueda hacer pivot_root.De cero a corriendo
en tres comandos.
curl -fsSL https://hull.getmentat.run/install.sh | sh
# o manualmente:
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 myappOcho comandos.
Cero archivos de configuracion.
| hull run [--rootless] <manifest> | Iniciar un contenedor desde un manifest JSON |
| hull ps | Listar contenedores corriendo con PID, uptime, argv |
| hull stop <name> | Apagado graceful (SIGTERM) |
| hull kill <name> | Kill inmediato (SIGKILL) |
| hull exec <name> <cmd...> | Ejecutar un comando dentro de un contenedor corriendo |
| hull logs <name> | Imprimir stdout/stderr capturado |
| hull inspect <name> | Mostrar cgroup, namespaces, puntos de montaje |
| hull pull <name>:<tag> | Descargar imagen del registry |
| hull push <rootfs> [--name <n>] [--tag <t>] | Subir rootfs al registry |
| hull version | Imprimir version |
0 exito, 1 uso, 2 runtime, 3 manifest, 127 exec fallido. En violacion seccomp (SIGSYS), hull lee dmesg e imprime el numero de syscall bloqueado.Siete capas.
Cada una independiente.
La falla de una capa no deshabilita las otras. Un workload que escape seccomp aun impacta Landlock. Un workload que evada Landlock aun ve un arbol PID aislado y un /proc vacio.
Allowlists de syscalls curadas
por tipo de workload.
default122 syscallswebappdefault + 3 syscallsnode32 syscallsdotnet36 syscallsbeam177 syscallsjavapermisivo syscallsBloqueados en todos los perfiles: ptrace, process_vm_readv, bpf, add_key/keyctl, userfaultfd, kexec_load, init_module. x86_64 y aarch64 soportados.
Pull. Run.
Sin Docker.
Las imagenes de Hull son tarballs de rootfs planos almacenados en un backend compatible con S3 (OxideStore). Sin capas, sin manifest lists, sin indices de plataforma. Un tarball por imagen, un comando para pull.
# Pull una imagen del registro de hull
hull pull hull.getmentat.run/node:22-slim
hull pull hull.getmentat.run/postgres:16
hull pull hull.getmentat.run/redis:7
# Ejecutar
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
)Tres modos de red.
"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 tiene dos
acepciones en ingles.
Y las dos describen lo que hace el runtime.
Hull→casco→los CONTIENE y PROTEGE
Un proceso dentro de un hull container no sabe que esta contenido. El host no sabe lo que corre adentro mas alla de lo que Hull permite explicitamente. Es exactamente la metafora del casco de un barco — separacion hermetica entre interior y exterior. Y practicamente: cuatro letras, facil en espanol ("jal"), misma energia fonetica que Docker, sin ambiguedad.
Hull vs Docker vs runc
| Hull | Docker | runc | |
| Tamano del binario | ~3 MB | ~200 MB (dockerd+containerd+runc+shim) | ~10 MB |
| Daemon requerido | No | Si (dockerd + containerd) | No (pero necesita caller) |
| Root requerido | No (--rootless) | Si (rootless experimental) | No (rootless) |
| Registry / pull | hull pull redis:7 (integrado) | docker pull (Docker Hub) | No (necesita herramienta externa) |
| Push imagenes | hull push /rootfs --name myapp | docker push | No |
| Exec en container | hull exec <name> <cmd> | docker exec | No (nsenter manual) |
| Replicas + LB | mt scale myapp 3 (round-robin auto) | docker swarm / compose | No |
| seccomp por defecto | KILL_PROCESS + audit (imprime syscall bloqueado) | EPERM (fallo silencioso) | KILL_PROCESS (si configurado) |
| Landlock LSM | Si (ABI v1, allowlist fs) | No | No |
| Perfiles de workload | 6 curados (default/webapp/node/dotnet/beam/java) | 1 generico | JSON custom |
| Networking | none / host / bridge (veth + nftables) | bridge / host / overlay / macvlan | solo host (necesita CNI) |
| PID isolation (bridge) | Si (double-fork NEWPID) | Si | Si |
| Formato de imagen | Tarball rootfs plano (hull pull/push) | Capas OCI | Bundle OCI |
| Imagenes base | 6 (node, postgres, redis, python, busybox) | Docker Hub (millones) | Ninguna |
| Estado | Archivos JSON en disco (~/.hull/state/) | containerd + ttrpc | Archivos JSON (state.json) |
| Dependencias | Ninguna (binario Zig estatico) | containerd, runc, snapshotter | libseccomp (opcional) |
Un archivo JSON.
Tres campos requeridos.
Requeridos
| name | string | Nombre del contenedor (1-64 chars) |
| rootfs | string | Ruta al directorio rootfs o .tar.gz |
| argv | string[] | Comando + argumentos |
Opcionales (todos tienen defaults)
| env | string[] | Variables de entorno |
| profile | string | Perfil seccomp (default) |
| network | string | none / host / bridge |
| hostname | string | Hostname del contenedor |
| cwd | string | Directorio de trabajo (/) |
| limits.* | number | memory_mb, cpu, pids |
| mounts[] | object | Bind mounts (host, container, readonly) |
| bridge.* | object | name, subnet, ip, mtu |