---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Ejecución de Docker-in-Docker dentro de gVisor y de Nested Sandboxes con gvx
description: Por qué la instalación de docker.io o el anidamiento de runsc dentro de un contenedor gVisor falla con modprobe, iptables-nft, overlay2, cgroups y seccomp EBUSY, y cómo gvx ejecuta de forma segura docker:dind anidado dentro de un kernel aislado en espacio de usuario sin --privileged.
language: es
date: 2026-11-06T12:02:55.931762327Z
path: /blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03
go_package: z.gfire.dev/gvx
go_repourl: https://github.com/gfire-sigs/gvx.git
category: lowlevel
---

## Solución rápida: ejecutar `docker:dind` de forma segura dentro de un sandbox `gVisor` (`runsc`) sin privilegios

Las plataformas de agentes de IA en la nube, los despliegues `agent-sandbox` de Kubernetes (`RuntimeClass: gvisor`) y los arneses de evaluación de aprendizaje por refuerzo aíslan las cargas de trabajo no confiables dentro de contenedores **gVisor (`runsc`)** específicamente para proteger el kernel del host. Cuando una carga de trabajo dentro de un contenedor `runsc` sin privilegios predeterminado intenta instalar `docker.io` o `docker-ce` mediante `apt-get` e iniciar `dockerd`, falla incluso si tiene `root` (`UID 0`) dentro del contenedor.

Con **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`), puede **ejecutar de forma segura** `docker:dind`, `docker build` y `docker compose` directamente dentro de un contenedor `gVisor` (`runsc`) sin privilegios (o anidar `gvx` dentro de `gvx`) **sin debilitar el `RuntimeClass` externo, sin exponer sockets del host y sin conceder `--privileged`**.

> **Preservación y refuerzo del aislamiento del sandbox externo**: Ejecutar `rungv` dentro de `runsc` no elude ninguna restricción aplicada por el sandbox externo de `gVisor`. En su lugar, `gvx` crea un segundo kernel `Sentry` de gVisor anidado en espacio de usuario, con su propio `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), su propio `netstack` en memoria y un volumen `ext4lite` de un único archivo. Los `dockerd` y los contenedores `runc` internos están doblemente aislados en sandbox: sus llamadas al sistema, montajes y paquetes sin procesar nunca salen del kernel de espacio de usuario de `gvx` interno.

```sh
# Dentro de un contenedor gVisor (runsc) existente o de un Pod de Kubernetes:
# Instale rungv desde un archivo de versión de GitHub con atestación SLSA o mediante go install:
CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &

# Ejecute compilaciones y contenedores Docker estándar contra el socket de gvx interno
export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done
docker info --format '{{.Driver}} {{.OperatingSystem}}'
# Salida: overlayfs Alpine Linux ... (containerized)

docker build -t nested-app:test .
docker run --rm nested-app:test
```

---

## Cinco razones por las que la instalación directa de Docker falla dentro de `gVisor` (`runsc`)

Cuando un usuario o un agente de programación basado en IA ejecuta `apt-get install -y docker.io` (o `docker-ce`) dentro de un contenedor `runsc` estándar e intenta iniciar `dockerd`, encuentra cinco barreras técnicas secuenciales.

### 1. Carga de módulos del kernel (`modprobe`) frente al kernel virtual `4.4.0`

Durante la instalación del paquete y el inicio de `dockerd`, los scripts de inicialización ejecutan `modprobe` para cargar `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` y `xt_conntrack`. El Sentry de gVisor externo bloquea `init_module(2)` y `finit_module(2)` e informa de una versión virtual del kernel (`4.4.0`) que no tiene un directorio `/lib/modules/4.4.0`:

```text
modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
```

### 2. Fallos de `iptables-nft` (netlink de `nf_tables`) y del puente `docker0`

En Ubuntu 22.04, Ubuntu 24.04 y Debian 11/12, `apt-get install docker.io` selecciona `iptables-nft` (respaldado por el subsistema netlink `nf_tables`) como alternativa predeterminada `/usr/sbin/iptables`. En un contenedor gVisor estándar sin privilegios de administración de red ni soporte de netlink `nf_tables`, `dockerd` falla al inicializar la cadena NAT `DOCKER` y el puente `docker0`:

```text
failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: iptables failed: iptables -t nat -N DOCKER
iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
iptables: No chain/target/match by that name.
Unable to enable SKIP DNAT rule: (iptables failed: iptables --wait -t nat -I DOCKER -i docker0 -j RETURN: iptables: No chain/target/match by that name.)
Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
```

### 3. Fallo de `overlay2` sobre el rootfs `gofer` / `9p` y agotamiento del disco con `vfs`

A menos que el administrador externo de `runsc` haya preconfigurado explícitamente un montaje `tmpfs` o un filestore dedicado en `/var/lib/docker`, el rootfs del contenedor está respaldado por el `gofer` de gVisor (`9p`/`lisafs`) o por un montaje overlay anidado. Montar `overlay2` sobre `/var/lib/docker` falla:

```text
level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
failed to mount overlay: invalid argument
Error starting daemon: error initializing graphdriver: driver not supported: overlay2
```

Cuando usuarios o agentes intentan eludir este error iniciando `dockerd --storage-driver=vfs`, Docker desactiva por completo la compartición de capas Copy-on-Write. Cada instrucción `RUN` de un `Dockerfile` realiza una copia recursiva completa (`dirCopy`) de todo el sistema de archivos raíz, lo que multiplica una imagen de 500 MB a lo largo de 10 pasos de compilación en 5 GB de escrituras y termina el sandbox con `no space left on device` o con un tiempo de espera de E/S.

### 4. Rechazos de montaje de cgroups y de `/proc` en `runc` anidado

Incluso si `dockerd` se fuerza a iniciar con `dockerd --iptables=false --bridge=none --storage-driver=vfs`, ejecutar `docker run` o `docker build` invoca `runc` para crear el contenedor interno. En un contenedor gVisor predeterminado sin privilegios, `runc` falla al montar cgroups, al volver a montar `/proc` o al cargar filtros seccomp internos:

```text
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process:
error during container init: error mounting "cgroup" to rootfs at "/sys/fs/cgroup": mount cgroup:/sys/fs/cgroup/cpuset (via /proc/self/fd/6), flags: 0xf, data: cpuset: operation not permitted
cgroups: cgroup mountpoint does not exist: unknown
runc create failed: systemd not running on this host: cannot use systemd cgroup manager
error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
error during container init: error loading seccomp filter into kernel: operation not permitted
```

### 5. `runsc` anidado falla con `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Si un usuario intenta ejecutar otra instancia de `runsc` dentro de `runsc` usando la plataforma `systrap`, la inicialización se interrumpe de inmediato:

```text
creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
```

Tanto el kernel de Linux como el Sentry de gVisor permiten **como máximo un** listener `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) por jerarquía de tareas. Como el sandbox externo ya ocupa esa ranura de listener, la llamada interna `seccomp(2)` devuelve `EBUSY` (`device or resource busy`).

---

## Cómo `gvx` resuelve la ejecución anidada dentro de `gVisor`

`gvx` aborda las cinco barreras dentro de su propio kernel de espacio de usuario y de su capa de plataforma, lo que permite que `rungv` se ejecute dentro de un contenedor externo de `gVisor` (`runsc`) o incluso de forma recursiva dentro de otro contenedor `gvx` (`gvx` en `gvx`).

![Ejecución anidada de gvx dentro de gVisor externo](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Reserva automática de `systrap` desde `EBUSY` hacia el bucle futex `_RUN_SYSCALL_LOOP`

En `systrap` de Linux, cada subproceso stub ejecuta dos tipos de hilos:

- **Hilos de ejecución del invitado (`sysmsg`)**: interceptan las llamadas al sistema del invitado mediante `SECCOMP_RET_TRAP` (`SIGSYS`) y trampolines `jmp rel32` parcheados en el binario (`syshandler`), e intercambian el estado de los registros con el Sentry mediante memoria compartida y `SYS_FUTEX`.
- **Un `syscallThread` stub (`syscall_thread.go`)**: ejecuta las llamadas al sistema del espacio de direcciones (`mmap`, `munmap`, `clone`) dentro del subproceso stub en nombre del Sentry.

En el gVisor upstream, `syscallThread` admite dos modos de activación en `stub_amd64.s` y `stub_arm64.s`:

1. **Bucle de notificación de usuario de seccomp (`_RUN_SECCOMP_LOOP = 6`, cuando `t.seccompNotify != nil`)**: Durante `syscallThread.init()`, `t.installSeccompNotify()` instala un listener `SECCOMP_RET_USER_NOTIF` (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`) para que el Sentry pueda despachar solicitudes mediante `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` y `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Bucle de llamadas al sistema con futex (`_RUN_SYSCALL_LOOP = 5`, cuando `t.seccompNotify == nil`)**: `syscallThread` ejecuta `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`), durmiendo en `SYS_FUTEX` (`FUTEX_WAIT`) sobre la región compartida de 2 páginas `syscallSentryMessage` (`PROT_READ` en el stub) y `syscallStubMessage` (`PROT_READ | PROT_WRITE` en el stub).

El gVisor upstream llama incondicionalmente a `t.installSeccompNotify()` siempre que `seccompNotifyIsSupported` sea verdadero, y aborta la creación del stub si `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` devuelve `EBUSY`.

En `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` y `syscall_thread.go`), `syscallThread.init()` llama a `t.installSeccompNotifyIfFree()`:

```go
// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
var seccompNotifyBusy atomic.Bool

func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
	if seccompNotifyBusy.Load() {
		return nil, nil
	}
	f, err := t.installSeccompNotify()
	if errors.Is(err, unix.EBUSY) {
		if seccompNotifyBusy.CompareAndSwap(false, true) {
			log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
				"The stub threads use the slower futex loop instead of seccomp-unotify.")
		}
		return nil, nil
	}
	return f, err
}
```

Cuando `installSeccompNotify()` devuelve `unix.EBUSY` porque un sandbox externo `gVisor` (`runsc`) o `gvx` ya ocupa la única ranura de listener de la cadena de filtros del proceso:

- `installSeccompNotifyIfFree()` establece `seccompNotifyBusy` (`atomic.Bool`), registra una única advertencia para el proceso y deja `t.seccompNotify == nil` para ese stub y para todos los stubs posteriores.
- En `syscallThread.detach()` (`syscall_thread_amd64.go` y `syscall_thread_arm64.go`), `t.seccompNotify == nil` establece `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) en `amd64` (o `regs.Regs[9] = _RUN_SYSCALL_LOOP` en `arm64`) en lugar de `_RUN_SECCOMP_LOOP` (`6`).
- En `syscallThread.syscall()` (`syscall_thread.go`), el Sentry interno despacha las llamadas al sistema del stub exclusivamente mediante memoria compartida y `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Notificar al hilo de llamadas al sistema sobre una nueva solicitud.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Esperar la respuesta.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Como los filtros `SECCOMP_RET_TRAP` sobre los hilos invitados se apilan a una profundidad arbitraria y `syscallThread` recurre a `_RUN_SYSCALL_LOOP` sin solicitar un segundo listener de notificación de usuario, `gvx` se ejecuta correctamente dentro de `runsc` y admite anidamiento recursivo (`gvx` dentro de `gvx`).

### 2. `overlayfs` nativo sobre volúmenes `ext4lite` de un único archivo

Para evitar montar `overlayfs` sobre el sistema de archivos raíz `gofer` (`9p`/`lisafs`) del contenedor `gVisor` externo, `-v gvx-docker:/var/lib/docker` respalda `/var/lib/docker` con el sistema de archivos interno de `gvx`, **`ext4lite`**, almacenado en un único archivo regular (`gvx-docker.ext4`).

Para el `runsc` externo, `gvx-docker.ext4` es simplemente un archivo regular al que se accede mediante `pread64(2)` y `pwrite64(2)`. Dentro de `gvx`, `ext4lite` proporciona un sistema de archivos `ext4` con journal JBD2, atributos extendidos `trusted.overlay.*` y `security.capability`, nodos whiteout de dispositivos de caracteres (`0:0`), entradas de directorio `d_type` y `RENAME_EXCHANGE`, lo que permite que el `dockerd` interno utilice su controlador de almacenamiento nativo `overlayfs`.

### 3. `netstack` autocontenido con `docker0` e `iptables-legacy`

Dentro de `gvx`, `dockerd` no interactúa con las interfaces de red ni con las tablas netfilter del contenedor `runsc` externo. En su lugar:

- `dockerd` crea su puente `docker0` y sus pares `veth` dentro del `netstack` propio de `gvx` en espacio de usuario.
- Todas las reglas de `iptables-legacy` (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) se ejecutan contra la implementación interna de netfilter de `gvx`.
- Los contenedores `runc` anidados montan sus jerarquías `/proc`, `/sys` y `/sys/fs/cgroup` dentro de la tabla de montajes `VFS2` en memoria de `gvx`, sin invocar nunca la llamada al sistema `mount(2)` del `runsc` externo.

---

## Verificación de `docker:dind` anidado dentro de `gVisor` (o `gvx` dentro de `gvx`)

Puede verificar ambos escenarios (`rungv` dentro de `runsc` o `rungv` dentro de `rungv`) con los siguientes comandos:

```sh
# Dentro de un contenedor gVisor (runsc) externo o de un contenedor rungv externo:
mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
DIND_PID=$!

export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done

# Verifique que el daemon Docker interno utiliza overlayfs nativo (y no vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Salida: Storage Driver: overlayfs

# Ejecute una compilación multicapa y un contenedor anidado
mkdir -p /tmp/nested-test && cd /tmp/nested-test
cat <<'EOF' > Dockerfile
FROM alpine:3.20
RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
USER appuser
WORKDIR /app
RUN echo "nested-build-ok" > status.txt
CMD ["cat", "/app/status.txt"]
EOF

docker build -t nested-test:latest .
docker run --rm nested-test:latest
# Salida: nested-build-ok

kill -INT $DIND_PID
```

---

## Lecturas adicionales de esta serie

1. **[gvx: ejecución de contenedores Linux, Docker y Kubernetes en un solo proceso sin privilegios](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Descripción general de `gvx` en Windows (`whpx`, `systrap`), macOS (`hvf`) y Linux (`systrap`).
2. **[Ejecutar Docker, docker build y docker compose sin root ni espacios de nombres de usuario](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Cómo `gvx` ejecuta `docker:dind` cuando `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` y `/dev/fuse` están bloqueados.
3. **[Ejecutar Docker-in-Docker dentro de gVisor y sandboxes anidados con gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (esta entrada)
4. **[Entornos Docker aislados y rápidos para rollouts de RL de agentes de IA con gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Cómo usar `gvx` y las instantáneas copy-on-write de `ext4lite` de un único archivo para ejecutar cientos de entornos paralelos de `SWE-bench` y de rollouts de RL con reinicios de 100 milisegundos.
