---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Environnements Docker isolés et rapides pour les déploiements RL d'agents IA avec gvx
description: Comment exécuter de manière sécurisée des centaines d'environnements Docker et Kubernetes parallèles, fortement isolés et non privilégiés pour SWE-bench et les déploiements (rollouts) d'apprentissage par renforcement d'agents de codage, avec des réinitialisations de volumes ext4lite en copie sur écriture (copy-on-write) d'une durée de 100 millisecondes.
language: fr
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
---

## Le goulot d'étranglement de sécurité et d'infrastructure dans les rollouts RL des agents de codage

L'entraînement et l'évaluation d'agents d'ingénierie logicielle sur des benchmarks et des frameworks d'apprentissage par renforcement (RL) tels que **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** et **OpenHands** exigent l'exécution de milliers de rollouts concurrents de **code non fiable généré par des LLM**. Dans les tâches d'ingénierie logicielle réelles, un agent ne se contente pas de modifier des fichiers texte : il exécute `docker build`, démarre des bases de données et des microservices via `docker compose`, lance des suites de tests d'intégration et inspecte les journaux des conteneurs.

À l'échelle des rollouts (128 à 1 024 épisodes concurrents par cluster), l'infrastructure de conteneurs existante se heurte à quatre goulots d'étranglement systémiques en matière de sécurité et de performance :

1. **Contention de verrou sur le `dockerd` hôte et fuite d'état** : faire pointer des centaines de workers RL concurrents vers un unique `/var/run/docker.sock` de l'hôte accorde à chaque agent un accès équivalent à root sur le démon hôte, sérialise l'extraction des couches d'images, épuise les allocations de sous-réseau de `docker0`, déclenche des erreurs `Another app is currently holding the xtables lock` dans l'`iptables` de l'hôte, et laisse des conteneurs orphelins fuir entre les épisodes.
2. **Risque d'évasion de conteneur via DinD privilégié** : l'exécution de conteneurs `docker:dind` standard requiert `--privileged` (`CAP_SYS_ADMIN`), ce qui expose aux rollouts RL non fiables les périphériques blocs bruts de l'hôte, les release agents de cgroups et les modules noyau.
3. **Contraintes de virtualisation matérielle (`/dev/kvm`)** : les runtimes microVM (`Firecracker`, `QEMU`, `Kata Containers`) isolent proprement le noyau interne, mais ils requièrent `/dev/kvm`. Les pods Kubernetes standard sans virtualisation imbriquée, les instances cloud bare-metal où KVM est désactivé, et les sandboxes `gVisor` existantes (`runsc`) ne peuvent pas lancer de microVM KVM.
4. **Latence de la réinitialisation d'épisode (`env.reset()`)** : démarrer un environnement `docker:dind` neuf, puis télécharger ou décompresser les images de base pour chaque épisode RL, prend 10 à 60 secondes, laissant les workers d'entraînement GPU inactifs pendant l'attente de la réinitialisation des environnements.

**[gvx](https://github.com/gfire-sigs/gvx)** (CLI `rungv` et SDK Go `z.gfire.dev/gvx/pkg/gvx`) fournit un **runtime de rollout sécurisé et protégé en profondeur**, qui résout les quatre goulots d'étranglement. Chaque environnement de rollout s'exécute comme **un unique processus utilisateur non privilégié et auto-verrouillé** (isolé par le noyau `Sentry` de gVisor de Google, par `pkg/hostlockdown` et par un `netstack` en mémoire), dont l'intégralité de l'état `/var/lib/docker` réside dans **un unique fichier `.ext4` en copy-on-write (`ext4lite`)**.

> **Sécurité zéro privilège pour les rollouts d'agents non fiables** : chaque épisode `gvx` verrouille son propre processus système hôte avant d'exécuter le code invité (`PR_SET_NO_NEW_PRIVS` + `seccomp` BPF sous Linux, `deny default` Seatbelt sous macOS, Restricted Token + Job Object sous Windows). Même si un agent RL exécute `docker run --privileged` ou des commandes destructrices `rm -rf /` au sein de son épisode, ces opérations restent strictement confinées au noyau `Sentry` en espace utilisateur de cet épisode et à son fichier de volume `.ext4` éphémère.

---

## Architecture de rollout : instances parallèles non privilégiées + snapshots reflink

Dans `gvx`, chaque worker de rollout est un processus système ordinaire non privilégié, doté de son propre noyau gVisor `Sentry`, de son propre `netstack` en espace utilisateur (`10.0.2.15/24`) et de son propre volume bloc `ext4lite` (`ep-N.ext4`), tout en partageant un cache d'images OCI `erofs` en lecture seule, adressé par contenu, à travers le cache de pages de l'hôte.

![Architecture de rollout RL parallèle d'agents avec gvx](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Pourquoi les volumes `ext4lite` à fichier unique changent l'économie de la réinitialisation RL

Comme le système de fichiers `ext4lite` de `gvx` stocke un volume `ext4` complet avec journal JBD2 (incluant les répertoires de couches `overlayfs` de Docker, les attributs étendus `trusted.overlay.*`, les nœuds de périphériques whiteout et les métadonnées d'images) dans un unique fichier régulier de l'hôte, la gestion du cycle de vie d'un épisode se réduit à une seule opération sur fichier :

1. **`env.reset()` de 100 millisecondes via `cp --reflink=always`** :
   - Vous construisez une fois un fichier `golden-docker.ext4` contenant des images de base préchargées (`python:3.11`, `postgres:16`, `redis:7`, ou les images de base spécifiques aux tâches `SWE-bench`).
   - Au démarrage de chaque épisode RL, `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (sur XFS, Btrfs, ZFS ou macOS APFS) crée un clone copy-on-write indépendant en moins de 100 millisecondes, sans copier les blocs disque sous-jacents.
   - Seuls les blocs de 4 KiB modifiés pendant cet épisode précis consomment de l'espace disque supplémentaire.
2. **Démantèlement sans résidu (`env.close()`)** :
   - Terminer le processus `rungv` (`sb.Close()` en Go) et supprimer le lien de `ep-0042.ext4` récupère 100 % des processus, sockets réseau, points de montage et stockage de l'épisode en environ 10 millisecondes. Rien n'est jamais monté dans la table de montage du noyau hôte ni enregistré dans les cgroups de l'hôte.

---

## Procédure pas à pas : snapshot `.ext4` doré et réinitialisation rapide d'épisode

Vous pouvez installer `rungv` sur chaque nœud worker de rollout soit en téléchargeant le binaire précompilé attesté SLSA depuis [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`), soit en exécutant `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest`.

### Étape 1 : Créer une seule fois le volume `docker:dind` doré

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

# 1. Démarrer une instance docker:dind d'amorçage adossée à 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éremplir les images de base et le cache de build dans 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. Arrêter proprement l'instance d'amorçage afin qu'ext4lite vide ses tampons et compacte l'image
kill -INT $SEED_PID
wait $SEED_PID
```

### Étape 2 : Lancer des épisodes RL parallèles en 100 millisecondes

Chaque worker de rollout parallèle clone `golden-docker.ext4` par reflink et démarre une instance `docker:dind` isolée dont toutes les images sont déjà présentes dans `/var/lib/docker` :

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

# Clone copy-on-write instantané (~100 ms sur XFS/Btrfs/APFS)
cp --reflink=always \
  /srv/rl-snapshots/golden-docker.ext4 \
  "/srv/rl-episodes/${EPISODE_ID}.ext4"

# Lancer l'épisode isolé non privilégié avec 4 GiB de RAM et 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

# Dans le worker de rollout, les images sont immédiatement disponibles sans téléchargement réseau
docker images

# Démantèlement de l'épisode après le calcul de la récompense (~10 ms)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Pool de rollouts programmatique avec le SDK Go `z.gfire.dev/gvx/pkg/gvx`

Si votre serveur d'environnements RL ou votre harnais d'évaluation est écrit en Go (ou appelle un démon de rollout Go via gRPC), vous pouvez gérer des épisodes de conteneurs parallèles directement via `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. Cloner par reflink le volume ext4lite doré
	cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
	if err := cpCmd.Run(); err != nil {
		return fmt.Errorf("clone golden volume: %w", err)
	}

	// 2. Appliquer un délai d'expiration en temps réel par épisode
	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, // Plafond de 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)
	}
}
```

---

## Comparaison des runtimes de sandbox pour agents RL

| Métrique / Exigence                                       | `dockerd` hôte partagé            | `docker:dind` privilégié | Firecracker / microVM KVM      | `gVisor` standard (`runsc`)       | `gvx` (`rungv` / `pkg/gvx`)                 |
| :-------------------------------------------------------- | :-------------------------------- | :----------------------- | :----------------------------- | :-------------------------------- | :------------------------------------------ |
| **`root` de l'hôte ou `--privileged` requis**             | Oui (`docker.sock` = root)        | Oui (`--privileged`)     | Nécessite `/dev/kvm` + config `TAP` | Oui (ou `userns` + `mount`)       | **Non (processus unique non privilégié)**   |
| **Fonctionne dans des Pods K8s non privilégiés / `runsc` existant** | Non                     | Non                      | Non (pas de `/dev/kvm`)        | Échec (`EBUSY` sur seccomp imbriqué) | **Oui (repli automatique sur la boucle futex)** |
| **Prend en charge `docker build` et `docker compose` internes** | Partage le démon hôte          | Oui                      | Oui                            | Échec sans réglage DinD hôte      | **Oui (`overlayfs` sur `ext4lite`)**        |
| **Isolation réseau par épisode**                          | `docker0` / verrou `xtables` partagé | Espace de noms séparé | NIC de VM séparée              | Netns séparé                      | **Isolé dans un `netstack` interne au processus** |
| **Latence de réinitialisation d'épisode à chaud (`env.reset()`)** | 5 s à 30 s (purge conteneur/volume) | 10 s à 60 s (démon à froid) | ~150 ms (snapshot de VM)  | 5 s à 20 s                        | **~100 ms (`cp --reflink=always` de `.ext4`)** |
| **Développement local multiplateforme (Windows / macOS / Linux)** | Nécessite une VM Linux    | Nécessite une VM Linux   | KVM Linux uniquement           | Linux uniquement                  | **Natif sous Windows, macOS et Linux**      |

---

## Autres lectures dans cette série

1. **[gvx : exécuter des conteneurs Linux, Docker et Kubernetes dans un seul processus non privilégié](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)** : parcours architectural complet de `gvx` sous Windows (`whpx`, `systrap`), macOS (`hvf`) et Linux (`systrap`).
2. **[Exécuter Docker, docker build et docker compose sans root ni espaces de noms utilisateur](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)** : guide d'amorçage en 3 étapes et de dépannage rapide lorsque `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` et `/dev/fuse` sont bloqués.
3. **[Exécuter Docker-in-Docker dans gVisor et dans des sandboxes imbriqués avec gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** : analyse approfondie des raisons pour lesquelles `docker.io` et `runsc` imbriqué échouent à l'intérieur de `gVisor` (`runsc`), et de la manière dont `gvx` bascule sur la boucle futex `_RUN_SYSCALL_LOOP` en cas d'`EBUSY`.
4. **[Environnements Docker isolés et rapides pour les rollouts RL d'agents IA avec gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** (cet article)
