---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Ambientes Docker Isolados e Rápidos para Rollouts de RL de Agentes de IA com gvx
description: Como executar de forma segura centenas de ambientes Docker e Kubernetes paralelos, fortemente isolados e sem privilégios, destinados a SWE-bench e a rollouts de aprendizado por reforço para agentes de codificação, com redefinições de volumes ext4lite de copy-on-write em 100 milissegundos.
language: pt
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
---

## O Gargalo de Segurança e Infraestrutura nos Rollouts de RL de Agentes de Programação

O treinamento e a avaliação de agentes de engenharia de software em benchmarks e frameworks de aprendizado por reforço (RL), como **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** e **OpenHands**, exigem a execução de milhares de rollouts concorrentes de **código não confiável gerado por LLMs**. Em tarefas reais de engenharia de software, um agente não se limita a editar arquivos de texto: ele executa `docker build`, inicia bancos de dados e microsserviços via `docker compose`, executa suítes de testes de integração e inspeciona logs de contêineres.

Em escala de rollout (128 a 1.024 episódios concorrentes por cluster), a infraestrutura de contêineres existente enfrenta quatro gargalos sistêmicos de segurança e desempenho:

1. **Contenção de Lock e Vazamento de Estado no `dockerd` do Host**: Apontar centenas de workers de RL concorrentes para um único `/var/run/docker.sock` do host concede a cada agente acesso equivalente ao de root ao daemon do host, serializa a extração de camadas de imagens, esgota as alocações de sub-rede do `docker0`, dispara erros `Another app is currently holding the xtables lock` no `iptables` do host e vaza contêineres órfãos entre episódios.
2. **Risco de Escape de Contêiner via DinD Privilegiado**: Executar contêineres `docker:dind` padrão exige `--privileged` (`CAP_SYS_ADMIN`), o que expõe dispositivos de bloco brutos do host, release agents de cgroup e módulos do kernel a rollouts de RL não confiáveis.
3. **Restrições de Virtualização por Hardware (`/dev/kvm`)**: Runtimes de microVM (`Firecracker`, `QEMU`, `Kata Containers`) isolam adequadamente os kernels internos, mas exigem `/dev/kvm`. Pods Kubernetes padrão sem virtualização aninhada, instâncias de nuvem bare-metal com KVM desabilitado e sandboxes `gVisor` (`runsc`) existentes não conseguem iniciar microVMs KVM.
4. **Latência Elevada no Reset de Episódio (`env.reset()`)**: Iniciar um novo ambiente `docker:dind` e baixar ou desempacotar imagens base para cada episódio de RL leva de 10 a 60 segundos, deixando os workers de treinamento em GPU ociosos enquanto aguardam os resets do ambiente.

**[gvx](https://github.com/gfire-sigs/gvx)** (CLI `rungv` e SDK em Go `z.gfire.dev/gvx/pkg/gvx`) fornece um **runtime de rollout seguro, com defesa em profundidade**, que resolve os quatro gargalos. Cada ambiente de rollout é executado como **um único processo de espaço de usuário, não privilegiado e com auto-bloqueio** (isolado pelo kernel `Sentry` do gVisor do Google, por `pkg/hostlockdown` e por um `netstack` em memória), cujo estado completo de `/var/lib/docker` reside dentro de **um único arquivo `.ext4` com Copy-on-Write (`ext4lite`)**.

> **Segurança Zero-Privilege para Rollouts de Agentes Não Confiáveis**: Cada episódio `gvx` bloqueia o próprio processo do sistema operacional hospedeiro antes de executar código convidado (`PR_SET_NO_NEW_PRIVS` + `seccomp` BPF no Linux, Seatbelt `deny default` no macOS, Restricted Token + Job Object no Windows). Mesmo que um agente de RL execute `docker run --privileged` ou comandos destrutivos `rm -rf /` dentro de seu episódio, essas operações permanecem estritamente confinadas ao kernel `Sentry` de espaço de usuário desse episódio e ao seu arquivo de volume `.ext4` efêmero.

---

## Arquitetura de Rollout: Instâncias Paralelas Não Privilegiadas + Snapshots Reflink

No `gvx`, cada worker de rollout é um processo comum do sistema operacional, sem privilégios, com seu próprio kernel `Sentry` do gVisor, seu próprio `netstack` de espaço de usuário (`10.0.2.15/24`) e seu próprio volume de bloco `ext4lite` (`ep-N.ext4`), enquanto compartilha um cache de imagens OCI `erofs` somente leitura, endereçado por conteúdo, por meio do page cache do host.

![Arquitetura Paralela de Rollout de Agentes RL com gvx](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Por Que Volumes `ext4lite` de Arquivo Único Mudam a Economia do Reset em RL

Como o sistema de arquivos `ext4lite` do `gvx` armazena um volume `ext4` completo com journal JBD2 (incluindo os diretórios de camadas do `overlayfs` do Docker, atributos estendidos `trusted.overlay.*`, nós de dispositivo whiteout e metadados de imagem) dentro de um único arquivo regular do host, o gerenciamento do ciclo de vida do episódio se reduz a uma única operação de arquivo:

1. **`env.reset()` de 100 Milissegundos via `cp --reflink=always`**:
   - Você constrói um arquivo `golden-docker.ext4` uma única vez, contendo imagens base pré-baixadas (`python:3.11`, `postgres:16`, `redis:7` ou imagens base específicas de tarefas do `SWE-bench`).
   - No início de cada episódio de RL, `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (em XFS, Btrfs, ZFS ou macOS APFS) cria um clone independente com Copy-on-Write em menos de 100 milissegundos, sem copiar os blocos de disco subjacentes.
   - Apenas os blocos de 4 KiB modificados durante aquele episódio específico consomem espaço adicional em disco.
2. **Teardown Sem Resíduos (`env.close()`)**:
   - Encerrar o processo `rungv` (`sb.Close()` em Go) e remover o arquivo `ep-0042.ext4` (unlink) recupera 100% dos processos, sockets de rede, montagens e armazenamento do episódio em cerca de 10 milissegundos. Nada é montado na tabela de montagens do kernel do host nem registrado em cgroups do host.

---

## Fluxo de Trabalho Passo a Passo: Snapshot `.ext4` Dourado e Reset Rápido de Episódio

Você pode instalar o `rungv` em cada nó worker de rollout, baixando o binário pré-compilado com atestação SLSA em [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`) ou executando `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest`.

### Passo 1: Criar o Volume `docker:dind` Dourado Uma Única Vez

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

# 1. Inicia uma instância semente docker:dind apoiada em 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. Pré-popula imagens base e cache 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. Interrompe graciosamente a instância semente para que o ext4lite descarregue (flush) e compacte a imagem
kill -INT $SEED_PID
wait $SEED_PID
```

### Passo 2: Gerar Episódios Paralelos de RL em 100 Milissegundos

Cada worker de rollout paralelo clona `golden-docker.ext4` via reflink e inicializa uma instância `docker:dind` isolada, com todas as imagens já presentes em `/var/lib/docker`:

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

# Clone instantâneo com Copy-on-Write (~100 ms em XFS/Btrfs/APFS)
cp --reflink=always \
  /srv/rl-snapshots/golden-docker.ext4 \
  "/srv/rl-episodes/${EPISODE_ID}.ext4"

# Inicia episódio isolado e não privilegiado com 4 GiB de RAM e 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 do worker de rollout, as imagens ficam imediatamente disponíveis, sem pull pela rede
docker images

# Teardown do episódio após o cálculo da recompensa (~10 ms)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Pool de Rollouts Programático Usando o SDK em Go `z.gfire.dev/gvx/pkg/gvx`

Se o seu servidor de ambientes de RL ou o harness de avaliação for escrito em Go (ou chamar um daemon de rollout em Go via gRPC), você pode gerenciar episódios de contêineres paralelos diretamente pelo `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 o volume ext4lite dourado
	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 timeout de tempo de parede por episódio
	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, // Limite 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)
	}
}
```

---

## Comparação de Runtimes de Sandbox para Agentes de RL

| Métrica / Requisito                                       | `dockerd` Compartilhado do Host    | `docker:dind` Privilegiado | Firecracker / KVM MicroVM          | `gVisor` Padrão (`runsc`)           | `gvx` (`rungv` / `pkg/gvx`)                    |
| :-------------------------------------------------------- | :--------------------------------- | :------------------------- | :--------------------------------- | :---------------------------------- | :--------------------------------------------- |
| **`root` do host ou `--privileged` obrigatório**          | Sim (`docker.sock` = root)         | Sim (`--privileged`)       | Requer `/dev/kvm` + configuração de `TAP` | Sim (ou `userns` + `mount`)  | **Não (processo único sem privilégios)**       |
| **Funciona em Pods K8s sem privilégios / `runsc` existente** | Não                              | Não                        | Não (sem `/dev/kvm`)               | Falha (`EBUSY` em seccomp aninhado) | **Sim (fallback automático para o loop de futex)** |
| **Suporta `docker build` e `docker compose` internos**    | Compartilha o daemon do host       | Sim                        | Sim                                | Falha sem ajuste de DinD no host    | **Sim (`overlayfs` sobre `ext4lite`)**         |
| **Isolamento de rede por episódio**                       | `docker0` / lock `xtables` compartilhado | Namespace separado   | NIC de VM separada                 | netns separado                      | **`netstack` isolado no processo**             |
| **Latência de reset de episódio quente (`env.reset()`)**  | 5 s a 30 s (prune de contêiner/volume) | 10 s a 60 s (daemon frio) | ~150 ms (snapshot de VM)        | 5 s a 20 s                          | **~100 ms (`cp --reflink=always` de `.ext4`)** |
| **Desenvolvimento local multiplataforma (Windows / macOS / Linux)** | Requer VM Linux          | Requer VM Linux            | Somente KVM Linux                  | Somente Linux                       | **Nativo em Windows, macOS e Linux**           |

---

## Leitura Complementar Nesta Série

1. **[gvx: Executando Contêineres Linux, Docker e Kubernetes em um Único Processo Não Privilegiado](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Análise arquitetural completa do `gvx` no Windows (`whpx`, `systrap`), macOS (`hvf`) e Linux (`systrap`).
2. **[Executando Docker, docker build e docker compose Sem Root ou User Namespaces](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Guia rápido de inicialização em 3 passos e de solução de problemas quando `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` e `/dev/fuse` estão bloqueados.
3. **[Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: Análise detalhada de por que `docker.io` e `runsc` aninhado falham dentro do `gVisor` (`runsc`) e de como o `gvx` recorre ao loop de futex `_RUN_SYSCALL_LOOP` em caso de `EBUSY`.
4. **[Ambientes Docker Rápidos e Isolados para Rollouts de RL de Agentes de IA com gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** (este post)
