---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Schnelle isolierte Docker-Umgebungen für AI-Agenten-RL-Rollouts mit gvx
description: Wie man hunderte parallele, stark isolierte, unprivilegierte Docker- und Kubernetes-Umgebungen für SWE-bench- und Coding-Agent-Reinforcement-Learning-Rollouts sicher betreibt, mit 100-Millisekunden-Copy-on-Write-ext4lite-Volume-Resets.
language: de
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
---

## Der Sicherheits- und Infrastrukturengpass bei Coding-Agent-RL-Rollouts

Das Training und die Evaluierung von Software-Engineering-Agenten über Benchmarks und Reinforcement-Learning-(RL)-Frameworks wie **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** und **OpenHands** erfordern die Ausführung Tausender gleichzeitiger Rollouts von **nicht vertrauenswürdigem, durch LLMs generiertem Code**. Bei realen Software-Engineering-Aufgaben bearbeitet ein Agent nicht lediglich Textdateien: Er führt `docker build` aus, startet Datenbanken und Microservices über `docker compose`, führt Integrationstestsuiten aus und untersucht Container-Logs.

Im Rollout-Maßstab (128 bis 1.024 gleichzeitige Episoden pro Cluster) stößt die bestehende Container-Infrastruktur auf vier systemische Sicherheits- und Leistungsengpässe:

1. **Sperrkonflikte und Zustandslecks bei `dockerd` auf dem Host**: Wenn Hunderte gleichzeitiger RL-Worker auf einen einzigen Host-`/var/run/docker.sock` zugreifen, erhält jeder Agent root-äquivalenten Zugriff auf den Host-Daemon, die Extraktion der Image-Layer wird serialisiert, der Subnetz-Adresspool von `docker0` erschöpft sich, in den Host-`iptables` treten Fehler vom Typ `Another app is currently holding the xtables lock` auf, und verwaiste Container lecken über Episoden hinweg.
2. **Container-Escape-Risiko durch privilegiertes DinD**: Der Betrieb standardmäßiger `docker:dind`-Container erfordert `--privileged` (`CAP_SYS_ADMIN`), wodurch rohe Host-Blockgeräte, cgroup-Release-Agents und Kernelmodule für nicht vertrauenswürdige RL-Rollouts offengelegt werden.
3. **Einschränkungen der Hardwarevirtualisierung (`/dev/kvm`)**: MicroVM-Laufzeitumgebungen (`Firecracker`, `QEMU`, `Kata Containers`) isolieren innere Kernel sauber, benötigen jedoch `/dev/kvm`. Standard-Kubernetes-Pods ohne verschachtelte Virtualisierung, Bare-Metal-Cloud-Instanzen, auf denen KVM deaktiviert ist, und bestehende `gVisor`-(`runsc`)-Sandboxes können keine KVM-MicroVMs starten.
4. **Hohe Latenz beim Zurücksetzen der Episode (`env.reset()`)**: Das Starten einer neuen `docker:dind`-Umgebung sowie das Abrufen und Entpacken von Basis-Images für jede RL-Episode dauert 10 bis 60 Sekunden, sodass GPU-Trainingsworker untätig bleiben, während sie auf das Zurücksetzen der Umgebungen warten.

**[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`-CLI und `z.gfire.dev/gvx/pkg/gvx` Go-SDK) stellt eine **sichere Rollout-Laufzeitumgebung mit mehrschichtiger Verteidigung (defense-in-depth)** bereit, die alle vier Engpässe löst. Jede Rollout-Umgebung läuft als **einziger, unprivilegierter, selbst gesperrter User-Space-Prozess** (isoliert durch den gVisor-`Sentry`-Kernel von Google, `pkg/hostlockdown` und einen speicherinternen `netstack`), dessen gesamter `/var/lib/docker`-Zustand in **einer einzigen Copy-on-Write-`.ext4`-Datei (`ext4lite`)** liegt.

> **Null-Privilegien-Sicherheit für nicht vertrauenswürdige Agent-Rollouts**: Jede `gvx`-Episode sperrt ihren eigenen Host-OS-Prozess, bevor sie Gastcode ausführt (`PR_SET_NO_NEW_PRIVS` + BPF-`seccomp` unter Linux, Seatbelt `deny default` unter macOS, Restricted Token + Job Object unter Windows). Selbst wenn ein RL-Agent innerhalb seiner Episode `docker run --privileged` oder destruktive `rm -rf /`-Befehle ausführt, bleiben diese Operationen strikt auf den User-Space-`Sentry`-Kernel dieser Episode und ihre flüchtige `.ext4`-Volume-Datei beschränkt.

---

## Rollout-Architektur: Parallele unprivilegierte Instanzen + Reflink-Snapshots

In `gvx` ist jeder Rollout-Worker ein gewöhnlicher unprivilegierter OS-Prozess mit eigenem gVisor-`Sentry`-Kernel, eigenem User-Space-`netstack` (`10.0.2.15/24`) und eigenem `ext4lite`-Blockvolume (`ep-N.ext4`). Alle Worker teilen sich dabei einen schreibgeschützten, inhaltsadressierbaren `erofs`-OCI-Image-Cache im Host-Page-Cache.

![Architektur paralleler Agent-RL-Rollouts mit gvx](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Warum einzelne `ext4lite`-Volumes die Ökonomie des RL-Resets verändern

Da das `ext4lite`-Dateisystem ein vollständiges JBD2-journalisiertes `ext4`-Volume (einschließlich der `overlayfs`-Layer-Verzeichnisse von Docker, der erweiterten Attribute `trusted.overlay.*`, der Whiteout-Gerätknoten und der Image-Metadaten) in einer einzigen regulären Host-Datei speichert, wird die Verwaltung des Episodenlebenszyklus zu einer einzigen Dateioperation:

1. **100-Millisekunden-`env.reset()` über `cp --reflink=always`**:
   - Sie erstellen einmalig eine Datei `golden-docker.ext4`, die vorab abgerufene Basis-Images enthält (`python:3.11`, `postgres:16`, `redis:7` oder aufgabenspezifische `SWE-bench`-Basis-Images).
   - Zu Beginn jeder RL-Episode erzeugt `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (auf XFS, Btrfs, ZFS oder macOS APFS) einen unabhängigen Copy-on-Write-Klon in weniger als 100 Millisekunden, ohne die zugrunde liegenden Plattenblöcke zu kopieren.
   - Nur die während dieser spezifischen Episode geänderten 4-KiB-Blöcke beanspruchen zusätzlichen Speicherplatz.
2. **Rückstandsfreier Abbau (`env.close()`)**:
   - Das Beenden des `rungv`-Prozesses (`sb.Close()` in Go) und das Entfernen von `ep-0042.ext4` gibt 100 % der Prozesse, Netzwerk-Sockets, Mounts und Speicherressourcen der Episode in etwa 10 Millisekunden frei. Nichts wird jemals in der Mount-Tabelle des Host-Kernels eingetragen oder in den cgroups des Hosts registriert.

---

## Schritt-für-Schritt-Workflow: Goldener `.ext4`-Snapshot und schnelles Zurücksetzen der Episode

Sie können `rungv` auf jedem Rollout-Worker-Knoten entweder durch Herunterladen des SLSA-attestierten, vorkompilierten Binärpakets aus den [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`) oder durch Ausführen von `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest` installieren.

### Schritt 1: Das goldene `docker:dind`-Volume einmalig erstellen

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

# 1. Eine Seed-docker:dind-Instanz starten, die von golden-docker.ext4 gestützt wird
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. Basis-Images und Build-Cache innerhalb von golden-docker.ext4 vorab befüllen
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. Die Seed-Instanz kontrolliert beenden, damit ext4lite die Puffer leert und das Image komprimiert
kill -INT $SEED_PID
wait $SEED_PID
```

### Schritt 2: Parallele RL-Episoden in 100 Millisekunden starten

Jeder parallele Rollout-Worker klont `golden-docker.ext4` per Reflink und startet eine isolierte `docker:dind`-Instanz, in der alle Images bereits in `/var/lib/docker` vorhanden sind:

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

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

# Isolierte unprivilegierte Episode mit 4 GiB RAM und 2 CPUs starten
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

# Innerhalb des Rollout-Workers sind Images sofort ohne Netzwerk-Pull verfügbar
docker images

# Abbau der Episode nach der Berechnung der Belohnung (~10 ms)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Programmatischer Rollout-Pool mit dem `z.gfire.dev/gvx/pkg/gvx` Go-SDK

Wenn Ihr RL-Umgebungsserver oder Ihr Evaluierungsharness in Go geschrieben ist (oder einen Go-Rollout-Daemon über gRPC aufruft), können Sie parallele Container-Episoden direkt über `z.gfire.dev/gvx/pkg/gvx` verwalten:

```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. Golden-ext4lite-Volume per Reflink klonen
	cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
	if err := cpCmd.Run(); err != nil {
		return fmt.Errorf("clone golden volume: %w", err)
	}

	// 2. Ein Wanduhr-Zeitlimit pro Episode anwenden
	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, // 4 GiB Obergrenze
			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)
	}
}
```

---

## Vergleich von Laufzeitumgebungen für Agent-RL-Sandboxes

| Metrik / Anforderung                                      | Gemeinsamer Host-`dockerd`        | Privilegiertes `docker:dind` | Firecracker / KVM-MicroVM        | Standard-`gVisor` (`runsc`)           | `gvx` (`rungv` / `pkg/gvx`)                 |
| :-------------------------------------------------------- | :-------------------------------- | :--------------------------- | :------------------------------- | :------------------------------------ | :------------------------------------------ |
| **Host-`root` oder `--privileged` erforderlich**          | Ja (`docker.sock` = root)         | Ja (`--privileged`)          | Benötigt `/dev/kvm` + `TAP`-Setup | Ja (oder `userns` + `mount`)          | **Nein (einzelner unprivilegierter Prozess)** |
| **Funktioniert in unprivilegierten K8s-Pods / bestehenden `runsc`** | Nein                    | Nein                         | Nein (kein `/dev/kvm`)           | Schlägt fehl (`EBUSY` bei verschachteltem seccomp) | **Ja (automatischer Fallback auf futex-Schleife)** |
| **Unterstützt inneres `docker build` und `docker compose`** | Teilt den Host-Daemon             | Ja                           | Ja                               | Schlägt ohne Host-DinD-Feinabstimmung fehl | **Ja (`overlayfs` auf `ext4lite`)**     |
| **Netzwerkisolation pro Episode**                         | Gemeinsames `docker0` / `xtables`-Lock | Separater Namespace      | Separate VM-NIC                  | Separater netns                       | **Isoliertes In-Prozess-`netstack`**        |
| **Latenz beim Zurücksetzen der Warm-Episode (`env.reset()`)** | 5 s bis 30 s (Container-/Volume-Bereinigung) | 10 s bis 60 s (kalter Daemon) | ~150 ms (VM-Snapshot)       | 5 s bis 20 s                          | **~100 ms (`cp --reflink=always` `.ext4`)** |
| **Plattformübergreifende lokale Entwicklung (Windows / macOS / Linux)** | Erfordert Linux-VM      | Erfordert Linux-VM           | Nur Linux-KVM                    | Nur Linux                             | **Nativ unter Windows, macOS und Linux**    |

---

## Weitere Lektüre in dieser Reihe

1. **[gvx: Linux-Container, Docker und Kubernetes in einem unprivilegierten Prozess ausführen](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Vollständiger architektonischer Überblick über `gvx` unter Windows (`whpx`, `systrap`), macOS (`hvf`) und Linux (`systrap`).
2. **[Docker, docker build und docker compose ohne Root oder User Namespaces ausführen](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Kurze Anleitung in drei Schritten zur Einrichtung und Fehlerbehebung, wenn `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` und `/dev/fuse` blockiert sind.
3. **[Docker-in-Docker innerhalb von gVisor und verschachtelten Sandboxes mit gvx ausführen](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: Detaillierte Analyse, warum `docker.io` und verschachteltes `runsc` innerhalb von `gVisor` (`runsc`) fehlschlagen und wie `gvx` bei `EBUSY` auf die futex-Schleife `_RUN_SYSCALL_LOOP` zurückfällt.
4. **[Schnelle isolierte Docker-Umgebungen für KI-Agent-RL-Rollouts mit gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** (dieser Beitrag)
