---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Ambienti Docker isolati e rapidi per i rollout di RL degli agenti AI con gvx
description: Come eseguire in modo sicuro centinaia di ambienti Docker e Kubernetes paralleli, fortemente isolati e senza privilegi, destinati a SWE-bench e ai rollout di apprendimento per rinforzo di agenti di coding, con ripristini dei volumi ext4lite in copy-on-write da 100 millisecondi.
language: it
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
---

## Il collo di bottiglia di sicurezza e infrastruttura nei rollout RL degli agenti di programmazione

L'addestramento e la valutazione di agenti per l'ingegneria del software su benchmark e framework di reinforcement learning (RL) come **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** e **OpenHands** richiedono l'esecuzione di migliaia di rollout concorrenti di **codice non attendibile generato da LLM**. Nei compiti reali di ingegneria del software, un agente non si limita a modificare file di testo: esegue `docker build`, avvia database e microservizi tramite `docker compose`, esegue suite di test di integrazione e ispeziona i log dei container.

Su scala di rollout (da 128 a 1.024 episodi concorrenti per cluster), l'infrastruttura di container esistente incontra quattro colli di bottiglia sistemici di sicurezza e prestazioni:

1. **Contesa di lock sul `dockerd` dell'host e perdita di stato**: Indirizzare centinaia di worker RL concorrenti verso un singolo `/var/run/docker.sock` dell'host concede a ogni agente un accesso equivalente a root al daemon dell'host, serializza l'estrazione dei layer delle immagini, esaurisce le allocazioni della subnet di `docker0`, provoca errori `Another app is currently holding the xtables lock` in `iptables` dell'host e lascia container orfani tra un episodio e l'altro.
2. **Rischio di evasione dal container tramite DinD privilegiato**: Eseguire container `docker:dind` standard richiede `--privileged` (`CAP_SYS_ADMIN`), che espone ai rollout RL non attendibili i block device grezzi dell'host, i release agent di cgroup e i moduli del kernel.
3. **Vincoli della virtualizzazione hardware (`/dev/kvm`)**: I runtime MicroVM (`Firecracker`, `QEMU`, `Kata Containers`) isolano in modo pulito i kernel interni, ma richiedono `/dev/kvm`. I pod Kubernetes standard senza virtualizzazione annidata, le istanze cloud bare-metal in cui KVM è disabilitato e le sandbox `gVisor` (`runsc`) esistenti non possono avviare microVM KVM.
4. **Latenza elevata del reset dell'episodio (`env.reset()`)**: Avviare un nuovo ambiente `docker:dind` e scaricare o decomprimere le immagini di base per ogni episodio RL richiede da 10 a 60 secondi, lasciando inattivi i worker di addestramento GPU in attesa del reset degli ambienti.

**[gvx](https://github.com/gfire-sigs/gvx)** (CLI `rungv` e SDK Go `z.gfire.dev/gvx/pkg/gvx`) fornisce un **runtime di rollout sicuro e con difesa in profondità** che risolve tutti e quattro i colli di bottiglia. Ogni ambiente di rollout viene eseguito come **un unico processo user-space non privilegiato e auto-bloccato** (isolato dal kernel `Sentry` di gVisor di Google, da `pkg/hostlockdown` e da un `netstack` in memoria) il cui intero stato di `/var/lib/docker` risiede all'interno di **un unico file `.ext4` copy-on-write (`ext4lite`)**.

> **Sicurezza a privilegi zero per i rollout di agenti non attendibili**: Ogni episodio `gvx` blocca il proprio processo del sistema operativo host prima di eseguire codice guest (`PR_SET_NO_NEW_PRIVS` + `seccomp` BPF su Linux, Seatbelt `deny default` su macOS, Restricted Token + Job Object su Windows). Anche se un agente RL esegue `docker run --privileged` o comandi distruttivi `rm -rf /` all'interno del proprio episodio, tali operazioni restano rigorosamente confinate al kernel `Sentry` user-space di quell'episodio e al suo file volume `.ext4` effimero.

---

## Architettura di rollout: istanze parallele non privilegiate + snapshot reflink

In `gvx`, ogni worker di rollout è un normale processo del sistema operativo non privilegiato, con il proprio kernel gVisor `Sentry`, il proprio `netstack` user-space (`10.0.2.15/24`) e il proprio volume a blocchi `ext4lite` (`ep-N.ext4`), condividendo al tempo stesso una cache di immagini OCI `erofs` in sola lettura, indirizzata per contenuto, tramite la page cache dell'host.

![Architettura di rollout RL parallela per agenti con gvx](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Perché i volumi `ext4lite` a file singolo cambiano l'economia del reset RL

Poiché il filesystem `ext4lite` di `gvx` memorizza un intero volume `ext4` con journal JBD2 (incluse le directory dei layer `overlayfs` di Docker, gli attributi estesi `trusted.overlay.*`, i nodi device whiteout e i metadati delle immagini) all'interno di un unico file regolare dell'host, la gestione del ciclo di vita dell'episodio diventa un'unica operazione su file:

1. **`env.reset()` in 100 millisecondi tramite `cp --reflink=always`**:
   - Si costruisce una sola volta un file `golden-docker.ext4` contenente le immagini di base preinstallate (`python:3.11`, `postgres:16`, `redis:7` o immagini di base specifiche per `SWE-bench`).
   - All'inizio di ogni episodio RL, `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (su XFS, Btrfs, ZFS o macOS APFS) crea un clone Copy-on-Write indipendente in meno di 100 millisecondi, senza copiare i blocchi del disco sottostante.
   - Solo i blocchi da 4 KiB modificati durante quello specifico episodio occupano spazio aggiuntivo su disco.
2. **Smantellamento senza residui (`env.close()`)**:
   - Terminare il processo `rungv` (`sb.Close()` in Go) ed eliminare `ep-0042.ext4` recupera il 100% dei processi, dei socket di rete, dei mount e dello storage dell'episodio in circa 10 millisecondi. Nulla viene mai montato nella mount table del kernel dell'host né registrato nei cgroup dell'host.

---

## Flusso di lavoro passo passo: snapshot `.ext4` golden e reset rapido dell'episodio

È possibile installare `rungv` su ciascun nodo worker di rollout scaricando il binario precompilato con attestazione SLSA dalle [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`) oppure eseguendo `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest`.

### Passo 1: creare una sola volta il volume `docker:dind` golden

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

# 1. Avvia un'istanza seed docker:dind basata su 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. Prepopola le immagini di base e la cache di build all'interno di 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. Arresta con garbo l'istanza seed affinché ext4lite svuoti e compatti l'immagine
kill -INT $SEED_PID
wait $SEED_PID
```

### Passo 2: avviare episodi RL paralleli in 100 millisecondi

Ogni worker di rollout parallelo clona `golden-docker.ext4` tramite reflink e avvia un'istanza `docker:dind` isolata, con tutte le immagini già presenti in `/var/lib/docker`:

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

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

# Avvia un episodio isolato non privilegiato con 4 GiB di RAM e 2 CPU
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

# Nel worker di rollout le immagini sono immediatamente disponibili, senza pull dalla rete
docker images

# Smantellamento dell'episodio dopo il calcolo della ricompensa (~10 ms)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Pool di rollout programmatico tramite l'SDK Go `z.gfire.dev/gvx/pkg/gvx`

Se il server dell'ambiente RL o l'harness di valutazione è scritto in Go (oppure chiama un daemon di rollout Go tramite gRPC), è possibile gestire episodi di container paralleli direttamente tramite `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 tramite reflink il volume 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. Applica il timeout di wall-clock per 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, // limite di 4 GiB
			CPUMillis:   2000,    // 2.0 CPU
		},
		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)
	}
}
```

---

## Confronto dei runtime sandbox per agenti RL

| Metrica / Requisito                                       | `dockerd` host condiviso          | `docker:dind` privilegiato | Firecracker / KVM MicroVM       | `gVisor` standard (`runsc`)        | `gvx` (`rungv` / `pkg/gvx`)                 |
| :-------------------------------------------------------- | :-------------------------------- | :------------------------- | :------------------------------ | :--------------------------------- | :------------------------------------------ |
| **`root` sull'host o `--privileged` richiesto**           | Sì (`docker.sock` = root)         | Sì (`--privileged`)        | Richiede `/dev/kvm` + setup `TAP` | Sì (o `userns` + `mount`)        | **No (singolo processo non privilegiato)**  |
| **Funziona in pod K8s non privilegiati / `runsc` esistenti** | No                             | No                         | No (nessun `/dev/kvm`)          | Fallisce (`EBUSY` su seccomp annidato) | **Sì (fallback automatico al loop futex)**  |
| **Supporta `docker build` e `docker compose` interni**    | Condivide il daemon dell'host     | Sì                         | Sì                              | Fallisce senza tuning di DinD sull'host | **Sì (`overlayfs` su `ext4lite`)**         |
| **Isolamento di rete per episodio**                       | `docker0` / lock `xtables` condivisi | Namespace separato      | NIC di VM separata              | Netns separato                     | **`netstack` isolato nel processo**         |
| **Latenza di reset dell'episodio a caldo (`env.reset()`)** | 5 s a 30 s (prune di container/volumi) | 10 s a 60 s (daemon a freddo) | ~150 ms (snapshot di VM)      | 5 s a 20 s                         | **~100 ms (`cp --reflink=always` di `.ext4`)** |
| **Sviluppo locale multipiattaforma (Windows / macOS / Linux)** | Richiede VM Linux            | Richiede VM Linux          | Solo Linux KVM                  | Solo Linux                         | **Nativo su Windows, macOS e Linux**        |

---

## Altri articoli di questa serie

1. **[gvx: Esecuzione di container Linux, Docker e Kubernetes in un unico processo non privilegiato](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Walkthrough architetturale completo di `gvx` su Windows (`whpx`, `systrap`), macOS (`hvf`) e Linux (`systrap`).
2. **[Esecuzione di Docker, docker build e docker compose senza root né user namespace](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Guida rapida di bootstrap in 3 passi e risoluzione dei problemi quando `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` e `/dev/fuse` sono bloccati.
3. **[Esecuzione di Docker-in-Docker all'interno di gVisor e di sandbox annidate con gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: Analisi dettagliata di perché `docker.io` e `runsc` annidato falliscono all'interno di `gVisor` (`runsc`) e di come `gvx` ricorre al loop futex `_RUN_SYSCALL_LOOP` in caso di `EBUSY`.
4. **[Ambienti Docker isolati e veloci per i rollout RL degli agenti AI con gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** (questo articolo)
