---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Snelle geïsoleerde Docker-omgevingen voor AI-agent RL-rollouts met gvx
description: Hoe hunderden parallelle, sterk geïsoleerde niet-geprivilegieerde Docker- en Kubernetes-omgevingen veilig kunnen worden uitgevoerd voor SWE-bench- en coding agent-reinforcement learning-rollouts, met 100-milliseconde copy-on-write ext4lite-volumereets.
language: nl
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
---

## Het beveiligings- en infrastructuurknelpunt in RL-rollouts van coderingsagenten

Het trainen en evalueren van softwareengineeringagenten over benchmarks en reinforcement learning (RL)-frameworks zoals **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** en **OpenHands** vereist het uitvoeren van duizenden gelijktijdige rollouts van **niet-vertrouwde, door LLM's gegenereerde code**. Bij echte softwareengineeringtaken bewerkt een agent niet alleen tekstbestanden: hij voert `docker build` uit, start databases en microservices op via `docker compose`, voert integratietestsuites uit en inspecteert containerlogs.

Op rolloutschaal (128 tot 1.024 gelijktijdige episodes per cluster) loopt bestaande containerinfrastructuur tegen vier systemische beveiligings- en prestatieknelpunten aan:

1. **Vergrendelingsconflicten en statuslekken in de host-`dockerd`**: Het richten van honderden gelijktijdige RL-workers op één host-`/var/run/docker.sock` geeft elke agent root-equivalente toegang tot de host-daemon, serialiseert het uitpakken van image-lagen, put de `docker0`-subnettoewijzingen uit, veroorzaakt de fout `Another app is currently holding the xtables lock` in host-`iptables` en laat verweesde containers achter over episodes heen.
2. **Ontsnappingsrisico bij privileged DinD-containers**: Standaard `docker:dind`-containers vereisen `--privileged` (`CAP_SYS_ADMIN`), waardoor ruwe host-blokapparaten, cgroup-release-agents en kernelmodules worden blootgesteld aan niet-vertrouwde RL-rollouts.
3. **Beperkingen van hardwarevirtualisatie (`/dev/kvm`)**: MicroVM-runtimes (`Firecracker`, `QEMU`, `Kata Containers`) isoleren interne kernels netjes, maar vereisen `/dev/kvm`. Standaard Kubernetes-pods zonder geneste virtualisatie, bare-metal cloudinstanties waarop KVM is uitgeschakeld, en bestaande `gVisor` (`runsc`)-sandboxes kunnen geen KVM-microVM's starten.
4. **Trage episodereset (`env.reset()`)-latentie**: Het starten van een nieuwe `docker:dind`-omgeving en het ophalen of uitpakken van basisimages voor elke RL-episode duurt 10 tot 60 seconden, waardoor GPU-trainingsworkers inactief blijven terwijl ze op omgevingsresets wachten.

**[gvx](https://github.com/gfire-sigs/gvx)** (`rungv` CLI en `z.gfire.dev/gvx/pkg/gvx` Go SDK) biedt een **veilige, defense-in-depth rolloutruntime** die alle vier knelpunten oplost. Elke rolloutomgeving draait als **één onprivileged, zelfvergrendelend user-space proces** (geïsoleerd door Google's gVisor `Sentry`-kernel, `pkg/hostlockdown` en een in-memory `netstack`), waarvan de volledige `/var/lib/docker`-status zich bevindt in **één copy-on-write `.ext4`-bestand (`ext4lite`)**.

> **Zero-Privilege-beveiliging voor niet-vertrouwde agentrollouts**: Elke `gvx`-episode vergrendelt zijn eigen host-OS-proces voordat gastcode wordt uitgevoerd (`PR_SET_NO_NEW_PRIVS` + BPF `seccomp` op Linux, Seatbelt `deny default` op macOS, Restricted Token + Job Object op Windows). Ook als een RL-agent `docker run --privileged` of destructieve `rm -rf /`-commando's binnen zijn episode uitvoert, blijven deze bewerkingen strikt beperkt tot de user-space `Sentry`-kernel en het vluchtige `.ext4`-volumebestand van die episode.

---

## Rolloutarchitectuur: parallelle onprivileged instanties + reflink-snapshots

In `gvx` is elke rolloutworker een gewoon onprivileged OS-proces met zijn eigen gVisor `Sentry`-kernel, zijn eigen user-space `netstack` (`10.0.2.15/24`) en zijn eigen `ext4lite`-blokvolume (`ep-N.ext4`), terwijl hij een alleen-lezen, content-adresseerbare `erofs`-OCI-imagecache deelt via de page cache van de host.

![Parallelle agent-RL-rolloutarchitectuur met gvx](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Waarom enkelbestands-`ext4lite`-volumes de RL-resetkosten veranderen

Omdat het `ext4lite`-bestandssysteem van `gvx` een volledig JBD2-gejournaled `ext4`-volume opslaat (inclusief de `overlayfs`-laagdirectories van Docker, `trusted.overlay.*`-extended attributes, whiteout-apparaatknooppunten en image-metadata) in één regulier hostbestand, wordt het beheer van de episodecyclus een enkele bestandsbewerking:

1. **100 milliseconden `env.reset()` via `cp --reflink=always`**:
   - Je bouwt één keer een `golden-docker.ext4`-bestand met vooraf opgehaalde basisimages (`python:3.11`, `postgres:16`, `redis:7` of taakspecifieke `SWE-bench`-basisimages).
   - Aan het begin van elke RL-episode maakt `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (op XFS, Btrfs, ZFS of macOS APFS) binnen 100 milliseconden een onafhankelijke Copy-on-Write-kloon, zonder de onderliggende schijfblokken te kopiëren.
   - Alleen de 4 KiB-blokken die tijdens die specifieke episode zijn gewijzigd, nemen extra schijfruimte in beslag.
2. **Teardown zonder residu (`env.close()`)**:
   - Het beëindigen van het `rungv`-proces (`sb.Close()` in Go) en het verwijderen van de link naar `ep-0042.ext4` geeft in ongeveer 10 milliseconden 100% van de processen, netwerksockets, mounts en opslag van de episode vrij. Er wordt nooit iets gemount in de mounttabel van de host-kernel of geregistreerd in host-cgroups.

---

## Stap-voor-stap-workflow: golden `.ext4`-snapshot en snelle episodereset

Je kunt `rungv` op elk rolloutworkerknooppunt installeren, door ofwel het SLSA-geattesteerde vooraf gebouwde binaire bestand te downloaden van [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`), of door `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest` uit te voeren.

### Stap 1: maak het golden `docker:dind`-volume eenmalig aan

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

# 1. Start een seed-instantie van docker:dind met golden-docker.ext4 als backend
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. Vul basisimages en buildcache vooraf in 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. Stop de seed-instantie netjes zodat ext4lite de image wegschrijft en comprimeert
kill -INT $SEED_PID
wait $SEED_PID
```

### Stap 2: start parallelle RL-episodes in 100 milliseconden

Elke parallelle rolloutworker kloont `golden-docker.ext4` via reflink en start een geïsoleerde `docker:dind`-instantie waarin alle images al aanwezig zijn in `/var/lib/docker`:

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

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

# Start een geïsoleerde episode zonder privileges met 4 GiB RAM en 2 CPU's
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

# Binnen de rollout-worker zijn images direct beschikbaar zonder netwerkpull
docker images

# Opruimen van de episode na reward-berekening (~10 ms)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Programmatische rolloutpool met de `z.gfire.dev/gvx/pkg/gvx` Go SDK

Als je RL-omgevingsserver of evaluatieharnas is geschreven in Go (of een Go-rolloutdaemon aanroept via gRPC), kun je parallelle containerepisodes rechtstreeks beheren 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. Maak een reflink-kloon van het golden ext4lite-volume
	cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
	if err := cpCmd.Run(); err != nil {
		return fmt.Errorf("clone golden volume: %w", err)
	}

	// 2. Pas een wandkloktimeout per episode toe
	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, // Limiet van 4 GiB
			CPUMillis:   2000,    // 2,0 CPU's
		},
		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)
	}
}
```

---

## Vergelijking van runtimes voor RL-sandboxes van agenten

| Metriek / Vereiste                                        | Gedeelde host-`dockerd`           | Privileged `docker:dind`  | Firecracker / KVM MicroVM       | Standaard `gVisor` (`runsc`)        | `gvx` (`rungv` / `pkg/gvx`)                  |
| :-------------------------------------------------------- | :-------------------------------- | :------------------------ | :------------------------------ | :---------------------------------- | :------------------------------------------- |
| **Host-`root` of `--privileged` vereist**                 | Ja (`docker.sock` = root)         | Ja (`--privileged`)       | Vereist `/dev/kvm` + `TAP`-setup | Ja (of `userns` + `mount`)          | **Nee (één onprivileged proces)**            |
| **Werkt in onprivileged K8s-pods / bestaande `runsc`**    | Nee                               | Nee                       | Nee (geen `/dev/kvm`)           | Mislukt (`EBUSY` bij geneste seccomp) | **Ja (automatische futex-loop-fallback)**    |
| **Ondersteunt geneste `docker build` & `docker compose`** | Deelt host-daemon                 | Ja                        | Ja                              | Mislukt zonder host-DinD-afstelling | **Ja (`overlayfs` op `ext4lite`)**           |
| **Netwerkisolatie per episode**                           | Gedeelde `docker0` / `xtables`-lock | Aparte namespace         | Aparte VM-NIC                   | Aparte netns                        | **Geïsoleerde in-process `netstack`**        |
| **Latentie bij warme episodereset (`env.reset()`)**       | 5s tot 30s (container/volume prune) | 10s tot 60s (koude daemon) | ~150 ms (VM-snapshot)           | 5s tot 20s                          | **~100 ms (`cp --reflink=always` `.ext4`)**  |
| **Lokale ontwikkeling op meerdere platforms (Windows / macOS / Linux)** | Vereist Linux-VM | Vereist Linux-VM      | Alleen Linux KVM                | Alleen Linux                        | **Native op Windows, macOS en Linux**        |

---

## Verder lezen in deze reeks

1. **[gvx: Linux-containers, Docker en Kubernetes draaien in één onprivileged proces](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Volledige architectuurrondleiding van `gvx` op Windows (`whpx`, `systrap`), macOS (`hvf`) en Linux (`systrap`).
2. **[Docker, docker build en docker compose draaien zonder root of user namespaces](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Snelle bootstrapgids in 3 stappen met probleemoplossing voor het geval `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` en `/dev/fuse` geblokkeerd zijn.
3. **[Docker-in-Docker draaien in gVisor en geneste sandboxes met gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: Diepgaande analyse van waarom `docker.io` en geneste `runsc` mislukken binnen `gVisor` (`runsc`) en hoe `gvx` terugvalt op de `_RUN_SYSCALL_LOOP` futex-loop bij `EBUSY`.
4. **[Snelle geïsoleerde Docker-omgevingen voor RL-rollouts van AI-agenten met gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** (dit bericht)
