Ambientes Docker Isolados e Rápidos para Rollouts de RL de Agentes de IA com gvx

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:
- Contenção de Lock e Vazamento de Estado no
dockerddo Host: Apontar centenas de workers de RL concorrentes para um único/var/run/docker.sockdo 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 dodocker0, dispara errosAnother app is currently holding the xtables locknoiptablesdo host e vaza contêineres órfãos entre episódios. - Risco de Escape de Contêiner via DinD Privilegiado: Executar contêineres
docker:dindpadrã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. - 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 sandboxesgVisor(runsc) existentes não conseguem iniciar microVMs KVM. - Latência Elevada no Reset de Episódio (
env.reset()): Iniciar um novo ambientedocker:dinde 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 (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
gvxbloqueia o próprio processo do sistema operacional hospedeiro antes de executar código convidado (PR_SET_NO_NEW_PRIVS+seccompBPF no Linux, Seatbeltdeny defaultno macOS, Restricted Token + Job Object no Windows). Mesmo que um agente de RL executedocker run --privilegedou comandos destrutivosrm -rf /dentro de seu episódio, essas operações permanecem estritamente confinadas ao kernelSentryde espaço de usuário desse episódio e ao seu arquivo de volume.ext4efê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.

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:
env.reset()de 100 Milissegundos viacp --reflink=always:- Você constrói um arquivo
golden-docker.ext4uma única vez, contendo imagens base pré-baixadas (python:3.11,postgres:16,redis:7ou imagens base específicas de tarefas doSWE-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.
- Você constrói um arquivo
- Teardown Sem Resíduos (
env.close()):- Encerrar o processo
rungv(sb.Close()em Go) e remover o arquivoep-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.
- Encerrar o processo
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 (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
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. Inicia uma instância semente docker:dind apoiada em golden-docker.ext4
4rungv run \
5 -p unix:/srv/rl-snapshots/seed.sock:2375 \
6 --mount type=volume,src=/srv/rl-snapshots/golden-docker.ext4,dst=/var/lib/docker \
7 -e DOCKER_TLS_CERTDIR= \
8 docker:dind --tls=false &
9SEED_PID=$!
10
11# 2. Pré-popula imagens base e cache de build dentro de golden-docker.ext4
12export DOCKER_HOST="unix:///srv/rl-snapshots/seed.sock"
13until docker info >/dev/null 2>&1; do sleep 0.5; done
14
15docker pull python:3.11-slim
16docker pull redis:7-alpine
17docker pull postgres:16-alpine
18
19# 3. Interrompe graciosamente a instância semente para que o ext4lite descarregue (flush) e compacte a imagem
20kill -INT $SEED_PID
21wait $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:
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# Clone instantâneo com Copy-on-Write (~100 ms em XFS/Btrfs/APFS)
5cp --reflink=always \
6 /srv/rl-snapshots/golden-docker.ext4 \
7 "/srv/rl-episodes/${EPISODE_ID}.ext4"
8
9# Inicia episódio isolado e não privilegiado com 4 GiB de RAM e 2 CPUs
10rungv run \
11 -m 4g --cpus 2 \
12 -p "unix:${SOCK}:2375" \
13 --mount "type=volume,src=/srv/rl-episodes/${EPISODE_ID}.ext4,dst=/var/lib/docker" \
14 -e DOCKER_TLS_CERTDIR= \
15 docker:dind --tls=false &
16EP_PID=$!
17
18export DOCKER_HOST="unix://${SOCK}"
19until docker info >/dev/null 2>&1; do sleep 0.2; done
20
21# Dentro do worker de rollout, as imagens ficam imediatamente disponíveis, sem pull pela rede
22docker images
23
24# Teardown do episódio após o cálculo da recompensa (~10 ms)
25kill -KILL $EP_PID
26rm -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:
1package main
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "log"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "time"
12
13 "z.gfire.dev/gvx/pkg/gvx"
14)
15
16func runEpisode(ctx context.Context, goldenExt4, scratchDir string, epID int) error {
17 epExt4 := filepath.Join(scratchDir, fmt.Sprintf("ep-%04d.ext4", epID))
18 defer os.Remove(epExt4)
19
20 // 1. Clona por reflink o volume ext4lite dourado
21 cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
22 if err := cpCmd.Run(); err != nil {
23 return fmt.Errorf("clone golden volume: %w", err)
24 }
25
26 // 2. Aplica timeout de tempo de parede por episódio
27 epCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
28 defer cancel()
29
30 var stdout, stderr bytes.Buffer
31 sb, err := gvx.New(epCtx, gvx.Config{
32 Platform: gvx.PlatformAuto,
33 NetworkMode: gvx.NetModeNAT,
34 NetRaw: true,
35 AllowPacketSocketWrite: true,
36 AllowSUID: true,
37 OCIImage: "docker:dind",
38 Entrypoint: []string{"sh", "-c"},
39 EntrypointSet: true,
40 Cmd: []string{
41 "dockerd-entrypoint.sh --tls=false >/var/log/dockerd.log 2>&1 & " +
42 "until docker info >/dev/null 2>&1; do sleep 0.2; done && " +
43 "docker run --rm python:3.11-slim python -c 'print(6 * 7)'",
44 },
45 Envv: []string{"DOCKER_TLS_CERTDIR="},
46 Mounts: []gvx.Mount{
47 {
48 Kind: gvx.MountVolume,
49 ImagePath: epExt4,
50 GuestPath: "/var/lib/docker",
51 },
52 },
53 Resources: gvx.Resources{
54 MemoryBytes: 4 << 30, // Limite de 4 GiB
55 CPUMillis: 2000, // 2.0 CPUs
56 },
57 Stdout: &stdout,
58 Stderr: &stderr,
59 })
60 if err != nil {
61 return fmt.Errorf("init episode %d: %w", epID, err)
62 }
63 defer sb.Close()
64
65 exitCode, err := sb.Run(epCtx)
66 if err != nil {
67 return fmt.Errorf("run episode %d: %w", epID, err)
68 }
69
70 log.Printf("episode %d finished: exit=%d stdout=%q", epID, exitCode, stdout.String())
71 return nil
72}
73
74func main() {
75 ctx := context.Background()
76 if err := runEpisode(ctx, "/srv/rl-snapshots/golden-docker.ext4", "/srv/rl-episodes", 1); err != nil {
77 log.Fatal(err)
78 }
79}
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
- gvx: Executando Contêineres Linux, Docker e Kubernetes em um Único Processo Não Privilegiado: Análise arquitetural completa do
gvxno Windows (whpx,systrap), macOS (hvf) e Linux (systrap). - Executando Docker, docker build e docker compose Sem Root ou User Namespaces: Guia rápido de inicialização em 3 passos e de solução de problemas quando
sudo,unshare(CLONE_NEWUSER),mount(2)e/dev/fuseestão bloqueados. - Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com gvx: Análise detalhada de por que
docker.ioerunscaninhado falham dentro dogVisor(runsc) e de como ogvxrecorre ao loop de futex_RUN_SYSCALL_LOOPem caso deEBUSY. - Ambientes Docker Rápidos e Isolados para Rollouts de RL de Agentes de IA com gvx (este post)