---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Kjøring av Docker-in-Docker inne i gVisor og Nested Sandboxes med gvx
description: Hvorfor installasjon av docker.io eller nesting av runsc inne i en gVisor-container feiler på tvers av modprobe, iptables-nft, overlay2, cgroups og seccomp EBUSY, og hvordan gvx sikkert kjører nestet docker:dind inne i en isolert user-space kernel uten --privileged.
language: "no"
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
---

## Rask løsning: Sikker kjøring av `docker:dind` inne i en uprivilegert `gVisor` (`runsc`)-sandkasse

Plattformer for skybaserte AI-agenter, Kubernetes `agent-sandbox`-distribusjoner (`RuntimeClass: gvisor`) og evalueringsrammeverk for forsterkningslæring isolerer upålitelige arbeidslaster inne i **gVisor (`runsc`)**-containere, spesifikt for å beskytte vertskjernen. Når en arbeidslast i en standard uprivilegert `runsc`-container forsøker å installere `docker.io` eller `docker-ce` via `apt-get` og starte `dockerd`, mislykkes den, selv om den har `root` (`UID 0`) inne i containeren.

Med **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) kan du **sikkert** kjøre `docker:dind`, `docker build` og `docker compose` direkte inne i en uprivilegert `gVisor` (`runsc`)-container (eller nøste `gvx` inne i `gvx`) **uten å svekke den ytre `RuntimeClass`, eksponere vertssockets eller gi `--privileged`**.

> **Bevaring og styrking av ytre sandkasseisolasjon**: Å kjøre `rungv` inne i `runsc` omgår ikke en eneste begrensning håndhevet av den ytre `gVisor`-sandkassen. I stedet oppretter `gvx` en andre, nøstet brukerromsbasert gVisor `Sentry`-kjerne med sin egen `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), sin egen minnebaserte `netstack` og et enkelt-fil `ext4lite`-volum. Indre `dockerd`- og `runc`-containere er dobbelt sandkassert: deres systemkall, monteringer og rå pakker forlater aldri den indre `gvx`-brukerromskjernen.

```sh
# Inne i en eksisterende gVisor (runsc)-container eller Kubernetes-pod:
# Installer rungv fra et SLSA-attestert GitHub-utgivelsesarkiv 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 &

# Kjør standard Docker-bygg og -containere mot den indre 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 ... (containerisert)

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

---

## Fem grunner til at direkte Docker-installasjon mislykkes inne i `gVisor` (`runsc`)

Når en bruker eller en AI-kodeagent kjører `apt-get install -y docker.io` (eller `docker-ce`) inne i en standard `runsc`-container og forsøker å starte `dockerd`, møter den fem sekvensielle tekniske barrierer.

### 1. Innlasting av kjernemoduler (`modprobe`) mot virtuell kjerne `4.4.0`

Under pakkeinstallasjon og oppstart av `dockerd` kjører initialiseringsskript `modprobe` for å laste `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` og `xt_conntrack`. Den ytre gVisor Sentry blokkerer `init_module(2)` og `finit_module(2)` og rapporterer en virtuell kjerneversjon (`4.4.0`) som ikke har noen `/lib/modules/4.4.0`-katalog:

```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. Feil med `iptables-nft` (`nf_tables` Netlink) og `docker0`-bridge

På Ubuntu 22.04, Ubuntu 24.04 og Debian 11/12 velger `apt-get install docker.io` `iptables-nft` (støttet av `nf_tables`-netlink-subsystemet) som standard alternativ for `/usr/sbin/iptables`. I en standard gVisor-container uten nettverksadministratorprivilegier eller støtte for `nf_tables` netlink, krasjer `dockerd` under initialisering av `DOCKER` NAT-kjeden og `docker0`-bridgen:

```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`-feil på `gofer`/`9p`-rotfilsystem og utmattelse av disk ved `vfs`

Med mindre den ytre `runsc`-administratoren eksplisitt har forhåndskonfigurert en dedikert `tmpfs`- eller filestore-montering på `/var/lib/docker`, støttes containerens rotfilsystem av gVisors `gofer` (`9p`/`lisafs`) eller en nøstet overlay-montering. Montering av `overlay2` oppå `/var/lib/docker` mislykkes:

```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 brukere eller agenter forsøker å omgå denne feilen ved å starte `dockerd --storage-driver=vfs`, deaktiverer Docker fullstendig Copy-on-Write-deling av lag. Hver `RUN`-instruksjon i en `Dockerfile` utfører en fullstendig rekursiv kopiering (`dirCopy`) av hele rotfilsystemet, og multipliserer et 500 MB stort image over 10 byggesteg til 5 GB med skriving, og avslutter sandkassen med `no space left on device` eller en I/O-tidsavbrudd.

### 4. Avvisning av nøstet `runc`-cgroup og montering av `/proc`

Selv om `dockerd` tvinges til å starte med `dockerd --iptables=false --bridge=none --storage-driver=vfs`, kaller `docker run` eller `docker build` `runc` for å opprette den indre containeren. I en standard uprivilegert gVisor-container mislykkes `runc` ved montering av cgroups, ommontering av `/proc` eller innlasting av indre seccomp-filtre:

```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. Nøstet `runsc` mislykkes med `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Hvis en bruker forsøker å kjøre en annen `runsc`-instans inne i `runsc` ved bruk av `systrap`-plattformen, avbrytes initialiseringen umiddelbart:

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

Både Linux-kjernen og gVisor Sentry tillater **høyst én** `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`)-lytter per oppgavehierarki. Fordi den ytre sandkassen allerede har den lytterplassen, returnerer det indre `seccomp(2)`-kallet `EBUSY` (`device or resource busy`).

---

## Hvordan `gvx` løser nøstet kjøring inne i `gVisor`

`gvx` håndterer alle fem barrierene inne i sin egen brukerrom-kjerne og plattformlag, og gjør det mulig for `rungv` å kjøre inne i en ytre `gVisor` (`runsc`)-container, eller til og med rekursivt inne i en annen `gvx`-container (`gvx` i `gvx`).

![Nøstet gvx-kjøring inne i ytre gVisor](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Automatisk `systrap`-reserveløsning fra `EBUSY` til `_RUN_SYSCALL_LOOP`-futex-løkken

I Linux `systrap` kjører hver stub-underprosess to typer tråder:

- **Gjesteutførelsestråder (`sysmsg`)**: Fanger gjestens systemkall via `SECCOMP_RET_TRAP` (`SIGSYS`) og binært patchede `jmp rel32`-trampoliner (`syshandler`), og utveksler registertilstand med Sentry over delt minne og `SYS_FUTEX`.
- **En stub-`syscallThread` (`syscall_thread.go`)**: Utfører adresserommets systemkall (`mmap`, `munmap`, `clone`) inne i stub-underprosessen på vegne av Sentry.

I upstream gVisor støtter `syscallThread` to vekkingsmodi i `stub_amd64.s` og `stub_arm64.s`:

1. **Seccomp User-Notify-løkke (`_RUN_SECCOMP_LOOP = 6`, når `t.seccompNotify != nil`)**: Under `syscallThread.init()` installerer `t.installSeccompNotify()` en `SECCOMP_RET_USER_NOTIF`-lytter (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`), slik at Sentry kan sende forespørsler via `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` og `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Futex-systemkalløkke (`_RUN_SYSCALL_LOOP = 5`, når `t.seccompNotify == nil`)**: `syscallThread` kjører `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`) og sover på `SYS_FUTEX` (`FUTEX_WAIT`) over det delte 2-sides `syscallSentryMessage`-området (`PROT_READ` i stub) og `syscallStubMessage`-området (`PROT_READ | PROT_WRITE` i stub).

Upstream gVisor kaller ubetinget `t.installSeccompNotify()` når `seccompNotifyIsSupported` er sann, og avbryter opprettelsen av stub hvis `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` returnerer `EBUSY`.

I `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` og `syscall_thread.go`) kaller `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()` returnerer `unix.EBUSY` fordi en ytre `gVisor` (`runsc`)- eller ytre `gvx`-sandkasse allerede har prosessfilterkjedens eneste lytterplass:

- `installSeccompNotifyIfFree()` setter `seccompNotifyBusy` (`atomic.Bool`), logger én advarsel for prosessen, og lar `t.seccompNotify == nil` være for den stuben og alle påfølgende stubber.
- I `syscallThread.detach()` (`syscall_thread_amd64.go` og `syscall_thread_arm64.go`) setter `t.seccompNotify == nil` `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) på `amd64` (eller `regs.Regs[9] = _RUN_SYSCALL_LOOP` på `arm64`) i stedet for `_RUN_SECCOMP_LOOP` (`6`).
- I `syscallThread.syscall()` (`syscall_thread.go`) sender den indre Sentry stub-systemkall utelukkende via delt minne og `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Varsle systemkalltråden om en ny systemkallforespørsel.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Vent på svar.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Fordi `SECCOMP_RET_TRAP`-filtre på gjestetråder kan stables til vilkårlig dybde, og `syscallThread` faller tilbake til `_RUN_SYSCALL_LOOP` uten å be om en andre brukervarsellytter, kjører `gvx` problemfritt inne i `runsc` og støtter rekursiv nøsting (`gvx` i `gvx`).

### 2. Innebygd `overlayfs` på enkeltfil-`ext4lite`-volumer

For å unngå å montere `overlayfs` oppå den ytre gVisor-containerens `gofer` (`9p`/`lisafs`)-rotfilsystem, understøtter `-v gvx-docker:/var/lib/docker` `/var/lib/docker` med `gvx`s interne **`ext4lite`**-filsystem lagret i en enkelt vanlig fil (`gvx-docker.ext4`).

For ytre `runsc` er `gvx-docker.ext4` bare én vanlig fil som aksesseres via `pread64(2)` og `pwrite64(2)`. Inne i `gvx` gir `ext4lite` et JBD2-journalført `ext4`-filsystem med `trusted.overlay.*`- og `security.capability`-utvidede attributter, tegnenhetsbaserte whiteout-noder (`0:0`), `d_type`-katalogoppføringer og `RENAME_EXCHANGE`, noe som lar den indre `dockerd` bruke sin innebygde `overlayfs`-lagringsdriver.

### 3. Selvstendig `netstack` med `docker0` og `iptables-legacy`

Inne i `gvx` samhandler ikke `dockerd` med den ytre `runsc`-containerens nettverksgrensesnitt eller netfilter-tabeller. I stedet:

- Oppretter `dockerd` sin `docker0`-bridge og `veth`-par inne i `gvx`s egen brukerrom-`netstack`.
- Alle `iptables-legacy`-regler (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) kjøres mot `gvx`s interne netfilter-implementasjon.
- Nøstede `runc`-containere monterer sine `/proc`-, `/sys`- og `/sys/fs/cgroup`-hierarkier inne i `gvx`s minnebaserte `VFS2`-monteringstabell, og kaller aldri det ytre `runsc`-`mount(2)`-systemkallet.

---

## Verifisering av nøstet `docker:dind` inne i `gVisor` (eller `gvx` i `gvx`)

Du kan verifisere begge scenarioene (`rungv` inne i `runsc`, eller `rungv` inne i `rungv`) med følgende kommandoer:

```sh
# Inne i en ytre gVisor (runsc)-container eller en ytre 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

# Verifiser at den indre Docker-daemonen bruker innebygd overlayfs (ikke vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Utdata: Storage Driver: overlayfs

# Kjør et flerlags-bygg og utfør en nøstet 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
```

---

## Videre lesing i denne serien

1. **[gvx: Kjøring av Linux-containere, Docker og Kubernetes i én uprivilegert prosess](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Oversikt over `gvx` på Windows (`whpx`, `systrap`), macOS (`hvf`) og Linux (`systrap`).
2. **[Kjøring av Docker, docker build og docker compose uten root eller brukernavnrom](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Hvordan `gvx` kjører `docker:dind` når `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` og `/dev/fuse` er blokkert.
3. **[Docker-in-Docker inne i gVisor og nøstede sandkasser med gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (dette innlegget)
4. **[Raske isolerte Docker-miljøer for RL-utrulling av AI-agenter med gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Hvordan bruke `gvx` og enkeltfil `ext4lite` copy-on-write-øyeblikksbilder for å kjøre hundrevis av parallelle `SWE-bench`- og RL-utrullingsmiljøer med 100-millisekunders tilbakestillinger.
