---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: gvx を用いた gVisor 内および入れ子の Sandboxes 内での Docker-in-Docker の実行
description: docker.io のインストールや gVisor コンテナ内での runsc のネストが、modprobe、iptables-nft、overlay2、cgroups、seccomp EBUSY にわたって失敗する理由、そして gvx が --privileged を用いることなく、分離されたユーザー空間カーネル内で入れ子の docker:dind を安全に実行する方法について。
language: ja
date: 2026-11-06T12:02:55.931762327Z
path: /blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03
go_package: z.gfire.dev/gvx
go_repourl: https://github.com/gfire-sigs/gvx.git
category: lowlevel
---

## クイックソリューション: 制限された`gVisor` (`runsc`) サンドボックス内で`docker:dind`を安全に実行する

クラウドAIエージェントプラットフォーム、Kubernetesの`agent-sandbox`デプロイメント(`RuntimeClass: gvisor`)、および強化学習の評価ハーネスは、ホストカーネルを保護するために、信頼されていないワークロードを**gVisor (`runsc`)** コンテナ内に隔離する。デフォルトの非特権`runsc`コンテナ内のワークロードが、`apt-get`で`docker.io`または`docker-ce`をインストールし、`dockerd`を起動しようとすると、コンテナ内で`root`(`UID 0`)権限を持っていても失敗する。

**[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) を使用すると、外側の`RuntimeClass`を弱めたり、ホストソケットを公開したり、`--privileged`を付与したりすることなく、非特権`gVisor` (`runsc`) コンテナ内で`docker:dind`、`docker build`、`docker compose`を**安全に**直接実行できる(または`gvx`内で`gvx`をネストする)。

> **外側のサンドボックス隔離の維持と強化**: `runsc`内で`rungv`を実行しても、外側の`gVisor`サンドボックスによって課される制限は一つも回避されない。代わりに`gvx`は、独自の`hostlockdown`(`PR_SET_NO_NEW_PRIVS` + `seccomp`)、独自のインメモリ`netstack`、および単一ファイルの`ext4lite`ボリュームを備えた、第二のネストされたユーザー空間gVisor `Sentry`カーネルを作成する。内部の`dockerd`と`runc`コンテナは二重にサンドボックス化されており、そのシステムコール、マウント、生パケットは内部の`gvx`ユーザー空間カーネルの外に出ることがない。

```sh
# 既存のgVisor (runsc) コンテナまたはKubernetes Pod内:
# SLSAで証明されたGitHubリリースアーカイブからrungvをインストールするか、go installを使用する:
CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &

# 内部のgvxソケットに対して標準的なDockerのビルドとコンテナを実行する
export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done
docker info --format '{{.Driver}} {{.OperatingSystem}}'
# 出力: overlayfs Alpine Linux ... (コンテナ化)

docker build -t nested-app:test .
docker run --rm nested-app:test
```

---

## `gVisor` (`runsc`) 内で直接のDockerインストールが失敗する5つの理由

ユーザーまたはAIコーディングエージェントが標準の`runsc`コンテナ内で`apt-get install -y docker.io`(または`docker-ce`)を実行し、`dockerd`の起動を試みると、五つの連続した技術的障壁に直面する。

### 1. 仮想カーネル`4.4.0`に対するカーネルモジュールの読み込み(`modprobe`)

パッケージのインストールおよび`dockerd`の起動中、初期化スクリプトは`overlay`、`br_netfilter`、`ip_tables`、`nf_nat`、`xt_conntrack`を読み込むために`modprobe`を実行する。外側のgVisor Sentryは`init_module(2)`および`finit_module(2)`をブロックし、`/lib/modules/4.4.0`ディレクトリが存在しない仮想カーネルバージョン(`4.4.0`)を報告する:

```text
modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
```

### 2. `iptables-nft` (`nf_tables` Netlink) および`docker0`ブリッジの失敗

Ubuntu 22.04、Ubuntu 24.04、Debian 11/12では、`apt-get install docker.io`は、デフォルトの`/usr/sbin/iptables`の代替として`iptables-nft`(`nf_tables` netlinkサブシステムをバックエンドとする)を選択する。ネットワーク管理権限または`nf_tables` netlinkのサポートを持たない標準のgVisorコンテナでは、`dockerd`はDOCKER NATチェーンと`docker0`ブリッジを初期化する際にクラッシュする:

```text
failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: iptables failed: iptables -t nat -N DOCKER
iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
iptables: No chain/target/match by that name.
Unable to enable SKIP DNAT rule: (iptables failed: iptables --wait -t nat -I DOCKER -i docker0 -j RETURN: iptables: No chain/target/match by that name.)
Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
```

### 3. `gofer` / `9p`ルートファイルシステム上の`overlay2`の失敗と`vfs`によるディスク枯渇

外側の`runsc`管理者が`/var/lib/docker`に専用の`tmpfs`またはfilestoreマウントを明示的に事前構成していない限り、コンテナのルートファイルシステムはgVisorの`gofer` (`9p`/`lisafs`) またはネストされたoverlayマウントによってバックアップされる。`/var/lib/docker`の上に`overlay2`をマウントしようとすると失敗する:

```text
level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
failed to mount overlay: invalid argument
Error starting daemon: error initializing graphdriver: driver not supported: overlay2
```

ユーザーまたはエージェントが`dockerd --storage-driver=vfs`で起動することによってこのエラーを回避しようとすると、Dockerはコピーオンライト(Copy-on-Write)によるレイヤー共有を完全に無効化する。`Dockerfile`内のすべての`RUN`命令は、ルートファイルシステム全体の完全な再帰コピー(`dirCopy`)を実行し、10のビルドステップにわたる500 MBのイメージを5 GBの書き込みに増大させ、サンドボックスを`no space left on device`またはI/Oタイムアウトで終了させる。

### 4. ネストされた`runc`のcgroupおよび`/proc`マウントの拒否

`dockerd --iptables=false --bridge=none --storage-driver=vfs`を強制的に起動したとしても、`docker run`または`docker build`は`runc`を呼び出して内部コンテナを作成する。デフォルトの非特権gVisorコンテナでは、`runc`はcgroupのマウント、`/proc`の再マウント、または内部seccompフィルタの読み込みに失敗する:

```text
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process:
error during container init: error mounting "cgroup" to rootfs at "/sys/fs/cgroup": mount cgroup:/sys/fs/cgroup/cpuset (via /proc/self/fd/6), flags: 0xf, data: cpuset: operation not permitted
cgroups: cgroup mountpoint does not exist: unknown
runc create failed: systemd not running on this host: cannot use systemd cgroup manager
error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
error during container init: error loading seccomp filter into kernel: operation not permitted
```

### 5. ネストされた`runsc`が`SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`) で失敗する

ユーザーが`systrap`プラットフォームを使用して、`runsc`内で別の`runsc`インスタンスを実行しようとすると、初期化は直ちに中止される:

```text
creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
```

Linuxカーネルとgvisor Sentryの両方は、タスク階層ごとに**最大一つ**の`SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) リスナーしか許可しない。外側のサンドボックスが既にそのリスナースロットを保持しているため、内部の`seccomp(2)`呼び出しは`EBUSY` (`device or resource busy`) を返す。

---

## `gvx`がgVisor内のネストされた実行をどのように解決するか

`gvx`は、独自のユーザー空間カーネルおよびプラットフォーム層内で五つの障壁すべてに対処し、`rungv`が外側のgVisor (`runsc`) コンテナ内、さらには別の`gvx`コンテナ内で再帰的に(`gvx`内の`gvx`として)実行できるようにする。

![外側のgVisor内でのネストされたgvx実行](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. `EBUSY`から`_RUN_SYSCALL_LOOP`のfutexループへの自動`systrap`フォールバック

Linuxの`systrap`では、各スタブサブプロセスは二種類のスレッドを実行する:

- **ゲスト実行スレッド(`sysmsg`)**: `SECCOMP_RET_TRAP` (`SIGSYS`) およびバイナリパッチされた`jmp rel32`トランポリン(`syshandler`)を介してゲストのシステムコールを捕捉し、共有メモリおよび`SYS_FUTEX`を介してレジスタの状態をSentryと交換する。
- **スタブの`syscallThread`(`syscall_thread.go`)**: Sentryに代わって、スタブサブプロセス内でアドレス空間のシステムコール(`mmap`、`munmap`、`clone`)を実行する。

アップストリームのgVisorでは、`syscallThread`は`stub_amd64.s`および`stub_arm64.s`において二つの起床モードをサポートしている:

1. **Seccomp User-Notifyループ(`_RUN_SECCOMP_LOOP = 6`、`t.seccompNotify != nil`の場合)**: `syscallThread.init()`の間に、`t.installSeccompNotify()`が`SECCOMP_RET_USER_NOTIF`リスナー(`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`)をインストールし、Sentryが`ioctl(SECCOMP_IOCTL_NOTIF_SEND)`および`ioctl(SECCOMP_IOCTL_NOTIF_RECV)`を介してリクエストを送信できるようにする。
2. **Futexシステムコールループ(`_RUN_SYSCALL_LOOP = 5`、`t.seccompNotify == nil`の場合)**: `syscallThread`は`syscall_loop`(`stub_amd64.s` / `stub_arm64.s`)を実行し、共有の2ページ`syscallSentryMessage`(スタブ内では`PROT_READ`)および`syscallStubMessage`(スタブ内では`PROT_READ | PROT_WRITE`)領域に対して`SYS_FUTEX`(`FUTEX_WAIT`)でスリープする。

アップストリームのgVisorは、`seccompNotifyIsSupported`が真である場合は無条件に`t.installSeccompNotify()`を呼び出し、`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)`が`EBUSY`を返すとスタブの作成を中止する。

`gvx`(`pkg/sentry/platform/systrap/seccomp_notify_xplat.go`および`syscall_thread.go`)では、`syscallThread.init()`が`t.installSeccompNotifyIfFree()`を呼び出す:

```go
// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
var seccompNotifyBusy atomic.Bool

func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
	if seccompNotifyBusy.Load() {
		return nil, nil
	}
	f, err := t.installSeccompNotify()
	if errors.Is(err, unix.EBUSY) {
		if seccompNotifyBusy.CompareAndSwap(false, true) {
			log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
				"The stub threads use the slower futex loop instead of seccomp-unotify.")
		}
		return nil, nil
	}
	return f, err
}
```

`installSeccompNotify()`が`unix.EBUSY`を返すのは、外側の`gVisor` (`runsc`) または外側の`gvx`サンドボックスが、プロセスフィルタチェーンの単一のリスナースロットを既に保持しているためであり、その場合:

- `installSeccompNotifyIfFree()`は`seccompNotifyBusy` (`atomic.Bool`) を設定し、プロセスごとに一つの警告をログに記録し、そのスタブおよびその後のすべてのスタブに対して`t.seccompNotify == nil`のままにする。
- `syscallThread.detach()`(`syscall_thread_amd64.go`および`syscall_thread_arm64.go`)では、`t.seccompNotify == nil`により、`_RUN_SECCOMP_LOOP`(`6`)の代わりに、`amd64`では`regs.Rbx = _RUN_SYSCALL_LOOP`(`5`)が、`arm64`では`regs.Regs[9] = _RUN_SYSCALL_LOOP`が設定される。
- `syscallThread.syscall()`(`syscall_thread.go`)では、内部のSentryが共有メモリと`SYS_FUTEX`のみを介してスタブのシステムコールを振り分ける:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// 新しいシステムコールリクエストについて、syscallスレッドに通知する。
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// 応答を待機する。
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

`SECCOMP_RET_TRAP`フィルタは任意の深さまでゲストスレッドのスタック上で機能し、`syscallThread`は二つ目のユーザー通知リスナーを要求することなく`_RUN_SYSCALL_LOOP`にフォールバックするため、`gvx`は`runsc`内でも問題なく動作し、再帰的なネスト(`gvx`内の`gvx`)をサポートする。

### 2. 単一ファイル`ext4lite`ボリューム上のネイティブ`overlayfs`

外側のgVisorコンテナの`gofer` (`9p`/`lisafs`) ルートファイルシステムの上に`overlayfs`をマウントすることを避けるため、`-v gvx-docker:/var/lib/docker`は、`/var/lib/docker`を、単一の通常ファイル(`gvx-docker.ext4`)に格納された`gvx`の内部**`ext4lite`**ファイルシステムによってバックアップする。

外側の`runsc`にとって、`gvx-docker.ext4`は`pread64(2)`および`pwrite64(2)`を介してアクセスされる単なる一つの通常ファイルである。`gvx`の内部では、`ext4lite`はJBD2でジャーナリングされた`ext4`ファイルシステムを提供し、`trusted.overlay.*`および`security.capability`の拡張属性、文字デバイスのwhiteoutノード(`0:0`)、`d_type`ディレクトリエントリ、および`RENAME_EXCHANGE`をサポートする。これにより、内部の`dockerd`はネイティブの`overlayfs`ストレージドライバを使用できる。

### 3. `docker0`および`iptables-legacy`を備えた自己完結型`netstack`

`gvx`の内部では、`dockerd`は外側の`runsc`コンテナのネットワークインターフェースやnetfilterテーブルとやり取りしない。代わりに:

- `dockerd`は`gvx`独自のユーザー空間`netstack`内に、`docker0`ブリッジと`veth`ペアを作成する。
- すべての`iptables-legacy`ルール(`-t nat -N DOCKER`、`MASQUERADE`、`conntrack`、`addrtype`)は、`gvx`の内部netfilter実装に対して実行される。
- ネストされた`runc`コンテナは、`gvx`のインメモリ`VFS2`マウントテーブル内で`/proc`、`/sys`、`/sys/fs/cgroup`の階層をマウントし、外側の`runsc`の`mount(2)`システムコールを決して呼び出さない。

---

## gVisor内(または`gvx`内の`gvx`)でのネストされた`docker:dind`の検証

以下のコマンドを使用して、両方のシナリオ(`runsc`内の`rungv`、または`rungv`内の`rungv`)を検証できる:

```sh
# 外側のgVisor (runsc) コンテナまたは外側のrungvコンテナ内:
mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
DIND_PID=$!

export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done

# 内部のDockerデーモンがvfsではなくネイティブのoverlayfsを使用していることを確認する
docker info --format 'Storage Driver: {{.Driver}}'
# 出力: Storage Driver: overlayfs

# 複数レイヤーのビルドを実行し、ネストされたコンテナを実行する
mkdir -p /tmp/nested-test && cd /tmp/nested-test
cat <<'EOF' > Dockerfile
FROM alpine:3.20
RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
USER appuser
WORKDIR /app
RUN echo "nested-build-ok" > status.txt
CMD ["cat", "/app/status.txt"]
EOF

docker build -t nested-test:latest .
docker run --rm nested-test:latest
# 出力: nested-build-ok

kill -INT $DIND_PID
```

---

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

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)**: `root`、`sudo`、`unshare(CLONE_NEWUSER)`、`mount(2)`、`/dev/fuse`がブロックされている場合に、`gvx`が`docker:dind`をどのように実行するか。
3. **[gvxによるgVisorおよびネストされたサンドボックス内でのDocker-in-Docker実行](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**(本記事)
4. **[gvxによるAIエージェントRLロールアウトのための高速な隔離Docker環境](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: `gvx`と単一ファイルの`ext4lite`コピーオンライトスナップショットを使用して、100ミリ秒のリセットで数百の並列`SWE-bench`およびRLロールアウト環境を実行する方法。
