---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Entornos Docker Aislados y Rápidos para Rollouts de RL de Agentes de IA con gvx
description: Cómo ejecutar de forma segura cientos de entornos paralelos de Docker y Kubernetes, sin privilegios y con un aislamiento fuerte, para SWE-bench y para los rollouts de aprendizaje por refuerzo de agentes de programación, con reinicios de volúmenes ext4lite de copia en escritura en 100 milisegundos.
language: es
date: 2026-11-06T12:02:55.95126412Z
path: /blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04
go_package: z.gfire.dev/gvx
go_repourl: https://github.com/gfire-sigs/gvx.git
category: ai
---

## El cuello de botella de seguridad e infraestructura en los rollouts de RL de agentes de programación

Entrenar y evaluar agentes de ingeniería de software en benchmarks y frameworks de aprendizaje por refuerzo (RL) como **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** y **OpenHands** requiere ejecutar miles de rollouts concurrentes de **código no confiable generado por LLM**. En tareas reales de ingeniería de software, un agente no se limita a editar archivos de texto: ejecuta `docker build`, levanta bases de datos y microservicios mediante `docker compose`, ejecuta suites de pruebas de integración e inspecciona los logs de los contenedores.

A escala de rollout (de 128 a 1,024 episodios concurrentes por clúster), la infraestructura de contenedores existente enfrenta cuatro cuellos de botella sistémicos de seguridad y rendimiento:

1. **Contención de bloqueos y fuga de estado en el `dockerd` del host**: Apuntar cientos de workers de RL concurrentes a un único `/var/run/docker.sock` del host otorga a cada agente acceso equivalente a root sobre el daemon del host, serializa la extracción de capas de imágenes, agota las asignaciones de subred de `docker0`, provoca errores `Another app is currently holding the xtables lock` en el `iptables` del host y deja contenedores huérfanos filtrados entre episodios.
2. **Riesgo de escape de contenedor por DinD privilegiado**: Ejecutar contenedores `docker:dind` estándar requiere `--privileged` (`CAP_SYS_ADMIN`), lo que expone a los rollouts de RL no confiables los dispositivos de bloque crudos del host, los release agents de cgroups y los módulos del kernel.
3. **Restricciones de virtualización por hardware (`/dev/kvm`)**: Los runtimes de microVM (`Firecracker`, `QEMU`, `Kata Containers`) aíslan limpiamente los kernels internos, pero requieren `/dev/kvm`. Los pods estándar de Kubernetes sin virtualización anidada, las instancias de nube bare-metal donde KVM está deshabilitado y los sandboxes `gVisor` (`runsc`) existentes no pueden lanzar microVMs KVM.
4. **Latencia lenta de `env.reset()` al reiniciar episodios**: Iniciar un entorno `docker:dind` nuevo y descargar o desempaquetar imágenes base para cada episodio de RL tarda entre 10 y 60 segundos, dejando inactivos a los workers de entrenamiento en GPU mientras esperan los reinicios del entorno.

**[gvx](https://github.com/gfire-sigs/gvx)** (CLI `rungv` y SDK de Go `z.gfire.dev/gvx/pkg/gvx`) proporciona un **runtime de rollouts seguro y con defensa en profundidad** que resuelve los cuatro cuellos de botella. Cada entorno de rollout se ejecuta como **un único proceso de espacio de usuario, sin privilegios y con autobloqueo** (aislado por el kernel `Sentry` de gVisor de Google, `pkg/hostlockdown` y un `netstack` en memoria), cuyo estado completo de `/var/lib/docker` reside dentro de **un único archivo `.ext4` con copy-on-write (`ext4lite`)**.

> **Seguridad sin privilegios para rollouts de agentes no confiables**: Cada episodio de `gvx` bloquea su propio proceso del sistema operativo anfitrión antes de ejecutar código invitado (`PR_SET_NO_NEW_PRIVS` + `seccomp` BPF en Linux, Seatbelt `deny default` en macOS, Restricted Token + Job Object en Windows). Incluso si un agente de RL ejecuta `docker run --privileged` o comandos destructivos `rm -rf /` dentro de su episodio, esas operaciones permanecen estrictamente confinadas al kernel `Sentry` de espacio de usuario de ese episodio y a su archivo de volumen `.ext4` efímero.

---

## Arquitectura de rollouts: instancias paralelas sin privilegios + snapshots por reflink

En `gvx`, cada worker de rollout es un proceso ordinario del sistema operativo sin privilegios, con su propio kernel `Sentry` de gVisor, su propio `netstack` de espacio de usuario (`10.0.2.15/24`) y su propio volumen de bloques `ext4lite` (`ep-N.ext4`), mientras comparte, a través de la caché de páginas del host, una caché de imágenes OCI `erofs` de solo lectura y direccionable por contenido.

![Arquitectura de rollouts paralelos de agentes de RL con gvx](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Por qué los volúmenes `ext4lite` de un solo archivo cambian la economía del reinicio en RL

Dado que el sistema de archivos `ext4lite` de `gvx` almacena un volumen `ext4` completo con journaling JBD2 (incluidos los directorios de capas de `overlayfs` de Docker, los atributos extendidos `trusted.overlay.*`, los nodos de dispositivo whiteout y los metadatos de imágenes) dentro de un único archivo regular del host, la gestión del ciclo de vida de los episodios se convierte en una única operación sobre archivos:

1. **`env.reset()` de 100 milisegundos mediante `cp --reflink=always`**:
   - Construyes una vez un archivo `golden-docker.ext4` que contiene imágenes base preextraídas (`python:3.11`, `postgres:16`, `redis:7` o imágenes base específicas de tareas de `SWE-bench`).
   - Al inicio de cada episodio de RL, `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (en XFS, Btrfs, ZFS o macOS APFS) crea un clon independiente con copy-on-write en menos de 100 milisegundos, sin copiar los bloques de disco subyacentes.
   - Solo los bloques de 4 KiB modificados durante ese episodio específico consumen espacio adicional en disco.
2. **Desmontaje sin residuos (`env.close()`)**:
   - Terminar el proceso `rungv` (`sb.Close()` en Go) y desvincular `ep-0042.ext4` recupera el 100 % de los procesos, sockets de red, montajes y almacenamiento del episodio en ~10 milisegundos. Nada se monta jamás en la tabla de montajes del kernel anfitrión ni se registra en los cgroups del host.

---

## Flujo de trabajo paso a paso: snapshot dorado `.ext4` y reinicio rápido de episodios

Puedes instalar `rungv` en cada nodo worker de rollouts descargando el binario precompilado con atestación SLSA desde [GitHub Releases](https://github.com/gfire-sigs/gvx/releases) (`gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml --deny-self-hosted-runners`) o ejecutando `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest`.

### Paso 1: crear una vez el volumen dorado `docker:dind`

```sh
mkdir -p /srv/rl-snapshots /srv/rl-episodes

# 1. Inicia una instancia semilla de docker:dind respaldada por golden-docker.ext4
rungv run \
  -p unix:/srv/rl-snapshots/seed.sock:2375 \
  --mount type=volume,src=/srv/rl-snapshots/golden-docker.ext4,dst=/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
SEED_PID=$!

# 2. Precarga las imágenes base y la caché de build dentro de golden-docker.ext4
export DOCKER_HOST="unix:///srv/rl-snapshots/seed.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done

docker pull python:3.11-slim
docker pull redis:7-alpine
docker pull postgres:16-alpine

# 3. Detén de forma ordenada la instancia semilla para que ext4lite vacíe y compacte la imagen
kill -INT $SEED_PID
wait $SEED_PID
```

### Paso 2: lanzar episodios paralelos de RL en 100 milisegundos

Cada worker de rollout paralelo clona `golden-docker.ext4` mediante reflink y arranca una instancia `docker:dind` aislada con todas las imágenes ya presentes en `/var/lib/docker`:

```sh
EPISODE_ID="ep-0042"
SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"

# Clon instantáneo con copy-on-write (~100 ms en XFS/Btrfs/APFS)
cp --reflink=always \
  /srv/rl-snapshots/golden-docker.ext4 \
  "/srv/rl-episodes/${EPISODE_ID}.ext4"

# Lanza un episodio aislado sin privilegios con 4 GiB de RAM y 2 CPUs
rungv run \
  -m 4g --cpus 2 \
  -p "unix:${SOCK}:2375" \
  --mount "type=volume,src=/srv/rl-episodes/${EPISODE_ID}.ext4,dst=/var/lib/docker" \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
EP_PID=$!

export DOCKER_HOST="unix://${SOCK}"
until docker info >/dev/null 2>&1; do sleep 0.2; done

# Dentro del worker de rollout, las imágenes están disponibles de inmediato sin descarga por red
docker images

# Desmontaje del episodio tras el cálculo de la recompensa (~10 ms)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Pool programático de rollouts usando el SDK de Go `z.gfire.dev/gvx/pkg/gvx`

Si tu servidor de entornos de RL o tu harness de evaluación está escrito en Go (o llama a un daemon de rollouts en Go mediante gRPC), puedes gestionar episodios de contenedores paralelos directamente a través de `z.gfire.dev/gvx/pkg/gvx`:

```go
package main

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"z.gfire.dev/gvx/pkg/gvx"
)

func runEpisode(ctx context.Context, goldenExt4, scratchDir string, epID int) error {
	epExt4 := filepath.Join(scratchDir, fmt.Sprintf("ep-%04d.ext4", epID))
	defer os.Remove(epExt4)

	// 1. Clona por reflink el volumen golden ext4lite
	cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
	if err := cpCmd.Run(); err != nil {
		return fmt.Errorf("clone golden volume: %w", err)
	}

	// 2. Aplica un timeout de reloj de pared por episodio
	epCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
	defer cancel()

	var stdout, stderr bytes.Buffer
	sb, err := gvx.New(epCtx, gvx.Config{
		Platform:               gvx.PlatformAuto,
		NetworkMode:            gvx.NetModeNAT,
		NetRaw:                 true,
		AllowPacketSocketWrite: true,
		AllowSUID:              true,
		OCIImage:               "docker:dind",
		Entrypoint:             []string{"sh", "-c"},
		EntrypointSet:          true,
		Cmd: []string{
			"dockerd-entrypoint.sh --tls=false >/var/log/dockerd.log 2>&1 & " +
				"until docker info >/dev/null 2>&1; do sleep 0.2; done && " +
				"docker run --rm python:3.11-slim python -c 'print(6 * 7)'",
		},
		Envv: []string{"DOCKER_TLS_CERTDIR="},
		Mounts: []gvx.Mount{
			{
				Kind:      gvx.MountVolume,
				ImagePath: epExt4,
				GuestPath: "/var/lib/docker",
			},
		},
		Resources: gvx.Resources{
			MemoryBytes: 4 << 30, // Límite de 4 GiB
			CPUMillis:   2000,    // 2.0 CPUs
		},
		Stdout: &stdout,
		Stderr: &stderr,
	})
	if err != nil {
		return fmt.Errorf("init episode %d: %w", epID, err)
	}
	defer sb.Close()

	exitCode, err := sb.Run(epCtx)
	if err != nil {
		return fmt.Errorf("run episode %d: %w", epID, err)
	}

	log.Printf("episode %d finished: exit=%d stdout=%q", epID, exitCode, stdout.String())
	return nil
}

func main() {
	ctx := context.Background()
	if err := runEpisode(ctx, "/srv/rl-snapshots/golden-docker.ext4", "/srv/rl-episodes", 1); err != nil {
		log.Fatal(err)
	}
}
```

---

## Comparación de runtimes de sandbox para agentes de RL

| Métrica / Requisito                                       | `dockerd` compartido del host     | `docker:dind` privilegiado | Firecracker / microVM KVM      | `gVisor` estándar (`runsc`)       | `gvx` (`rungv` / `pkg/gvx`)                 |
| :-------------------------------------------------------- | :-------------------------------- | :------------------------- | :----------------------------- | :-------------------------------- | :------------------------------------------ |
| **Requiere `root` del host o `--privileged`**             | Sí (`docker.sock` = root)         | Sí (`--privileged`)        | Necesita `/dev/kvm` + config. `TAP` | Sí (o `userns` + `mount`)   | **No (un único proceso sin privilegios)**   |
| **Funciona dentro de Pods de K8s sin privilegios / `runsc` existente** | No                   | No                         | No (sin `/dev/kvm`)            | Falla (`EBUSY` en seccomp anidado) | **Sí (bucle de futex como fallback automático)** |
| **Soporta `docker build` y `docker compose` internos**    | Comparte el daemon del host       | Sí                         | Sí                             | Falla sin ajuste de DinD en el host | **Sí (`overlayfs` sobre `ext4lite`)**      |
| **Aislamiento de red por episodio**                       | `docker0` / bloqueo `xtables` compartido | Namespace separado  | NIC de VM separada             | netns separado                    | **`netstack` aislado en el proceso**        |
| **Latencia de reinicio de episodio en caliente (`env.reset()`)** | 5 s a 30 s (poda de contenedores/volúmenes) | 10 s a 60 s (daemon frío) | ~150 ms (snapshot de VM) | 5 s a 20 s                        | **~100 ms (`cp --reflink=always` de `.ext4`)** |
| **Desarrollo local multiplataforma (Windows / macOS / Linux)** | Requiere VM Linux             | Requiere VM Linux          | Solo KVM de Linux              | Solo Linux                        | **Nativo en Windows, macOS y Linux**        |

---

## Más lecturas en 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)**: Recorrido arquitectónico completo de `gvx` en Windows (`whpx`, `systrap`), macOS (`hvf`) y Linux (`systrap`).
2. **[Ejecutar Docker, docker build y docker compose sin root ni user namespaces](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Guía rápida de arranque en 3 pasos y de resolución de problemas cuando `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)**: Análisis detallado de por qué `docker.io` y `runsc` anidado fallan dentro de `gVisor` (`runsc`) y de cómo `gvx` recurre al bucle de futex `_RUN_SYSCALL_LOOP` ante `EBUSY`.
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)** (esta entrada)
