---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Ausführen von Docker-in-Docker innerhalb von gVisor und verschachtelten Sandboxes mit gvx
description: Warum die Installation von docker.io oder das Verschachteln von runsc innerhalb eines gVisor-Containers über modprobe, iptables-nft, overlay2, cgroups und seccomp EBUSY fehlschlägt, und wie gvx nested docker:dind sicher innerhalb eines isolierten User-Space-Kernels ohne --privileged ausführt.
language: de
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
---

## Schnelllösung: Sicheres Ausführen von `docker:dind` in einer unprivilegierten `gVisor` (`runsc`) Sandbox

Cloud-KI-Agentenplattformen, Kubernetes-`agent-sandbox`-Deployments (`RuntimeClass: gvisor`) und Evaluationsumgebungen für Reinforcement Learning isolieren nicht vertrauenswürdige Workloads gezielt in **gVisor (`runsc`)**-Containern, um den Host-Kernel zu schützen. Wenn ein Workload in einem standardmäßig unprivilegierten `runsc`-Container versucht, `docker.io` oder `docker-ce` per `apt-get` zu installieren und `dockerd` zu starten, schlägt dies fehl, selbst wenn er innerhalb des Containers `root` (`UID 0`) ist.

Mit **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) können Sie `docker:dind`, `docker build` und `docker compose` **sicher** direkt in einem unprivilegierten `gVisor` (`runsc`)-Container ausführen (oder `gvx` innerhalb von `gvx` verschachteln), **ohne die äußere `RuntimeClass` zu schwächen, Host-Sockets offenzulegen oder `--privileged` zu gewähren**.

> **Erhaltung und Stärkung der äußeren Sandbox-Isolation**: Das Ausführen von `rungv` innerhalb von `runsc` umgeht keine einzige Einschränkung, die durch die äußere `gVisor`-Sandbox erzwungen wird. Stattdessen erzeugt `gvx` einen zweiten, verschachtelten `gVisor`-`Sentry`-Kernel im Benutzerraum mit eigenem `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), eigenem In-Memory-`netstack` und einem `ext4lite`-Volume aus einer einzelnen Datei. Innere `dockerd`- und `runc`-Container sind doppelt gesandboxt: Ihre Systemaufrufe, Mounts und Rohpakete verlassen den inneren `gvx`-Benutzerraum-Kernel niemals.

```sh
# Innerhalb eines bestehenden gVisor (runsc)-Containers oder Kubernetes-Pods:
# Installieren Sie rungv aus einem SLSA-attestierten GitHub-Release-Archiv oder über 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 &

# Standard-Docker-Builds und Container gegen den inneren gvx-Socket ausführen
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}}'
# Ausgabe: overlayfs Alpine Linux ... (containerized)

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

---

## Fünf Gründe, warum die direkte Docker-Installation innerhalb von `gVisor` (`runsc`) fehlschlägt

Wenn ein Benutzer oder ein KI-Coding-Agent `apt-get install -y docker.io` (oder `docker-ce`) in einem Standard-`runsc`-Container ausführt und versucht, `dockerd` zu starten, stößt er auf fünf aufeinanderfolgende technische Hürden.

### 1. Laden von Kernelmodulen (`modprobe`) gegen den virtuellen Kernel `4.4.0`

Während der Paketinstallation und des `dockerd`-Starts führen Initialisierungsskripte `modprobe` aus, um `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` und `xt_conntrack` zu laden. Der äußere gVisor-Sentry blockiert `init_module(2)` und `finit_module(2)` und meldet eine virtuelle Kernelversion (`4.4.0`), für die kein Verzeichnis `/lib/modules/4.4.0` existiert:

```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) und `docker0`-Bridge-Fehler

Auf Ubuntu 22.04, Ubuntu 24.04 sowie Debian 11/12 wählt `apt-get install docker.io` `iptables-nft` (gestützt auf das Netlink-Subsystem `nf_tables`) als Standardalternative für `/usr/sbin/iptables`. In einem Standard-gVisor-Container ohne Netzwerkadministrationsrechte oder `nf_tables`-Netlink-Unterstützung stürzt `dockerd` bei der Initialisierung der NAT-Kette `DOCKER` und der Bridge `docker0` ab:

```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. `overlay2`-Fehler auf `gofer`- / `9p`-Rootfs und `vfs`-Speichererschöpfung

Sofern der äußere `runsc`-Administrator nicht explizit ein dediziertes `tmpfs`- oder Filestore-Mount unter `/var/lib/docker` vorkonfiguriert hat, wird das Container-Rootfs durch den `gofer` (`9p`/`lisafs`) von gVisor oder ein verschachteltes Overlay-Mount gestützt. Das Mounten von `overlay2` auf `/var/lib/docker` schlägt fehl:

```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
```

Wenn Benutzer oder Agenten versuchen, diesen Fehler zu umgehen, indem sie `dockerd --storage-driver=vfs` starten, deaktiviert Docker das Teilen von Copy-on-Write-Layern vollständig. Jede `RUN`-Anweisung in einem `Dockerfile` führt eine vollständige rekursive Kopie (`dirCopy`) des gesamten Root-Dateisystems durch. Dadurch werden aus einem 500-MB-Image über 10 Build-Schritte 5 GB Schreibvorgänge, und die Sandbox bricht mit `no space left on device` oder einem I/O-Timeout ab.

### 4. Ablehnung verschachtelter `runc`-Cgroup- und `/proc`-Mounts

Selbst wenn `dockerd` mit `dockerd --iptables=false --bridge=none --storage-driver=vfs` gezwungen wird zu starten, ruft `docker run` oder `docker build` `runc` auf, um den inneren Container zu erstellen. In einem standardmäßig unprivilegierten gVisor-Container schlägt `runc` beim Mounten von Cgroups, beim erneuten Mounten von `/proc` oder beim Laden innerer seccomp-Filter fehl:

```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. Verschachteltes `runsc` schlägt mit `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`) fehl

Wenn ein Benutzer versucht, eine weitere `runsc`-Instanz innerhalb von `runsc` mit der `systrap`-Plattform auszuführen, bricht die Initialisierung sofort ab:

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

Sowohl der Linux-Kernel als auch der gVisor-Sentry erlauben **höchstens einen** Listener `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) pro Task-Hierarchie. Da die äußere Sandbox diesen Listener-Slot bereits belegt, liefert der innere `seccomp(2)`-Aufruf `EBUSY` (`device or resource busy`) zurück.

---

## Wie `gvx` verschachtelte Ausführung innerhalb von `gVisor` löst

`gvx` adressiert alle fünf Hürden in seinem eigenen Benutzerraum-Kernel und seiner Plattformschicht. Dadurch kann `rungv` innerhalb eines äußeren `gVisor`-Containers (`runsc`) oder sogar rekursiv innerhalb eines weiteren `gvx`-Containers (`gvx` in `gvx`) ausgeführt werden.

![Verschachtelte gvx-Ausführung innerhalb eines äußeren gVisor](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Automatischer `systrap`-Fallback von `EBUSY` auf die Futex-Schleife `_RUN_SYSCALL_LOOP`

In Linux-`systrap` führt jeder Stub-Subprozess zwei Arten von Threads aus:

- **Gast-Ausführungsthreads (`sysmsg`)**: Fangen Gast-Systemaufrufe über `SECCOMP_RET_TRAP` (`SIGSYS`) und binär gepatchte `jmp rel32`-Trampoline (`syshandler`) ab und tauschen Registerzustände mit dem Sentry über gemeinsamen Speicher und `SYS_FUTEX` aus.
- **Ein Stub-`syscallThread` (`syscall_thread.go`)**: Führt Systemaufrufe des Adressraums (`mmap`, `munmap`, `clone`) im Stub-Subprozess im Auftrag des Sentry aus.

Im upstream gVisor unterstützt `syscallThread` zwei Weckmodi in `stub_amd64.s` und `stub_arm64.s`:

1. **Seccomp-User-Notify-Schleife (`_RUN_SECCOMP_LOOP = 6`, wenn `t.seccompNotify != nil`)**: Während `syscallThread.init()` installiert `t.installSeccompNotify()` einen Listener `SECCOMP_RET_USER_NOTIF` (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`), damit der Sentry Anfragen über `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` und `ioctl(SECCOMP_IOCTL_NOTIF_RECV)` verteilen kann.
2. **Futex-Systemaufrufschleife (`_RUN_SYSCALL_LOOP = 5`, wenn `t.seccompNotify == nil`)**: Der `syscallThread` führt `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`) aus und wartet auf `SYS_FUTEX` (`FUTEX_WAIT`) über dem gemeinsamen Speicherbereich aus zwei Seiten, `syscallSentryMessage` (`PROT_READ` im Stub) und `syscallStubMessage` (`PROT_READ | PROT_WRITE` im Stub).

Upstream gVisor ruft `t.installSeccompNotify()` bedingungslos auf, sobald `seccompNotifyIsSupported` wahr ist, und bricht die Erstellung des Stubs ab, wenn `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` `EBUSY` zurückgibt.

In `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` und `syscall_thread.go`) ruft `syscallThread.init()` `t.installSeccompNotifyIfFree()` auf:

```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
}
```

Wenn `installSeccompNotify()` `unix.EBUSY` zurückgibt, weil eine äußere `gVisor`- (`runsc`) oder `gvx`-Sandbox bereits den einzigen Listener-Slot der Filterkette des Prozesses belegt:

- setzt `installSeccompNotifyIfFree()` `seccompNotifyBusy` (`atomic.Bool`), protokolliert eine Warnung für den Prozess und lässt `t.seccompNotify == nil` für diesen Stub sowie für alle nachfolgenden Stubs.
- setzt in `syscallThread.detach()` (`syscall_thread_amd64.go` und `syscall_thread_arm64.go`) bei `t.seccompNotify == nil` den Wert `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) auf `amd64` (bzw. `regs.Regs[9] = _RUN_SYSCALL_LOOP` auf `arm64`) anstelle von `_RUN_SECCOMP_LOOP` (`6`).
- verteilt der innere Sentry in `syscallThread.syscall()` (`syscall_thread.go`) Stub-Systemaufrufe ausschließlich über gemeinsamen Speicher und `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Benachrichtige den Syscall-Thread über eine neue Systemaufrufanfrage.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Auf Antwort warten.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Da `SECCOMP_RET_TRAP`-Filter auf Gast-Threads beliebig tief im Stack wirken und `syscallThread` ohne Anforderung eines zweiten User-Notification-Listeners auf `_RUN_SYSCALL_LOOP` zurückfällt, läuft `gvx` sauber innerhalb von `runsc` und unterstützt rekursive Verschachtelung (`gvx` in `gvx`).

### 2. Natives `overlayfs` auf `ext4lite`-Volumes aus einer einzelnen Datei

Um zu vermeiden, `overlayfs` auf das Root-Dateisystem des äußeren gVisor-Containers zu mounten, das vom `gofer` (`9p`/`lisafs`) bereitgestellt wird, stützt `-v gvx-docker:/var/lib/docker` `/var/lib/docker` auf das interne Dateisystem **`ext4lite`** von `gvx`, das in einer einzelnen regulären Datei (`gvx-docker.ext4`) gespeichert ist.

Für das äußere `runsc` ist `gvx-docker.ext4` lediglich eine reguläre Datei, auf die über `pread64(2)` und `pwrite64(2)` zugegriffen wird. Innerhalb von `gvx` stellt `ext4lite` ein JBD2-journalisiertes `ext4`-Dateisystem mit den erweiterten Attributen `trusted.overlay.*` und `security.capability`, Whiteout-Knoten als Zeichengeräte (`0:0`), `d_type`-Verzeichniseinträgen und `RENAME_EXCHANGE` bereit. Dadurch kann der innere `dockerd` seinen nativen Speichertreiber `overlayfs` verwenden.

### 3. Eigenständiges `netstack` mit `docker0` und `iptables-legacy`

Innerhalb von `gvx` interagiert `dockerd` nicht mit den Netzwerkschnittstellen oder den netfilter-Tabellen des äußeren `runsc`-Containers. Stattdessen:

- erstellt `dockerd` seine Bridge `docker0` und seine `veth`-Paare innerhalb des eigenen `netstack` von `gvx` im Benutzerraum.
- werden alle `iptables-legacy`-Regeln (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) gegen die interne netfilter-Implementierung von `gvx` ausgeführt.
- mounten verschachtelte `runc`-Container ihre Hierarchien `/proc`, `/sys` und `/sys/fs/cgroup` in der In-Memory-Mount-Tabelle `VFS2` von `gvx` und rufen niemals den Systemaufruf `mount(2)` des äußeren `runsc` auf.

---

## Verifizierung von verschachteltem `docker:dind` innerhalb von `gVisor` (oder `gvx` in `gvx`)

Sie können beide Szenarien (`rungv` in `runsc` oder `rungv` in `rungv`) mit den folgenden Befehlen überprüfen:

```sh
# Innerhalb eines äußeren gVisor (runsc)-Containers oder eines äußeren rungv-Containers:
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

# Prüfen, ob der innere Docker-Daemon den nativen overlayfs (nicht vfs) verwendet
docker info --format 'Storage Driver: {{.Driver}}'
# Ausgabe: Storage Driver: overlayfs

# Einen mehrschichtigen Build ausführen und einen verschachtelten Container starten
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
# Ausgabe: nested-build-ok

kill -INT $DIND_PID
```

---

## Weiterführende Lektüre in dieser Reihe

1. **[gvx: Ausführung von Linux-Containern, Docker und Kubernetes in einem einzigen unprivilegierten Prozess](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Überblick über `gvx` unter Windows (`whpx`, `systrap`), macOS (`hvf`) und Linux (`systrap`).
2. **[Docker, docker build und docker compose ohne Root oder User Namespaces ausführen](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Wie `gvx` `docker:dind` ausführt, wenn `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` und `/dev/fuse` blockiert sind.
3. **[Docker-in-Docker innerhalb von gVisor und verschachtelten Sandboxes mit gvx ausführen](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (dieser Beitrag)
4. **[Schnelle isolierte Docker-Umgebungen für RL-Rollouts von KI-Agenten mit gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Wie Sie `gvx` und Copy-on-Write-Snapshots aus einzelnen `ext4lite`-Dateien verwenden, um Hunderte parallele `SWE-bench`- und RL-Rollout-Umgebungen mit 100-Millisekunden-Resets auszuführen.
