---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: gvxによるAI Agent RL Rolloutsのための高速な隔離Docker環境
description: SWE-bench およびコーディングエージェントの強化学習ロールアウトのために、数百の並列かつ強固に隔離された非特権の Docker および Kubernetes 環境を安全に実行する方法。100ミリ秒でのコピーオンライト ext4lite ボリュームのリセットを含む。
language: ja
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
---

## コーディングエージェントのRLロールアウトにおけるセキュリティとインフラストラクチャのボトルネック

ベンチマークおよび強化学習（RL）フレームワーク（**SWE-bench**、**SWE-ReX**、**R2E-Gym**、**SWE-Gym**、**Verl**、**OpenHands** など）にまたがってソフトウェアエンジニアリング・エージェントを訓練および評価するには、**信頼されていないLLM生成コード**の数千件の同時ロールアウトを実行する必要がある。実世界のソフトウェアエンジニアリング・タスクでは、エージェントは単にテキストファイルを編集するだけではない。`docker build` を実行し、`docker compose` を介してデータベースやマイクロサービスを起動し、統合テストスイートを実行し、コンテナのログを検査する。

ロールアウトの規模（クラスタあたり128〜1,024の同時エピソード）では、既存のコンテナインフラストラクチャは、システム的なセキュリティおよびパフォーマンスのボトルネックに四つ直面する。

1. **ホストの `dockerd` におけるロック競合と状態漏洩**: 数百の同時RLワーカーを単一のホスト `/var/run/docker.sock` に向けると、すべてのエージェントがホストデーモンに対してroot相当のアクセス権を得ることになり、イメージレイヤーの展開が直列化され、`docker0` のサブネット割り当てが枯渇し、ホストの `iptables` で `Another app is currently holding the xtables lock` エラーが発生し、エピソードをまたいで孤立したコンテナが漏洩する。
2. **特権DinDコンテナのエスケープリスク**: 標準の `docker:dind` コンテナを実行するには `--privileged`（`CAP_SYS_ADMIN`）が必要であり、これにより生のホストブロックデバイス、cgroupのrelease agent、カーネルモジュールが信頼されていないRLロールアウトに露出する。
3. **ハードウェア仮想化（`/dev/kvm`）の制約**: MicroVMランタイム（`Firecracker`、`QEMU`、`Kata Containers`）は内部カーネルを明確に隔離するが、これらは `/dev/kvm` を必要とする。ネスト仮想化を伴わない標準のKubernetesポッド、KVMが無効化されたベアメタルのクラウドインスタンス、および既存の `gVisor`（`runsc`）サンドボックスでは、KVM microVMを起動できない。
4. **エピソードリセット（`env.reset()`）の遅延**: RLエピソードごとに新しい `docker:dind` 環境を起動し、ベースイメージを取得または展開するには10〜60秒かかり、環境のリセットを待つ間GPUの訓練ワーカーがアイドル状態のままになる。

**[gvx](https://github.com/gfire-sigs/gvx)**（`rungv` CLIおよび `z.gfire.dev/gvx/pkg/gvx` Go SDK）は、上記の四つのボトルネックすべてを解決する**安全で多層防御型のロールアウトランタイム**を提供する。各ロールアウト環境は、**単一の非特権かつ自己ロックされたユーザー空間プロセス**として実行される（GoogleのgVisorの`Sentry`カーネル、`pkg/hostlockdown`、およびインメモリの`netstack`によって隔離される）。その `/var/lib/docker` の状態全体は、**単一のコピーオンライトな `.ext4` ファイル（`ext4lite`）**内に存在する。

> **信頼されていないエージェントのロールアウトのためのゼロ特権セキュリティ**: すべての `gvx` エピソードは、ゲストコードを実行する前に自身のホストOSプロセスをロックダウンする（Linuxでは `PR_SET_NO_NEW_PRIVS` と BPF `seccomp`、macOSでは Seatbelt `deny default`、Windowsでは Restricted Token と Job Object）。RLエージェントがエピソード内で `docker run --privileged` や破壊的な `rm -rf /` コマンドを実行したとしても、それらの操作は、そのエピソード固有のユーザー空間 `Sentry` カーネルおよび一時的な `.ext4` ボリュームファイルの内部に厳密に閉じ込められたままとなる。

---

## ロールアウトアーキテクチャ: 並列な非特権インスタンスとReflinkスナップショット

`gvx` では、各ロールアウトワーカーは、独自のgVisor `Sentry` カーネル、独自のユーザー空間 `netstack`（`10.0.2.15/24`）、および独自の `ext4lite` ブロックボリューム（`ep-N.ext4`）を備えた通常の非特権OSプロセスであり、読み取り専用のコンテンツアドレッサブルな `erofs` OCIイメージキャッシュをホストのページキャッシュ全体で共有する。

![gvxによる並列エージェントRLロールアウトアーキテクチャ](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### 単一ファイルの `ext4lite` ボリュームがRLリセットの経済性を変える理由

`gvx` の `ext4lite` ファイルシステムは、JBD2ジャーナリングされた `ext4` ボリューム全体（Dockerの `overlayfs` レイヤーディレクトリ、`trusted.overlay.*` 拡張属性、whiteoutデバイスノード、イメージメタデータを含む）を一つの通常のホストファイル内に格納するため、エピソードのライフサイクル管理は単一のファイル操作となる。

1. **`cp --reflink=always` による100ミリ秒の `env.reset()`**:
   - 事前に取得したベースイメージ（`python:3.11`、`postgres:16`、`redis:7`、またはタスク固有の `SWE-bench` ベースイメージ）を含む `golden-docker.ext4` ファイルを一度だけ構築する。
   - 各RLエピソードの開始時に、`cp --reflink=always golden-docker.ext4 ep-0042.ext4`（XFS、Btrfs、ZFS、またはmacOS APFS上）を実行すると、基盤となるディスクブロックをコピーすることなく、100ミリ秒未満で独立したコピーオンライトのクローンが作成される。
   - そのエピソードの間に変更された4 KiBブロックのみが、追加のディスク容量を消費する。
2. **残留物のないティアダウン（`env.close()`）**:
   - `rungv` プロセスを終了し（Goでは `sb.Close()`）、`ep-0042.ext4` のリンクを解除すると、約10ミリ秒でそのエピソードのプロセス、ネットワークソケット、マウント、ストレージの100%が回収される。ホストカーネルのマウントテーブルに何かがマウントされることや、ホストのcgroupに登録されることは一切ない。

---

## 段階的ワークフロー: ゴールデン `.ext4` スナップショットと高速エピソードリセット

各ロールアウトワーカーノードへの `rungv` のインストールは、[GitHub Releases](https://github.com/gfire-sigs/gvx/releases) からSLSA認証済みのビルド済みバイナリをダウンロードする（`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`）か、`CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest` を実行することで行える。

### ステップ1: ゴールデン `docker:dind` ボリュームを一度だけ作成する

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

# 1. golden-docker.ext4 を基盤とするシードの docker:dind インスタンスを起動する
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. 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. ext4lite がイメージをフラッシュおよび圧縮できるよう、シードインスタンスを正常に停止する
kill -INT $SEED_PID
wait $SEED_PID
```

### ステップ2: 100ミリ秒で並列RLエピソードを起動する

各並列ロールアウトワーカーは、reflinkによって `golden-docker.ext4` を複製し、`/var/lib/docker` にすべてのイメージが既に存在している状態で、隔離された `docker:dind` インスタンスを起動する。

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

# 即時のコピーオンライトクローン（XFS/Btrfs/APFS上で約100ms）
cp --reflink=always \
  /srv/rl-snapshots/golden-docker.ext4 \
  "/srv/rl-episodes/${EPISODE_ID}.ext4"

# 4 GiBのRAMと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

# ロールアウトワーカー内では、ネットワークからの取得なしにイメージがただちに利用可能である
docker images

# 報酬計算後のエピソードのティアダウン（約10ms）
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## `z.gfire.dev/gvx/pkg/gvx` Go SDKを用いたプログラムによるロールアウトプール

RL環境サーバーまたは評価ハーネスがGoで記述されている場合（あるいはgRPC経由でGoのロールアウトデーモンを呼び出す場合）、`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. ゴールデンな ext4lite ボリュームを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. エピソードごとの実時間タイムアウトを適用する
	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の上限
			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)
	}
}
```

---

## エージェントRLサンドボックスランタイムの比較

| 指標 / 要件                                                | 共有ホストの `dockerd`            | 特権 `docker:dind`       | Firecracker / KVM MicroVM      | 標準の `gVisor`（`runsc`）        | `gvx`（`rungv` / `pkg/gvx`）                |
| :-------------------------------------------------------- | :-------------------------------- | :----------------------- | :----------------------------- | :-------------------------------- | :------------------------------------------ |
| **ホストの `root` または `--privileged` が必要か**           | はい（`docker.sock` = root）        | はい（`--privileged`）     | `/dev/kvm` と `TAP` の設定が必要 | はい（または `userns` + `mount`）       | **いいえ（単一の非特権プロセス）**              |
| **非特権のK8s Pod / 既存の `runsc` 内で動作するか**           | いいえ                            | いいえ                   | いいえ（`/dev/kvm` がない）       | 失敗（ネストされたseccompで `EBUSY`）    | **はい（futexループへの自動フォールバック）**     |
| **内部での `docker build` と `docker compose` をサポートするか** | ホストデーモンを共有                | はい                     | はい                           | ホスト側のDinDチューニングなしでは失敗     | **はい（`ext4lite` 上の `overlayfs`）**        |
| **エピソードごとのネットワーク隔離**                          | `docker0` / `xtables` ロックを共有   | 別の名前空間              | 別のVM NIC                     | 別のnetns                          | **プロセス内の隔離された `netstack`**            |
| **ウォーム状態でのエピソードリセット遅延（`env.reset()`）**      | 5秒〜30秒（コンテナ/ボリュームの削除）  | 10秒〜60秒（コールドデーモン）| 約150ms（VMスナップショット）      | 5秒〜20秒                           | **約100ms（`cp --reflink=always` の `.ext4`）** |
| **クロスプラットフォームのローカル開発（Windows / macOS / Linux）** | Linux VMが必要                      | Linux VMが必要             | Linux KVMのみ                  | Linuxのみ                           | **Windows、macOS、Linuxでネイティブに動作**      |

---

## このシリーズの関連記事

1. **[gvx: 非特権プロセス一つでLinuxコンテナ、Docker、Kubernetesを実行する](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Windows（`whpx`、`systrap`）、macOS（`hvf`）、Linux（`systrap`）にわたる `gvx` のアーキテクチャの完全な解説。
2. **[rootやユーザー名前空間なしでDocker、docker build、docker composeを実行する](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: `sudo`、`unshare(CLONE_NEWUSER)`、`mount(2)`、`/dev/fuse` がブロックされている場合の、3ステップの簡易ブートストラップおよびトラブルシューティングガイド。
3. **[gvxを用いてgVisorおよびネストされたサンドボックス内でDocker-in-Dockerを実行する](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: `docker.io` およびネストされた `runsc` が `gVisor`（`runsc`）内で失敗する理由と、`gvx` が `EBUSY` 発生時に `_RUN_SYSCALL_LOOP` futexループへどのようにフォールバックするかについての詳細解説。
4. **[gvxによるAIエージェントRLロールアウトのための高速な隔離Docker環境](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**（本記事）
