---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Docker-in-Docker Uitvoeren binnen gVisor en Geneste Sandboxes met gvx
description: Waarom het installeren van docker.io of het nesten van runsc binnen een gVisor-container mislukt door modprobe, iptables-nft, overlay2, cgroups en seccomp EBUSY, en hoe gvx nested docker:dind veilig binnen een geïsoleerde kernel in gebruikersruimte uitvoert zonder --privileged.
language: nl
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
---

## Snelle oplossing: `docker:dind` veilig uitvoeren binnen een niet-geprivilegieerde `gVisor` (`runsc`) sandbox

Cloud-AI-agentplatforms, Kubernetes `agent-sandbox`-implementaties (`RuntimeClass: gvisor`) en evaluatieomgevingen voor reinforcement learning isoleren onbetrouwbare workloads binnen **gVisor (`runsc`)**-containers, specifiek om de host-kernel te beschermen. Wanneer een workload binnen een standaard niet-geprivilegieerde `runsc`-container probeert `docker.io` of `docker-ce` te installeren via `apt-get` en `dockerd` te starten, mislukt dit, ook al heeft het proces `root` (`UID 0`) binnen de container.

Met **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) kunt u `docker:dind`, `docker build` en `docker compose` **veilig** rechtstreeks uitvoeren binnen een niet-geprivilegieerde `gVisor` (`runsc`)-container (of `gvx` binnen `gvx` nesten), **zonder** de buitenste `RuntimeClass` te verzwakken, host-sockets bloot te stellen of `--privileged` toe te kennen.

> **Behoud en versterking van de isolatie van de buitenste sandbox**: Het uitvoeren van `rungv` binnen `runsc` omzeilt geen enkele beperking die door de buitenste `gVisor`-sandbox wordt afgedwongen. In plaats daarvan maakt `gvx` een tweede, genest user-space gVisor-`Sentry`-kernel aan, met een eigen `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), een eigen in-memory `netstack` en een enkelbestands `ext4lite`-volume. Interne `dockerd`- en `runc`-containers zijn dubbel gesandboxed: hun systeemaanroepen, mounts en ruwe pakketten verlaten de interne user-space-kernel van `gvx` nooit.

```sh
# Binnen een bestaande gVisor (runsc)-container of Kubernetes Pod:
# Installeer rungv uit een SLSA-geattesteerd GitHub-releasearchief of via 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 &

# Voer standaard Docker-builds en -containers uit via de interne gvx-socket
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}}'
# Uitvoer: overlayfs Alpine Linux ... (gecontaineriseerd)

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

---

## Vijf redenen waarom directe Docker-installatie mislukt binnen `gVisor` (`runsc`)

Wanneer een gebruiker of AI-codeeragent `apt-get install -y docker.io` (of `docker-ce`) uitvoert binnen een standaard `runsc`-container en probeert `dockerd` te starten, loopt die tegen vijf opeenvolgende technische barrières aan.

### 1. Laden van kernelmodules (`modprobe`) tegen de virtuele kernel `4.4.0`

Tijdens de installatie van pakketten en het opstarten van `dockerd` voeren initialisatiescripts `modprobe` uit om `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` en `xt_conntrack` te laden. De buitenste gVisor-Sentry blokkeert `init_module(2)` en `finit_module(2)` en rapporteert een virtuele kernelversie (`4.4.0`) waarvoor geen map `/lib/modules/4.4.0` bestaat:

```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. Fouten bij `iptables-nft` (`nf_tables` Netlink) en de `docker0`-bridge

Op Ubuntu 22.04, Ubuntu 24.04 en Debian 11/12 kiest `apt-get install docker.io` `iptables-nft` (ondersteund door het `nf_tables` netlink-subsysteem) als standaardalternatief voor `/usr/sbin/iptables`. In een standaard gVisor-container zonder netwerkbeheerprivileges of `nf_tables` netlink-ondersteuning loopt `dockerd` vast bij het initialiseren van de `DOCKER` NAT-chain en de `docker0`-bridge:

```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. Mislukking van `overlay2` op `gofer`/`9p`-rootfs en `vfs`-schijfuitputting

Tenzij de beheerder van de buitenste `runsc` expliciet een speciale `tmpfs`- of filestore-mount op `/var/lib/docker` heeft geconfigureerd, wordt de container-rootfs ondersteund door de `gofer` (`9p`/`lisafs`) van gVisor of door een geneste overlay-mount. Het mounten van `overlay2` bovenop `/var/lib/docker` mislukt:

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

Wanneer gebruikers of agents deze fout proberen te omzeilen door `dockerd --storage-driver=vfs` te starten, schakelt Docker het delen van Copy-on-Write-lagen volledig uit. Elke `RUN`-instructie in een `Dockerfile` voert dan een volledige recursieve kopie (`dirCopy`) uit van het gehele root-bestandssysteem. Hierdoor loopt een image van 500 MB over 10 buildstappen op tot 5 GB aan schrijfacties, waarna de sandbox eindigt met `no space left on device` of een I/O-timeout.

### 4. Afwijzing van geneste `runc`-cgroup- en `/proc`-mounts

Zelfs als `dockerd` wordt gedwongen te starten met `dockerd --iptables=false --bridge=none --storage-driver=vfs`, roept `docker run` of `docker build` `runc` aan om de interne container te maken. In een standaard niet-geprivilegieerde gVisor-container mislukt `runc` bij het mounten van cgroups, het opnieuw mounten van `/proc` of het laden van interne seccomp-filters:

```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. Genest `runsc` mislukt met `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Als een gebruiker probeert een andere `runsc`-instantie binnen `runsc` te draaien met het `systrap`-platform, breekt de initialisatie direct af:

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

Zowel de Linux-kernel als de gVisor-Sentry staan **maximaal één** `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`)-listener per taakhiërarchie toe. Omdat de buitenste sandbox die listener-plek al bezet houdt, geeft de interne `seccomp(2)`-aanroep `EBUSY` terug (`device or resource busy`).

---

## Hoe `gvx` geneste uitvoering binnen `gVisor` mogelijk maakt

`gvx` pakt alle vijf barrières aan binnen de eigen user-space-kernel en platformlaag, waardoor `rungv` kan draaien binnen een buitenste `gVisor` (`runsc`)-container of zelfs recursief binnen een andere `gvx`-container (`gvx` in `gvx`).

![Genest gvx-gebruik binnen een buitenste gVisor](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Automatische `systrap`-terugval van `EBUSY` naar de `_RUN_SYSCALL_LOOP` futex-lus

In Linux `systrap` draait elk stub-subproces twee soorten threads:

- **Threads voor gastuitvoering (`sysmsg`)**: Onderscheppen gast-systeemaanroepen via `SECCOMP_RET_TRAP` (`SIGSYS`) en binair gepatchte `jmp rel32`-trampolines (`syshandler`), en wisselen registerstatus uit met de Sentry via gedeeld geheugen en `SYS_FUTEX`.
- **Een stub-`syscallThread` (`syscall_thread.go`)**: Voert adresruimte-systeemaanroepen (`mmap`, `munmap`, `clone`) uit binnen het stub-subproces namens de Sentry.

In upstream gVisor ondersteunt `syscallThread` twee wake-upmodi in `stub_amd64.s` en `stub_arm64.s`:

1. **Seccomp User-Notify-lus (`_RUN_SECCOMP_LOOP = 6`, wanneer `t.seccompNotify != nil`)**: Tijdens `syscallThread.init()` installeert `t.installSeccompNotify()` een `SECCOMP_RET_USER_NOTIF`-listener (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`), zodat de Sentry verzoeken kan verzenden via `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` en `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Futex-systeemaanroeplus (`_RUN_SYSCALL_LOOP = 5`, wanneer `t.seccompNotify == nil`)**: De `syscallThread` voert `syscall_loop` uit (`stub_amd64.s` / `stub_arm64.s`) en slaapt op `SYS_FUTEX` (`FUTEX_WAIT`) over het gedeelde gebied van 2 pagina's met `syscallSentryMessage` (`PROT_READ` in de stub) en `syscallStubMessage` (`PROT_READ | PROT_WRITE` in de stub).

Upstream gVisor roept `t.installSeccompNotify()` onvoorwaardelijk aan wanneer `seccompNotifyIsSupported` waar is, en breekt het aanmaken van de stub af wanneer `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` `EBUSY` teruggeeft.

In `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` en `syscall_thread.go`) roept `syscallThread.init()` `t.installSeccompNotifyIfFree()` aan:

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

Wanneer `installSeccompNotify()` `unix.EBUSY` teruggeeft omdat een buitenste `gVisor` (`runsc`)- of buitenste `gvx`-sandbox de enkele listener-plek in de filterketen van het proces al bezet houdt:

- `installSeccompNotifyIfFree()` zet `seccompNotifyBusy` (`atomic.Bool`), logt één waarschuwing voor het proces en laat `t.seccompNotify == nil` voor die stub en alle volgende stubs.
- In `syscallThread.detach()` (`syscall_thread_amd64.go` en `syscall_thread_arm64.go`) zet `t.seccompNotify == nil` `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) op `amd64` (of `regs.Regs[9] = _RUN_SYSCALL_LOOP` op `arm64`) in plaats van `_RUN_SECCOMP_LOOP` (`6`).
- In `syscallThread.syscall()` (`syscall_thread.go`) verstuurt de interne Sentry stub-systeemaanroepen uitsluitend via gedeeld geheugen en `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Meld de syscall-thread over een nieuw systeemaanroepverzoek.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Wacht op antwoord.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Omdat `SECCOMP_RET_TRAP`-filters op gasttreads willekeurig diep gestapeld kunnen zijn en `syscallThread` terugvalt op `_RUN_SYSCALL_LOOP` zonder een tweede user-notification-listener aan te vragen, draait `gvx` zonder problemen binnen `runsc` en ondersteunt het recursief nesten (`gvx` in `gvx`).

### 2. Native `overlayfs` op enkelbestands `ext4lite`-volumes

Om te voorkomen dat `overlayfs` bovenop het `gofer` (`9p`/`lisafs`)-rootbestandssysteem van de buitenste gVisor-container wordt gemount, ondersteunt `-v gvx-docker:/var/lib/docker` `/var/lib/docker` met het interne bestandssysteem **`ext4lite`** van `gvx`, opgeslagen in één regulier bestand (`gvx-docker.ext4`).

Voor de buitenste `runsc` is `gvx-docker.ext4` slechts één regulier bestand dat wordt benaderd via `pread64(2)` en `pwrite64(2)`. Binnen `gvx` biedt `ext4lite` een JBD2-gejournaled `ext4`-bestandssysteem met `trusted.overlay.*`- en `security.capability`-uitgebreide attributen, whiteout-knooppunten voor tekenapparaten (`0:0`), `d_type`-directoryvermeldingen en `RENAME_EXCHANGE`. Hierdoor kan de interne `dockerd` zijn native `overlayfs`-opslagstuurprogramma gebruiken.

### 3. Zelfstandige `netstack` met `docker0` en `iptables-legacy`

Binnen `gvx` communiceert `dockerd` niet met de netwerkinterfaces of netfilter-tabellen van de buitenste `runsc`-container. In plaats daarvan:

- `dockerd` maakt zijn `docker0`-bridge en `veth`-paren aan binnen de eigen user-space `netstack` van `gvx`.
- Alle `iptables-legacy`-regels (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) worden uitgevoerd tegen de interne netfilter-implementatie van `gvx`.
- Geneste `runc`-containers mounten hun `/proc`-, `/sys`- en `/sys/fs/cgroup`-hiërarchieën binnen de in-memory `VFS2`-mounttabel van `gvx`, zonder ooit de `mount(2)`-systeemaanroep van de buitenste `runsc` aan te roepen.

---

## Verifiëren van geneste `docker:dind` binnen `gVisor` (of `gvx` in `gvx`)

U kunt beide scenario's (`rungv` binnen `runsc` of `rungv` binnen `rungv`) verifiëren met de volgende commando's:

```sh
# Binnen een buitenste gVisor (runsc)-container of een buitenste rungv-container:
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

# Controleer of de interne Docker-daemon native overlayfs gebruikt (en niet vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Uitvoer: Storage Driver: overlayfs

# Voer een build met meerdere lagen uit en start een geneste container
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
# Uitvoer: nested-build-ok

kill -INT $DIND_PID
```

---

## Verder lezen in deze reeks

1. **[gvx: Linux-containers, Docker en Kubernetes uitvoeren in één niet-geprivilegieerd proces](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Overzicht van `gvx` op Windows (`whpx`, `systrap`), macOS (`hvf`) en Linux (`systrap`).
2. **[Docker, docker build en docker compose uitvoeren zonder root of user namespaces](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Hoe `gvx` `docker:dind` uitvoert wanneer `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` en `/dev/fuse` zijn geblokkeerd.
3. **[Docker-in-Docker binnen gVisor en geneste sandboxes uitvoeren met gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (dit bericht)
4. **[Snelle geïsoleerde Docker-omgevingen voor RL-rollouts van AI-agents met gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Hoe u `gvx` en enkelbestands `ext4lite`-copy-on-write-snapshots gebruikt om honderden parallelle `SWE-bench`- en RL-rolloutomgevingen uit te voeren met resets van 100 milliseconden.
