---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Szybkie izolowane środowiska Docker dla rolloutów RL agentów AI z gvx
description: Jak bezpiecznie uruchamiać setki równoległych, silnie izolowanych, nieuprzywilejowanych środowisk Docker i Kubernetes dla SWE-bench oraz rolloutów uczenia ze wzmocnieniem agentów kodujących, z resetowaniem woluminów ext4lite metodą copy-on-write w czasie 100 milisekund?
language: pl
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
---

## Wąskie gardło bezpieczeństwa i infrastruktury w rolloutach RL agentów kodujących

Trenowanie i ewaluacja agentów inżynierii oprogramowania w ramach benchmarków oraz frameworków uczenia przez wzmacnianie (RL), takich jak **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** i **OpenHands**, wymaga uruchamiania tysięcy równoległych rolloutów **niezaufanego kodu generowanego przez LLM**. W rzeczywistych zadaniach inżynierii oprogramowania agent nie tylko edytuje pliki tekstowe: uruchamia `docker build`, podnosi bazy danych i mikrousługi za pomocą `docker compose`, wykonuje zestawy testów integracyjnych i analizuje logi kontenerów.

Przy skali rolloutów (od 128 do 1 024 równoległych epizodów na klaster) istniejąca infrastruktura kontenerowa napotyka cztery systemowe wąskie gardła bezpieczeństwa i wydajności:

1. **Rywalizacja o blokady `dockerd` hosta i wyciek stanu**: Skierowanie setek równoległych workerów RL na pojedynczy host `/var/run/docker.sock` daje każdemu agentowi uprawnienia równoważne root do demona hosta, serializuje wypakowywanie warstw obrazów, wyczerpuje pule podsieci `docker0`, wywołuje błędy `Another app is currently holding the xtables lock` w `iptables` hosta oraz pozostawia osierocone kontenery między epizodami.
2. **Ryzyko ucieczki z kontenera w trybie uprzywilejowanym DinD**: Uruchamianie standardowych kontenerów `docker:dind` wymaga `--privileged` (`CAP_SYS_ADMIN`), co udostępnia niezaufanym rolloutom RL surowe urządzenia blokowe hosta, agenty zwalniania cgroup oraz moduły jądra.
3. **Ograniczenia wirtualizacji sprzętowej (`/dev/kvm`)**: Środowiska MicroVM (`Firecracker`, `QEMU`, `Kata Containers`) czysto izolują wewnętrzne jądra, ale wymagają `/dev/kvm`. Standardowe pody Kubernetes bez zagnieżdżonej wirtualizacji, instancje chmurowe bare-metal, na których KVM jest wyłączony, oraz istniejące piaskownice `gVisor` (`runsc`) nie mogą uruchamiać microVM KVM.
4. **Wolny reset epizodu (`env.reset()`)**: Uruchomienie świeżego środowiska `docker:dind` oraz pobieranie lub rozpakowywanie obrazów bazowych dla każdego epizodu RL trwa od 10 do 60 sekund, pozostawiając workery treningowe GPU bezczynne podczas oczekiwania na reset środowiska.

**[gvx](https://github.com/gfire-sigs/gvx)** (CLI `rungv` oraz SDK Go `z.gfire.dev/gvx/pkg/gvx`) zapewnia **bezpieczne, wielowarstwowe (defense-in-depth) środowisko uruchomieniowe rolloutów**, które rozwiązuje wszystkie cztery wąskie gardła. Każde środowisko rolloutu działa jako **pojedynczy, nieuprzywilejowany, samoblokujący się proces przestrzeni użytkownika** (izolowany przez jądro `Sentry` gVisor firmy Google, `pkg/hostlockdown` oraz `netstack` działający w pamięci), którego cały stan `/var/lib/docker` znajduje się wewnątrz **pojedynczego pliku `.ext4` z kopiowaniem przy zapisie (`ext4lite`)**.

> **Bezpieczeństwo bez uprawnień dla niezaufanych rolloutów agentów**: Każdy epizod `gvx` blokuje własny proces systemu hosta przed uruchomieniem kodu gościa (`PR_SET_NO_NEW_PRIVS` + BPF `seccomp` w Linuksie, Seatbelt `deny default` w macOS, Restricted Token + Job Object w Windows). Nawet jeśli agent RL wykona `docker run --privileged` lub destrukcyjne polecenia `rm -rf /` w ramach swojego epizodu, operacje te pozostają ściśle ograniczone do jądra `Sentry` przestrzeni użytkownika tego epizodu oraz jego efemerycznego pliku woluminu `.ext4`.

---

## Architektura rolloutów: równoległe instancje bez uprawnień + migawki reflink

W `gvx` każdy worker rolloutu jest zwykłym nieuprzywilejowanym procesem systemu operacyjnego, z własnym jądrem gVisor `Sentry`, własnym `netstack` w przestrzeni użytkownika (`10.0.2.15/24`) oraz własnym blokowym woluminem `ext4lite` (`ep-N.ext4`), a jednocześnie współdzieli tylko do odczytu adresowaną zawartością pamięć podręczną obrazów OCI `erofs` w ramach pamięci podręcznej stron hosta.

![Równoległa architektura rolloutów RL agentów z gvx](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Dlaczego woluminy `ext4lite` w jednym pliku zmieniają ekonomię resetów RL

Ponieważ system plików `ext4lite` w `gvx` przechowuje cały wolumin `ext4` z dziennikiem JBD2 (w tym katalogi warstw `overlayfs` Dockera, rozszerzone atrybuty `trusted.overlay.*`, węzły urządzeń whiteout oraz metadane obrazów) w jednym zwykłym pliku hosta, zarządzanie cyklem życia epizodu sprowadza się do jednej operacji na pliku:

1. **Reset `env.reset()` w 100 milisekund za pomocą `cp --reflink=always`**:
   - Plik `golden-docker.ext4` budujesz jednorazowo; zawiera on wstępnie pobrane obrazy bazowe (`python:3.11`, `postgres:16`, `redis:7` lub specyficzne dla zadania obrazy bazowe `SWE-bench`).
   - Na początku każdego epizodu RL polecenie `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (w systemach XFS, Btrfs, ZFS lub macOS APFS) tworzy niezależny klon Copy-on-Write w czasie krótszym niż 100 milisekund, bez kopiowania bazowych bloków dyskowych.
   - Dodatkowe miejsce na dysku zajmują wyłącznie bloki 4 KiB zmodyfikowane podczas tego konkretnego epizodu.
2. **Zakończenie bez pozostałości (`env.close()`)**:
   - Zakończenie procesu `rungv` (`sb.Close()` w Go) oraz usunięcie dowiązania `ep-0042.ext4` zwalnia 100% procesów, gniazd sieciowych, punktów montowania i pamięci masowej epizodu w ok. 10 milisekund. Nic nigdy nie jest montowane w tabeli montowań jądra hosta ani rejestrowane w cgroupach hosta.

---

## Procedura krok po kroku: złota migawka `.ext4` i szybki reset epizodu

Program `rungv` można zainstalować na każdym węźle workera rolloutu, pobierając gotowy plik binarny z poświadczeniem SLSA z [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`) albo uruchamiając `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest`.

### Krok 1: Jednorazowe utworzenie złotego woluminu `docker:dind`

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

# 1. Uruchom instancję bazową docker:dind opartą na 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. Wstępnie wypełnij obrazy bazowe i pamięć podręczną budowania wewnątrz 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. Łagodnie zatrzymaj instancję bazową, aby ext4lite opróżnił bufory i skompaktował obraz
kill -INT $SEED_PID
wait $SEED_PID
```

### Krok 2: Uruchamianie równoległych epizodów RL w 100 milisekund

Każdy równoległy worker rolloutu klonuje `golden-docker.ext4` za pomocą reflink i uruchamia odizolowaną instancję `docker:dind` ze wszystkimi obrazami już obecnymi w `/var/lib/docker`:

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

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

# Uruchom odizolowany epizod bez uprawnień z 4 GiB RAM i 2 CPU
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

# Wewnątrz workera rolloutu obrazy są od razu dostępne bez pobierania przez sieć
docker images

# Zakończenie epizodu po obliczeniu nagrody (~10 ms)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Programowa pula rolloutów z użyciem SDK Go `z.gfire.dev/gvx/pkg/gvx`

Jeśli serwer środowiska RL lub harness ewaluacyjny jest napisany w Go (albo wywołuje demona rolloutów w Go przez gRPC), możesz zarządzać równoległymi epizodami kontenerów bezpośrednio za pomocą `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. Sklonuj złoty wolumin ext4lite za pomocą reflink
	cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
	if err := cpCmd.Run(); err != nil {
		return fmt.Errorf("clone golden volume: %w", err)
	}

	// 2. Zastosuj limit czasu rzeczywistego dla epizodu
	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, // limit 4 GiB
			CPUMillis:   2000,    // 2.0 CPU
		},
		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)
	}
}
```

---

## Porównanie środowisk uruchomieniowych piaskownic RL agentów

| Metryka / wymaganie                                       | Współdzielony `dockerd` hosta     | Uprzywilejowany `docker:dind` | Firecracker / KVM MicroVM       | Standardowy `gVisor` (`runsc`)     | `gvx` (`rungv` / `pkg/gvx`)                  |
| :-------------------------------------------------------- | :-------------------------------- | :---------------------------- | :------------------------------ | :--------------------------------- | :------------------------------------------- |
| **Wymagany `root` hosta lub `--privileged`**              | Tak (`docker.sock` = root)        | Tak (`--privileged`)          | Wymaga `/dev/kvm` + konfiguracji `TAP` | Tak (lub `userns` + `mount`)       | **Nie (pojedynczy nieuprzywilejowany proces)** |
| **Działa wewnątrz nieuprzywilejowanych podów K8s / istniejącego `runsc`** | Nie                  | Nie                           | Nie (brak `/dev/kvm`)           | Zawodzi (`EBUSY` przy zagnieżdżonym seccomp) | **Tak (automatyczny awaryjny tryb pętli futex)** |
| **Obsługuje wewnętrzne `docker build` i `docker compose`** | Współdzieli demona hosta         | Tak                           | Tak                             | Zawodzi bez strojenia DinD na hoście | **Tak (`overlayfs` na `ext4lite`)**            |
| **Izolacja sieci per epizod**                             | Współdzielona blokada `docker0` / `xtables` | Oddzielna przestrzeń nazw | Oddzielna karta sieciowa VM     | Oddzielny netns                    | **Odizolowany `netstack` w procesie**          |
| **Opóźnienie ciepłego resetu epizodu (`env.reset()`)**    | 5 s do 30 s (czyszczenie kontenera/woluminu) | 10 s do 60 s (zimny demon) | ~150 ms (migawka VM)       | 5 s do 20 s                        | **~100 ms (`cp --reflink=always` `.ext4`)**    |
| **Lokalny rozwój wieloplatformowy (Windows / macOS / Linux)** | Wymaga maszyny wirtualnej Linux | Wymaga maszyny wirtualnej Linux | Tylko Linux KVM            | Tylko Linux                        | **Natywnie na Windows, macOS i Linux**         |

---

## Dalsza lektura w tej serii

1. **[gvx: uruchamianie kontenerów Linux, Dockera i Kubernetes w jednym nieuprzywilejowanym procesie](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Kompletny przegląd architektury `gvx` w systemach Windows (`whpx`, `systrap`), macOS (`hvf`) i Linux (`systrap`).
2. **[Uruchamianie Dockera, docker build i docker compose bez roota i przestrzeni nazw użytkownika](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Szybki 3-etapowy przewodnik uruchomienia i rozwiązywania problemów, gdy zablokowane są `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` oraz `/dev/fuse`.
3. **[Uruchamianie Docker-in-Docker wewnątrz gVisor i zagnieżdżonych piaskownic z gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: Szczegółowe omówienie tego, dlaczego `docker.io` i zagnieżdżony `runsc` zawodzą wewnątrz `gVisor` (`runsc`) oraz jak `gvx` przechodzi na pętlę futex `_RUN_SYSCALL_LOOP` przy `EBUSY`.
4. **[Szybkie odizolowane środowiska Docker dla rolloutów RL agentów AI z gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** (ten wpis)
