---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Nopeat eristetyt Docker-ympäristöt AI-agenttien RL-rollouteille gvx:n avulla
description: Kuinka ajaa turvallisesti satoja rinnakkaisia, vahvasti eristettyjä ilman erioikeuksia toimivia Docker- ja Kubernetes-ympäristöjä SWE-bench- ja koodaavien agenttien vahvistusoppimisen rollout-ajoihin, joissa käytetään 100 millisekunnin kopioi-kirjoittaessa (copy-on-write) ext4lite-levyjen palautuksia.
language: fi
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
---

## Turvallisuus- ja infrastruktuuripullonkaula koodausagenttien RL-rollouteissa

Ohjelmistotekniikan agenttien koulutus ja arviointi useissa benchmarkeissa ja vahvistusoppimisen (RL) kehyksissä, kuten **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** ja **OpenHands**, edellyttää tuhansien samanaikaisten rollout-suoritusten ajamista luotettavaan koodiin, jonka LLM on generoinut. Todellisissa ohjelmistotehtävissä agentti ei pelkästään muokkaa tekstitiedostoja: se ajaa `docker build` -komennon, käynnistää tietokantoja ja mikropalveluita komennolla `docker compose`, suorittaa integraatiotestisarjoja ja tarkastelee konttien lokeja.

Rollout-mittakaavassa (128–1 024 samanaikaista episodia klusteria kohden) nykyinen konttiinfrastruktuuri kohtaa neljä järjestelmällistä turvallisuus- ja suorituskykypullonkaulaa:

1. **Isäntäkoneen `dockerd`-lukkiintuminen ja tilan vuotaminen**: Useiden satojen samanaikaisten RL-työntekijöiden osoittaminen yhteen isäntäkoneen `/var/run/docker.sock`-tiedostoon antaa jokaiselle agentille juurioikeudet vastaavan käyttöoikeuden isäntädaemoniin, sarjallistaa image-kerrosten purkamisen, ehtyy `docker0`-aliverkon osoitteista, aiheuttaa virheitä `Another app is currently holding the xtables lock` isäntäkoneen `iptables`-työkalussa ja vuotaa orpoja kontteja episodien välillä.
2. **Privilegoidun DinD-kontin karkaamisriski**: Vakiomuotoisten `docker:dind`-konttien ajaminen vaatii `--privileged`-lipun (`CAP_SYS_ADMIN`), joka altistaa raakoja isäntäkoneen lohkolaitteita, cgroup-release-agenteja ja ytimen moduuleja luottamattomille RL-rollouteille.
3. **Laitteistovirtualisoinnin (`/dev/kvm`) rajoitteet**: MicroVM-ajonaikat (`Firecracker`, `QEMU`, `Kata Containers`) eristävät sisäisen ytimen siististi, mutta ne vaativat `/dev/kvm`-laitteen. Tavalliset Kubernetes-podit ilman sisäkkäistä virtualisointia, bare-metal-pilvi-instanssit, joissa KVM on poistettu käytöstä, sekä olemassa olevat `gVisor` (`runsc`) -hiekkalaatikot eivät voi käynnistää KVM-microVM-koneita.
4. **Hidas episodin nollaus (`env.reset()`) -viive**: Uuden `docker:dind`-ympäristön käynnistäminen sekä peruskuvien lataaminen tai purkaminen jokaiselle RL-episodille kestää 10–60 sekuntia, ja GPU-koulutustyöntekijät jäävät tyhjäkäynnille odottamaan ympäristön nollauksia.

**[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`-CLI ja `z.gfire.dev/gvx/pkg/gvx` Go SDK) tarjoaa **turvallisen, syvällisesti puolustetun rollout-ajonaikan**, joka ratkaisee kaikki neljä pullonkaulaa. Jokainen rollout-ympäristö toimii **yhtenä ilman etuoikeuksia ajettavana, itselukittuvana käyttäjätilan prosessina** (eristettynä Googlen gVisor `Sentry`-ytimellä, `pkg/hostlockdown`-paketilla ja muistissa olevalla `netstack`-verkkopinolla), jonka koko `/var/lib/docker`-tila sijaitsee **yhdessä copy-on-write-`.ext4`-tiedostossa (`ext4lite`)**.

> **Nollaoikeuksinen turvallisuus luottamattomille agenttien rollouteille**: Jokainen `gvx`-episodi lukitsee oman isäntäkäyttöjärjestelmäprosessinsa ennen vieraskoodin ajamista (`PR_SET_NO_NEW_PRIVS` + BPF `seccomp` Linuxilla, Seatbelt `deny default` macOS:llä, Restricted Token + Job Object Windowsilla). Vaikka RL-agentti suorittaisi episodissaan komennon `docker run --privileged` tai tuhoavan komennon `rm -rf /`, nämä toiminnot pysyvät tiukasti rajattuina kyseisen episodin käyttäjätilan `Sentry`-ytimeen ja väliaikaiseen `.ext4`-volyymitiedostoon.

---

## Rollout-arkkitehtuuri: rinnakkaiset ilman etuoikeuksia ajettavat instanssit + reflink-snapshotit

`gvx`:ssä jokainen rollout-työntekijä on tavallinen ilman etuoikeuksia toimiva käyttöjärjestelmäprosessi, jolla on oma gVisor `Sentry`-ydin, oma käyttäjätilan `netstack` (`10.0.2.15/24`) ja oma `ext4lite`-lohkovolyymi (`ep-N.ext4`). Kaikki jakavat kuitenkin yhden luku-vain, sisältöosoitteellisen `erofs` OCI-image-välimuistin isäntäkoneen sivumuistissa.

![Rinnakkainen agentin RL-rollout-arkkitehtuuri gvx:llä](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Miksi yksitiedostoiset `ext4lite`-volyymit muuttavat RL-nollauksen talousmallia

Koska `gvx`:n `ext4lite`-tiedostojärjestelmä tallentaa koko JBD2-journaloidun `ext4`-volyymin (mukaan lukien Dockerin `overlayfs`-kerroshakemistot, `trusted.overlay.*`-laajennetut attribuutit, whiteout-laitesolmut ja image-metatiedot) yhteen tavalliseen isäntätiedostoon, episodin elinkaaren hallinta muuttuu yhdeksi tiedostotoiminnoksi:

1. **100 millisekunnin `env.reset()` komennolla `cp --reflink=always`**:
   - Rakennat `golden-docker.ext4`-tiedoston kerran. Se sisältää ennalta ladatut perus-imaget (`python:3.11`, `postgres:16`, `redis:7` tai tehtäväkohtaiset `SWE-bench`-perus-imaget).
   - Jokaisen RL-episodin alussa komento `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (XFS:llä, Btrfs:llä, ZFS:llä tai macOS APFS:llä) luo itsenäisen copy-on-write-kloonin alle 100 millisekunnissa kopioimatta taustalla olevia levylohkoja.
   - Vain tuon episodin aikana muokatut 4 KiB:n lohkot kuluttavat lisätilaa levyllä.
2. **Jäännöksetön purku (`env.close()`)**:
   - `rungv`-prosessin lopettaminen (`sb.Close()` Go-kielessä) ja tiedoston `ep-0042.ext4` poistaminen vapauttavat 100 % episodin prosesseista, verkkoyhteyksistä, liitoksista ja tallennustilasta noin 10 millisekunnissa. Mitään ei koskaan liitetä isäntäkoneen ytimen liitostauluun eikä rekisteröidä isäntäkoneen cgroupeihin.

---

## Vaiheittainen työnkulku: golden-`.ext4`-snapshot ja nopea episodin nollaus

Voit asentaa `rungv`-työkalun jokaiseen rollout-työntekijäsolmuun joko lataamalla SLSA-todennetun valmiiksi käännetyn binäärin [GitHub Releases](https://github.com/gfire-sigs/gvx/releases) -sivustolta (`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`) tai ajamalla komennon `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest`.

### Vaihe 1: Luo golden-`docker:dind`-volyymi kerran

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

# 1. Käynnistä siemen-docker:dind-instanssi, jota tukee 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. Esilataa perus-imaget ja build-välimuisti golden-docker.ext4-tiedoston sisälle
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. Pysäytä siemeninstanssi hallitusti, jotta ext4lite huuhtelee ja tiivistää imagen
kill -INT $SEED_PID
wait $SEED_PID
```

### Vaihe 2: Käynnistä rinnakkaiset RL-episodit 100 millisekunnissa

Jokainen rinnakkainen rollout-työntekijä kloonaa `golden-docker.ext4`-tiedoston reflinkin avulla ja käynnistää eristetyn `docker:dind`-instanssin, jossa kaikki imaget ovat jo hakemistossa `/var/lib/docker`:

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

# Välitön copy-on-write-kloni (~100 ms XFS/Btrfs/APFS-järjestelmillä)
cp --reflink=always \
  /srv/rl-snapshots/golden-docker.ext4 \
  "/srv/rl-episodes/${EPISODE_ID}.ext4"

# Käynnistä eristetty episodi ilman etuoikeuksia, 4 GiB RAM:lla ja 2 CPU:lla
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

# Rollout-työntekijän sisällä imaget ovat heti saatavilla ilman verkkolatausta
docker images

# Episodin purku palkkion laskennan jälkeen (~10 ms)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Ohjelmallinen rollout-allas käyttäen `z.gfire.dev/gvx/pkg/gvx` Go SDK:ta

Jos RL-ympäristöpalvelimesi tai arviointiharjoittimesi on kirjoitettu Go-kielellä tai kutsuu Go-rollout-taustaprosessia gRPC:n yli, voit hallita rinnakkaisia konttiepisodeja suoraan paketin `z.gfire.dev/gvx/pkg/gvx` kautta:

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

	// 2. Aseta episodikohtainen seinäkellioaikainen aikaraja
	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 katto
			CPUMillis:   2000,    // 2,0 CPU:ta
		},
		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)
	}
}
```

---

## Vertailu RL-agenttien hiekkalaatikkoympäristöistä

| Mittari / vaatimus                                        | Jaettu isäntäkoneen `dockerd`      | Privilegoitu `docker:dind` | Firecracker / KVM MicroVM          | Vakio `gVisor` (`runsc`)              | `gvx` (`rungv` / `pkg/gvx`)                  |
| :-------------------------------------------------------- | :--------------------------------- | :------------------------- | :--------------------------------- | :------------------------------------ | :------------------------------------------- |
| **Isäntäkoneen `root` tai `--privileged` vaaditaan**      | Kyllä (`docker.sock` = root)       | Kyllä (`--privileged`)     | Vaatii `/dev/kvm` + `TAP`-asetukset | Kyllä (tai `userns` + `mount`)        | **Ei (yksi prosessi ilman etuoikeuksia)**    |
| **Toimii rajoitetuissa K8s-podeissa / olemassa olevassa `runsc`:ssä** | Ei                       | Ei                         | Ei (ei `/dev/kvm`:ää)              | Epäonnistuu (`EBUSY` sisäkkäisessä seccompissa) | **Kyllä (automaattinen futex-silmukkavaraus)** |
| **Tukee sisäistä `docker build`- ja `docker compose`-käyttöä** | Jakaa isäntädaemonin          | Kyllä                      | Kyllä                              | Epäonnistuu ilman isäntä-DinD-säätöjä | **Kyllä (`overlayfs` `ext4lite`-tiedostolla)** |
| **Episodikohtainen verkkoeristys**                        | Jaettu `docker0` / `xtables`-lukko | Erillinen nimiavaruus      | Erillinen VM-verkkokortti          | Erillinen netns                       | **Eristetty prosessinsisäinen `netstack`**   |
| **Lämmin episodin nollauksen viive (`env.reset()`)**      | 5 s – 30 s (kontin/volyymin siivous) | 10 s – 60 s (kylmä daemon) | ~150 ms (VM-snapshot)              | 5 s – 20 s                            | **~100 ms (`cp --reflink=always` `.ext4`)**  |
| **Alustojen välinen paikallinen kehitys (Windows / macOS / Linux)** | Vaatii Linux-VM          | Vaatii Linux-VM            | Vain Linux KVM                     | Vain Linux                            | **Natiivi Windowsilla, macOS:llä ja Linuxilla** |

---

## Lisälukemista tästä sarjasta

1. **[gvx: Linux-konttien, Dockerin ja Kubernetesin ajaminen yhdessä ilman etuoikeuksia toimivassa prosessissa](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Täydellinen arkkitehtuurikatsaus `gvx`:stä Windowsilla (`whpx`, `systrap`), macOS:llä (`hvf`) ja Linuxilla (`systrap`).
2. **[Docker, docker build ja docker compose ilman juurioikeuksia tai käyttäjänimiavaruuksia](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Nopea kolmen vaiheen käynnistysopas ja vianmääritysopas tilanteisiin, joissa `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` ja `/dev/fuse` on estetty.
3. **[Docker-in-Docker gVisorin ja sisäkkäisten hiekkalaatikoiden sisällä gvx:llä](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: Syvällinen tarkastelu siitä, miksi `docker.io` ja sisäkkäinen `runsc` epäonnistuvat `gVisorin` (`runsc`) sisällä, ja miten `gvx` siirtyy varavaihtoehtoon `_RUN_SYSCALL_LOOP` futex-silmukkaan `EBUSY`-virheen ilmetessä.
4. **[Nopeat eristetyt Docker-ympäristöt AI-agenttien RL-rollouteille gvx:llä](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** (tämä artikkeli)
