---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Att köra Docker-in-Docker inuti gVisor och kapslade Sandboxes med gvx
description: Varför installation av docker.io eller nästling av runsc inom en gVisor-container misslyckas med modprobe, iptables-nft, overlay2, cgroups och seccomp EBUSY, och hur gvx säkert kör nästlad docker:dind inuti en isolerad kärna i användarutrymmet utan --privileged.
language: sv
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
---

## Snabb lösning: Säker körning av `docker:dind` i en ej privilegierad `gVisor` (`runsc`)-sandlåda

Molnbaserade AI-agentplattformar, Kubernetes-deployeringar av `agent-sandbox` (`RuntimeClass: gvisor`) och utvärderingsmiljöer för reinforcement learning isolerar opålitliga arbetslaster i **gVisor (`runsc`)**-containrar för att specifikt skydda värdens kärna. När en arbetslast i en standardmässig ej privilegierad `runsc`-container försöker installera `docker.io` eller `docker-ce` via `apt-get` och starta `dockerd` misslyckas den, även om den har `root` (`UID 0`) inuti containern.

Med **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) kan du **säkert** köra `docker:dind`, `docker build` och `docker compose` direkt inuti en ej privilegierad `gVisor` (`runsc`)-container (eller kapsla `gvx` inuti `gvx`) **utan att försvaga den yttre `RuntimeClass`, exponera värdens sockets eller använda `--privileged`**.

> **Bevarande och förstärkning av isolering i yttre sandlåda**: Att köra `rungv` inuti `runsc` innebär inte att någon restriktion som upprätthålls av den yttre `gVisor`-sandlådan kringgås. I stället skapar `gvx` en andra, kapslad användarrymdskärna av typen `gVisor Sentry` med sin egen `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), sin egen minnesbaserade `netstack` och en `ext4lite`-volym i en enda fil. Inre `dockerd`- och `runc`-containrar är dubbelt sandlådeisolerade: deras systemanrop, monteringar och råa paket lämnar aldrig den inre `gvx`-kärnan i användarrymden.

```sh
# Inuti en befintlig gVisor (runsc)-container eller Kubernetes Pod:
# Installera rungv från ett SLSA-attesterat GitHub-releasearkiv eller 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 &

# Kör vanliga Docker-byggen och containrar mot den inre gvx-socketen
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}}'
# Utdata: overlayfs Alpine Linux ... (containeriserad)

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

---

## Fem skäl till varför direkt Docker-installation misslyckas inuti `gVisor` (`runsc`)

När en användare eller en AI-kodningsagent kör `apt-get install -y docker.io` (eller `docker-ce`) inuti en standard-`runsc`-container och försöker starta `dockerd`, möter den fem på varandra följande tekniska hinder.

### 1. Laddning av kärnmoduler (`modprobe`) mot virtuell kärna `4.4.0`

Under paketinstallation och uppstart av `dockerd` kör initialiseringsskript `modprobe` för att ladda `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` och `xt_conntrack`. Den yttre gVisor Sentry blockerar `init_module(2)` och `finit_module(2)` och rapporterar en virtuell kärnversion (`4.4.0`) som saknar katalogen `/lib/modules/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. Fel i `iptables-nft` (`nf_tables` Netlink) och bryggan `docker0`

På Ubuntu 22.04, Ubuntu 24.04 och Debian 11/12 väljer `apt-get install docker.io` `iptables-nft` (baserat på undersystemet `nf_tables` netlink) som standardalternativ för `/usr/sbin/iptables`. I en standard gVisor-container utan nätverksadministratörsbehörighet eller stöd för `nf_tables` netlink kraschar `dockerd` vid initiering av NAT-kedjan `DOCKER` och bryggan `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. Fel vid `overlay2` på `gofer`/`9p`-rootfs och överbelastning av disk vid `vfs`

Om inte administratören för den yttre `runsc` uttryckligen har konfigurerat en dedikerad `tmpfs`- eller filestore-montering på `/var/lib/docker` backas containerns rootfs av gVisors `gofer` (`9p`/`lisafs`) eller av en kapslad overlay-montering. Montering av `overlay2` ovanpå `/var/lib/docker` misslyckas:

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

När användare eller agenter försöker kringgå detta fel genom att starta `dockerd --storage-driver=vfs` inaktiveras delning av Copy-on-Write-lager helt. Varje `RUN`-instruktion i en `Dockerfile` utför en fullständig rekursiv kopiering (`dirCopy`) av hela rotfilsystemet, vilket multiplicerar en avbildning på 500 MB över 10 byggsteg till 5 GB skrivningar och avslutar sandlådan med `no space left on device` eller en I/O-timeout.

### 4. Avvisningar av kapslad `runc` för cgroup- och `/proc`-montering

Även om `dockerd` tvingas starta med `dockerd --iptables=false --bridge=none --storage-driver=vfs` anropar `docker run` eller `docker build` `runc` för att skapa den inre containern. I en standardmässig ej privilegierad gVisor-container misslyckas `runc` vid montering av cgroups, ommontering av `/proc` eller laddning av inre seccomp-filter:

```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. Kapslad `runsc` misslyckas med `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Om en användare försöker köra en annan `runsc`-instans inuti `runsc` med plattformen `systrap` avbryts initieringen omedelbart:

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

Både Linux-kärnan och gVisor Sentry tillåter **högst en** `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`)-lyssnare per task-hierarki. Eftersom den yttre sandlådan redan innehar den lyssnarplatsen returnerar det inre `seccomp(2)`-anropet `EBUSY` (`device or resource busy`).

---

## Så löser `gvx` kapslad körning inuti `gVisor`

`gvx` hanterar alla fem hindren inom sin egen användarrymdskärna och plattformslager, vilket gör det möjligt för `rungv` att köras inuti en yttre `gVisor` (`runsc`)-container eller till och med rekursivt inuti en annan `gvx`-container (`gvx` i `gvx`).

![Kapslad gvx-körning inuti yttre gVisor](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Automatisk återgång från `systrap` vid `EBUSY` till futex-slingan `_RUN_SYSCALL_LOOP`

I Linux `systrap` kör varje stub-underprocess två typer av trådar:

- **Trådar för gästkörning (`sysmsg`)**: Fångar gästens systemanrop via `SECCOMP_RET_TRAP` (`SIGSYS`) och binärt patchade `jmp rel32`-trampoliner (`syshandler`), och utbyter registertillstånd med Sentry via delat minne och `SYS_FUTEX`.
- **En stub-`syscallThread` (`syscall_thread.go`)**: Kör adressrymdens systemanrop (`mmap`, `munmap`, `clone`) inuti stub-underprocessen för Sentrys räkning.

I upstream gVisor stöder `syscallThread` två väckningslägen i `stub_amd64.s` och `stub_arm64.s`:

1. **Seccomp User-Notify-slinga (`_RUN_SECCOMP_LOOP = 6`, när `t.seccompNotify != nil`)**: Under `syscallThread.init()` installerar `t.installSeccompNotify()` en `SECCOMP_RET_USER_NOTIF`-lyssnare (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`) så att Sentry kan skicka förfrågningar via `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` och `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Futex-slinga för systemanrop (`_RUN_SYSCALL_LOOP = 5`, när `t.seccompNotify == nil`)**: `syscallThread` kör `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`) och väntar på `SYS_FUTEX` (`FUTEX_WAIT`) över det delade 2-sidiga området `syscallSentryMessage` (`PROT_READ` i stubben) och `syscallStubMessage` (`PROT_READ | PROT_WRITE` i stubben).

Upstream gVisor anropar ovillkorligen `t.installSeccompNotify()` när `seccompNotifyIsSupported` är sant, och avbryter skapandet av stubben om `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` returnerar `EBUSY`.

I `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` och `syscall_thread.go`) anropar `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
}
```

När `installSeccompNotify()` returnerar `unix.EBUSY` eftersom en yttre `gVisor` (`runsc`) eller yttre `gvx`-sandlåda redan innehar filterkedjans enda lyssnarplats för processen:

- Sätter `installSeccompNotifyIfFree()` `seccompNotifyBusy` (`atomic.Bool`), loggar en varning för processen och lämnar `t.seccompNotify == nil` för den stubben och alla efterföljande stubbar.
- I `syscallThread.detach()` (`syscall_thread_amd64.go` och `syscall_thread_arm64.go`) sätter `t.seccompNotify == nil` `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) på `amd64` (eller `regs.Regs[9] = _RUN_SYSCALL_LOOP` på `arm64`) i stället för `_RUN_SECCOMP_LOOP` (`6`).
- I `syscallThread.syscall()` (`syscall_thread.go`) skickar den inre Sentry stubbens systemanrop enbart via delat minne och `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Meddela syscall-tråden om en ny begäran om systemanrop.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Vänta på svar.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Eftersom `SECCOMP_RET_TRAP`-filter på gästtrådar kan staplas godtyckligt djupt och `syscallThread` faller tillbaka till `_RUN_SYSCALL_LOOP` utan att begära en andra användarnotifieringslyssnare, kör `gvx` problemfritt inuti `runsc` och stöder rekursiv kapsling (`gvx` i `gvx`).

### 2. Inbyggt `overlayfs` på `ext4lite`-volymer i en enda fil

För att undvika att montera `overlayfs` ovanpå den yttre gVisor-containerns rotfilsystem på `gofer` (`9p`/`lisafs`) backar `-v gvx-docker:/var/lib/docker` `/var/lib/docker` med `gvx`:s interna filsystem **`ext4lite`**, lagrat i en enda vanlig fil (`gvx-docker.ext4`).

För den yttre `runsc` är `gvx-docker.ext4` bara en vanlig fil som nås via `pread64(2)` och `pwrite64(2)`. Inuti `gvx` tillhandahåller `ext4lite` ett `ext4`-filsystem med JBD2-journal, med utökade attribut `trusted.overlay.*` och `security.capability`, whiteout-noder av teckenenhetstyp (`0:0`), `d_type`-katalogposter och `RENAME_EXCHANGE`. Detta gör att den inre `dockerd` kan använda sin inbyggda `overlayfs`-lagringsdrivrutin.

### 3. Självständig `netstack` med `docker0` och `iptables-legacy`

Inuti `gvx` interagerar inte `dockerd` med den yttre `runsc`-containerns nätverksgränssnitt eller netfilter-tabeller. I stället:

- Skapar `dockerd` sin bro `docker0` och `veth`-par inuti `gvx`:s egen användarrymds-`netstack`.
- Alla regler för `iptables-legacy` (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) körs mot `gvx`:s interna netfilter-implementation.
- Kapslade `runc`-containrar monterar sina hierarkier `/proc`, `/sys` och `/sys/fs/cgroup` inuti `gvx`:s minnesbaserade `VFS2`-monteringstabell, och anropar aldrig det yttre `runsc`-systemanropet `mount(2)`.

---

## Verifiering av kapslad `docker:dind` inuti `gVisor` (eller `gvx` i `gvx`)

Du kan verifiera båda scenarierna (`rungv` inuti `runsc`, eller `rungv` inuti `rungv`) med följande kommandon:

```sh
# Inuti en yttre gVisor (runsc)-container eller en yttre 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

# Verifiera att den inre Docker-demonen använder native overlayfs (inte vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Utdata: Storage Driver: overlayfs

# Kör en flerlagersbygge och kör en kapslad 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
# Utdata: nested-build-ok

kill -INT $DIND_PID
```

---

## Fler läsningar i denna serie

1. **[gvx: Running Linux Containers, Docker, and Kubernetes in One Unprivileged Process](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Översikt över `gvx` på Windows (`whpx`, `systrap`), macOS (`hvf`) och Linux (`systrap`).
2. **[Running Docker, docker build, and docker compose Without Root or User Namespaces](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Hur `gvx` kör `docker:dind` när `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` och `/dev/fuse` är blockerade.
3. **[Running Docker-in-Docker Inside gVisor and Nested Sandboxes with gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (detta inlägg)
4. **[Fast Isolated Docker Environments for AI Agent RL Rollouts with gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Hur du använder `gvx` och `ext4lite`-ögonblicksbilder med copy-on-write i en enda fil för att köra hundratals parallella `SWE-bench`- och RL-rollout-miljöer med återställningar på 100 millisekunder.
