hull
Runtime de Contenedores

~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.

Esta pagina corre dentro de un contenedor hull — Next.js static export sobre busybox httpd, aislado con 7 capas de seguridad, orquestado por Mentat.
~3 MB
binario static-musl
7
capas de aislamiento
0
daemons
10
comandos CLI
6
imagenes base
6
perfiles seccomp

Descargas

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.

PlataformaAssetDescargar
Linux · x86_64hull-x86_64Descargar
Linux · aarch64hull-aarch64Obtener
Auto-deteccion (install.sh)install.shObtener
ChecksumsSHA256SUMSObtener
Firma · x86_64hull-x86_64.minisigObtener
Firma · aarch64hull-aarch64.minisigObtener
Llave publicaminisign.pubObtener
Binarios static-musl, sin dependencia de glibc. Codigo fuente en GitHub: github.com/rvielma/hull.
Verifica la firma

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.


Stacks validados

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.

StackQue se proboPerfilEstado
Frontend estaticoVue, Vite, React SPA, Next.js export — mismo patron que esta pagina (busybox httpd + dist/)default
Next.js 16 SSRpagina force-dynamic + ruta /api/hello, server-rendering por request, timestamps distintos verificadosdefault
.NET 8 ASP.NETself-contained publish (PublishSingleFile + InvariantGlobalization), Kestrel en 0.0.0.0:5050, /weatherforecast retorna JSONdefault
PostgreSQL 16Imagen Hull-native (debootstrap, sin docker-entrypoint, sin gosu suid), SELECT/INSERT/CREATE TABLE via psqldefault
Redis 7Imagen Hull-native, PING/SET/GET/INCR/DBSIZE via redis-clidefault
Imagenes Hull-native
Construidas con debootstrap + apt + setpriv. Sin docker-entrypoint.sh, sin gosu suid. Drop de UID con Linux capabilities + setresuid (patron post-docker).
Default seccomp cubre
Privilege drop (capset, setres*), SysV IPC (shared buffers de postgres), I/O vectorial (preadv/pwritev), io_uring (Node libuv), queries NUMA, variantes *at modernas, politica de scheduler. ptrace + bpf siguen bloqueados.
Rootless en Ubuntu 24.04
Funciona con un perfil AppArmor de una linea (flags=(unconfined) {userns,}).hull pull ajusta ownership a $SUDO_USER para que --rootless pueda hacer pivot_root.

Inicio rapido

De cero a corriendo
en tres comandos.

1. Instalar
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
2. Escribir un 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. Ejecutar
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

Referencia CLI

Ocho comandos.
Cero archivos de configuracion.

hull run [--rootless] <manifest>Iniciar un contenedor desde un manifest JSON
hull psListar 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 versionImprimir version
Codigos de salida: 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.

Modelo de Seguridad

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.

1
User namespace (NEWUSER)
El proceso cree que es uid 0; el host ve un uid sin privilegios. Habilitado con --rootless. El padre escribe uid_map/gid_map via fork-pipe dance.
2
PID namespace (NEWPID)
Arbol PID aislado. El workload es PID 1. No puede ver ni enviar signals a ningun proceso del host. El modo bridge usa double-fork para preservar el aislamiento.
3
Network namespace (NEWNET)
Tres modos: none (solo loopback — sin ruta de salida), host (stack compartido), bridge (par veth + bridge hull0 + masquerade nftables).
4
Mount namespace (NEWNS) + pivot_root
pivot_root (no chroot) hacia rootfs dedicado. El filesystem del host completamente invisible. /proc remontado para vista NEWPID.
5
cgroups v2
Limites duros en CPU (cpu.max), memoria (memory.max) y PIDs (pids.max). Aplicados por el kernel. El contenedor no puede fork-bomb ni agotar la RAM del host.
6
Landlock LSM
Allowlist de filesystem: rootfs read+exec, /tmp read+write, todo lo demas denegado. Ni uid 0 dentro del contenedor puede evadirlo. Skip graceful en kernels < 5.13.
7
seccomp-bpf
Allowlist de syscalls por perfil de workload. KILL_PROCESS en violacion (no EPERM como Docker). Instalado justo antes de execve. En kill, hull lee dmesg y reporta el syscall bloqueado.

Perfiles Seccomp

Allowlists de syscalls curadas
por tipo de workload.

default122 syscalls
Rust musl, Zig, Go, shell scripts
I/O + red + gestion de procesos + pipelines de shell. La base para servidores de binario unico.
webappdefault + 3 syscalls
Node.js 22+, Next.js SSR, runtimes userspace modernos
Default mas el trio io_uring (io_uring_setup/enter/register). Opt-in por las CVEs recientes — el grueso de los workloads se queda en el default mas estricto.
node32 syscalls
Node.js, Deno, Bun (libuv)
Ajustado: epoll, eventfd, signalfd, timerfd — el core de libuv. Sin syscalls legacy, sin creacion de archivos mas alla de openat.
dotnet36 syscalls
.NET 8/9 (CoreCLR, NativeAOT)
select/pselect6 + signalfd4 + memfd_create (staging de codigo JIT) + tgkill (signals de pthread). Minimo.
beam177 syscalls
Elixir, Erlang, Phoenix (BEAM VM)
Default + 55 extras: timerfd, signalfd, inotify, memfd_create, epoll_create, operaciones de archivo legacy (mkdir/unlink/chmod/chown).
javapermisivo syscalls
OpenJDK 8/11/17/21, Mirth Connect, apps JVM empaquetadas con install4j
Tabla completa curada — los workloads JVM ejercitan una superficie amplia (GC coordinado por signals, JNI, I/O async, JIT, IPC hsperfdata). Iterar syscall por syscall es O(N) fallos, asi que este profile permite la tabla; seccomp sigue bloqueando todo lo que esta fuera.

Bloqueados en todos los perfiles: ptrace, process_vm_readv, bpf, add_key/keyctl, userfaultfd, kexec_load, init_module. x86_64 y aarch64 soportados.


Registro de Imagenes

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 y ejecutar
# 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
)
Rootfs plano
Sin capas, sin dedup, sin graph driver. Un tarball se extrae una vez y se cachea. Los rebuilds son rapidos porque no hay nada que comparar.
Backend OxideStore
Object storage compatible con S3 corriendo en Mentat. Imagenes almacenadas localmente — sin dependencia de registro externo. Push/pull sobre HTTPS.
Importar desde Docker
Convierte imagenes Docker/OCI existentes a rootfs de hull. Aplana capas, elimina metadata, genera template de manifest. Usa lo que ya tienes.

Networking

Tres modos de red.

"network": "none"
Solo loopback. Sin ruta a internet, sin DNS. Para workloads de computo puro y sandboxes de build donde el acceso a red es un riesgo.
"network": "host"
Comparte el stack de red del host. Internet completo, loopback del host, todos los puertos. Zero overhead. Para servicios que hacen bind directo a puertos del host.
"network": "bridge"
Par veth por contenedor conectado a hull0 (10.88.0.0/24). Asignacion automatica de IP via archivos de lease. Masquerade nftables + FORWARD iptables. Los contenedores llegan a internet via la ruta por defecto del host.
Salida verificada del modo 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 ms

Por que el nombre

Hull tiene dos
acepciones en ingles.

Y las dos describen lo que hace el runtime.

Sustantivo — el casco de un barco
La estructura exterior que contiene la carga, protege del mar y separa el interior del exterior.
El barco no sabe lo que lleva adentro. La carga no sabe que esta en un barco.
hull → contiene → protege → aisla
Verbo — descascarar
Quitar la cascara o vaina de granos, nueces y semillas.
El proceso de separar lo interior de lo exterior — la misma frontera que el runtime traza entre un proceso y su host.
Por que fue el nombre perfecto
Dockerestibadormueve contenedores
Hullcascolos 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.


Comparativa

Hull vs Docker vs runc

HullDockerrunc
Tamano del binario~3 MB~200 MB (dockerd+containerd+runc+shim)~10 MB
Daemon requeridoNoSi (dockerd + containerd)No (pero necesita caller)
Root requeridoNo (--rootless)Si (rootless experimental)No (rootless)
Registry / pullhull pull redis:7 (integrado)docker pull (Docker Hub)No (necesita herramienta externa)
Push imageneshull push /rootfs --name myappdocker pushNo
Exec en containerhull exec <name> <cmd>docker execNo (nsenter manual)
Replicas + LBmt scale myapp 3 (round-robin auto)docker swarm / composeNo
seccomp por defectoKILL_PROCESS + audit (imprime syscall bloqueado)EPERM (fallo silencioso)KILL_PROCESS (si configurado)
Landlock LSMSi (ABI v1, allowlist fs)NoNo
Perfiles de workload6 curados (default/webapp/node/dotnet/beam/java)1 genericoJSON custom
Networkingnone / host / bridge (veth + nftables)bridge / host / overlay / macvlansolo host (necesita CNI)
PID isolation (bridge)Si (double-fork NEWPID)SiSi
Formato de imagenTarball rootfs plano (hull pull/push)Capas OCIBundle OCI
Imagenes base6 (node, postgres, redis, python, busybox)Docker Hub (millones)Ninguna
EstadoArchivos JSON en disco (~/.hull/state/)containerd + ttrpcArchivos JSON (state.json)
DependenciasNinguna (binario Zig estatico)containerd, runc, snapshotterlibseccomp (opcional)

Especificacion del Manifest

Un archivo JSON.
Tres campos requeridos.

Requeridos

namestringNombre del contenedor (1-64 chars)
rootfsstringRuta al directorio rootfs o .tar.gz
argvstring[]Comando + argumentos

Opcionales (todos tienen defaults)

envstring[]Variables de entorno
profilestringPerfil seccomp (default)
networkstringnone / host / bridge
hostnamestringHostname del contenedor
cwdstringDirectorio de trabajo (/)
limits.*numbermemory_mb, cpu, pids
mounts[]objectBind mounts (host, container, readonly)
bridge.*objectname, subnet, ip, mtu