Snelle geïsoleerde Docker-omgevingen voor AI-agent RL-rollouts met gvx

Het beveiligings- en infrastructuurknelpunt in Coding Agent RL Rollouts
Het trainen en evalueren van software engineering agents 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 onbetrouwbare, door LLM gegenereerde code. In praktijkgerichte softwareontwikkelingstaken 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:
- Vergrendelingsconcurrentie en statuslekkage van de host-
dockerd: Het richten van honderden gelijktijdige RL-workers op één host/var/run/docker.sockgeeft elke agent root-equivalente toegang tot de host-daemon, serialiseert het extraheren van imagelagen, put dedocker0-subnetallocaties uit, veroorzaakt foutenAnother app is currently holding the xtables lockin de host-iptables, en laat verweesde containers achter tussen episodes. - Risico op containerontsnapping via geprivilegieerde DinD: Het draaien van standaard
docker:dind-containers vereist--privileged(CAP_SYS_ADMIN), wat ruwe host-blokapparaten, cgroup release agents en kernelmodules blootstelt aan onbetrouwbare RL-rollouts. - Beperkingen van hardwarevirtualisatie (
/dev/kvm): MicroVM-runtimes (Firecracker,QEMU,Kata Containers) isoleren interne kernels op een zuivere manier, maar zij vereisen/dev/kvm. Standaard Kubernetes-pods zonder geneste virtualisatie, bare-metal-cloudinstanties waarop KVM is uitgeschakeld, en bestaandegVisor(runsc)-sandboxes kunnen geen KVM-microVMs starten. - Trage episode-reset (
env.reset())-latentie: Het starten van een nieuwedocker:dind-omgeving en het ophalen of uitpakken van basisimages voor elke RL-episode duurt 10 tot 60 seconden, waardoor GPU-trainingworkers stilvallen terwijl ze op resets van de omgeving wachten.
gvx (rungv-CLI en z.gfire.dev/gvx/pkg/gvx Go SDK) biedt een veilige, weinig inspanning vergende, tokenefficiënte rollout-runtime die alle vier de knelpunten oplost. Elke rollout-omgeving draait als één niet-geprivilegieerd, 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).
Weinig inspanning, tokenefficiënte uitvoering voor agents: Wanneer
docker buildofdocker composebinnen elke episode direct werkt (of door de agent in één beurt van drie commando's kan worden geboot viarungv), verspillen RL-rollouts geen contexttokens of stapbudgetten aan het oplossen vanservice docker start-ulimit-fouten,iptables-nft-fouten ofruncpivot_root-weigeringen. Elk token in het traject gaat naar het oplossen van de daadwerkelijke softwareontwikkelingstaak.
Zero-privilege beveiliging voor onbetrouwbare agent-rollouts: Elke
gvx-episode vergrendelt zijn eigen host-OS-proces voordat gastcode wordt uitgevoerd (PR_SET_NO_NEW_PRIVS+ BPFseccompop Linux, Seatbeltdeny defaultop macOS, Restricted Token + Job Object op Windows). Zelfs als een RL-agentdocker run --privilegedof destructieverm -rf /-commando's binnen zijn episode uitvoert, blijven die bewerkingen strikt beperkt tot de user-spaceSentry-kernel en het tijdelijke.ext4-volumebestand van die episode.
Rollout-architectuur: parallelle niet-geprivilegieerde instanties + reflink-snapshots
In gvx is elke rollout-worker een gewoon niet-geprivilegieerd 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-addressable erofs OCI-imagecache deelt via de page cache van de host.

Waarom enkelvoudige ext4lite-volumes de resetkosten van RL veranderen
Omdat het ext4lite-bestandssysteem een volledig JBD2-gejournald ext4-volume opslaat (inclusief de overlayfs-laagmappen van Docker, trusted.overlay.*-uitgebreide attributen, whiteout-apparaatknooppunten en imagemetadata) in één regulier hostbestand, wordt het beheer van de episodelevenscyclus één bestandsbewerking:
- 100 milliseconden
env.reset()viacp --reflink=always:- Je bouwt één keer een
golden-docker.ext4-bestand met vooraf gepulde basisimages (python:3.11,postgres:16,redis:7, of taakspecifiekeSWE-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 minder dan 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.
- Je bouwt één keer een
- Teardown zonder residu (
env.close()):- Het beëindigen van het
rungv-proces (sb.Close()in Go) en het loskoppelen vanep-0042.ext4geeft binnen ~10 milliseconden 100% van de processen, netwerksockets, mounts en opslag van de episode vrij. Niets wordt ooit gemount in de mounttabel van de host-kernel of geregistreerd in host-cgroups.
- Het beëindigen van het
Stapsgewijze werkwijze: golden .ext4-snapshot en snelle episode-reset
Je kunt rungv op elk rollout-workerknooppunt installeren, hetzij door de SLSA-geattesteerde vooraf gebouwde binary te downloaden van 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), hetzij 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
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. Start een seed-docker:dind-instantie met golden-docker.ext4 als back-end
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. Vul basisimages en buildcache vooraf in 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. Stop de seed-instantie op een nette manier zodat ext4lite de inhoud wegschrijft en comprimeert
20kill -INT $SEED_PID
21wait $SEED_PID
Stap 2: Start parallelle RL-episodes in 100 milliseconden
Elke parallelle rollout-worker kloont golden-docker.ext4 via reflink en start een geïsoleerde docker:dind-instantie met alle images al aanwezig in /var/lib/docker:
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# Onmiddellijke Copy-on-Write-kloon (~100 ms op XFS/Btrfs/APFS)
5cp --reflink=always \
6 /srv/rl-snapshots/golden-docker.ext4 \
7 "/srv/rl-episodes/${EPISODE_ID}.ext4"
8
9# Start een geïsoleerde niet-geprivilegieerde episode met 4 GiB RAM en 2 CPU's
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# Binnen de rollout-worker zijn images direct beschikbaar zonder netwerkpull
22docker images
23
24# Teardown van de episode na reward-berekening (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
Programmatische rollout-pool met de z.gfire.dev/gvx/pkg/gvx Go SDK
Als je RL-omgevingsserver of evaluatieharnas in Go is geschreven (of een Go-rollout-daemon via gRPC aanroept), kun je parallelle containerepisodes rechtstreeks beheren via 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. Reflink-kloon het golden ext4lite-volume
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. Pas een wall-clock-timeout per episode toe
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, // 4 GiB limiet
55 CPUMillis: 2000, // 2,0 CPU's
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}
Vergelijking van runtimes voor RL-sandboxes van agents
| Metriek / Vereiste | Gedeelde host-dockerd | Geprivilegieerde 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 (enkel niet-geprivilegieerd proces) |
Werkt binnen niet-geprivilegieerde K8s-Pods / bestaande runsc | Nee | Nee | Nee (geen /dev/kvm) | Mislukt (EBUSY bij geneste seccomp) | Ja (automatische futex-loop-fallback) |
Ondersteunt interne docker build & docker compose | Deelt host-daemon | Ja | Ja | Mislukt zonder host-DinD-afstemming | Ja (overlayfs op ext4lite) |
| Netwerkisolatie per episode | Gedeelde docker0 / xtables-lock | Aparte namespace | Aparte VM-NIC | Aparte netns | Geïsoleerde in-process netstack |
Latentie van warme episode-reset (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 platformen (Windows / macOS / Linux) | Vereist Linux-VM | Vereist Linux-VM | Alleen Linux KVM | Alleen Linux | Native op Windows, macOS en Linux |
Verder lezen in deze serie
- gvx: Linux-containers, Docker en Kubernetes uitvoeren in één niet-geprivilegieerd proces: Volledige architectuurwalkthrough van
gvxop Windows (whpx,systrap), macOS (hvf) en Linux (systrap). - Docker, docker build en docker compose uitvoeren zonder root of user namespaces: Snelle bootstrap in 3 stappen en troubleshootinggids wanneer
sudo,unshare(CLONE_NEWUSER),mount(2)en/dev/fusezijn geblokkeerd. - Docker-in-Docker uitvoeren binnen gVisor en geneste sandboxes met gvx: Diepgaande analyse van waarom
docker.ioen genesterunscfalen binnengVisor(runsc) en hoegvxterugvalt op de_RUN_SYSCALL_LOOPfutex-loop bijEBUSY. - Snelle geïsoleerde Docker-omgevingen voor RL-rollouts van AI-agents met gvx (deze post)