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

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:
- Vergrendelingsconflicten en statuslekken in 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 uitpakken van image-lagen, put dedocker0-subnettoewijzingen uit, veroorzaakt de foutAnother app is currently holding the xtables lockin host-iptablesen laat verweesde containers achter over episodes heen. - 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. - 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 bestaandegVisor(runsc)-sandboxes kunnen geen KVM-microVM's starten. - Trage episodereset (
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-trainingsworkers inactief blijven terwijl ze op omgevingsresets wachten.
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+ BPFseccompop Linux, Seatbeltdeny defaultop macOS, Restricted Token + Job Object op Windows). Ook als een RL-agentdocker run --privilegedof destructieverm -rf /-commando's binnen zijn episode uitvoert, blijven deze bewerkingen strikt beperkt tot de user-spaceSentry-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.

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:
- 100 milliseconden
env.reset()viacp --reflink=always:- Je bouwt één keer een
golden-docker.ext4-bestand met vooraf opgehaalde basisimages (python:3.11,postgres:16,redis:7of 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 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 verwijderen van de link naarep-0042.ext4geeft 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.
- Het beëindigen van het
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 (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
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. Start een seed-instantie van docker:dind met golden-docker.ext4 als backend
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 netjes zodat ext4lite de image wegschrijft en comprimeert
20kill -INT $SEED_PID
21wait $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:
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# Directe 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 episode zonder privileges 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# Opruimen van de episode na reward-berekening (~10 ms)
25kill -KILL $EP_PID
26rm -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:
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. Maak een reflink-kloon van 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 wandkloktimeout 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, // Limiet van 4 GiB
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 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
- gvx: Linux-containers, Docker en Kubernetes draaien in één onprivileged proces: Volledige architectuurrondleiding van
gvxop Windows (whpx,systrap), macOS (hvf) en Linux (systrap). - Docker, docker build en docker compose draaien zonder root of user namespaces: Snelle bootstrapgids in 3 stappen met probleemoplossing voor het geval
sudo,unshare(CLONE_NEWUSER),mount(2)en/dev/fusegeblokkeerd zijn. - Docker-in-Docker draaien in gVisor en geneste sandboxes met gvx: Diepgaande analyse van waarom
docker.ioen genesterunscmislukken binnengVisor(runsc) en hoegvxterugvalt op de_RUN_SYSCALL_LOOPfutex-loop bijEBUSY. - Snelle geïsoleerde Docker-omgevingen voor RL-rollouts van AI-agenten met gvx (dit bericht)