---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Spouštění Docker-in-Docker uvnitř gVisor a Nested Sandboxes pomocí gvx
description: Proč instalace docker.io nebo vnoření runsc uvnitř gVisor container selhává u modprobe, iptables-nft, overlay2, cgroups a seccomp EBUSY a jak gvx bezpečně spouští vnořený docker:dind uvnitř izolovaného user-space kernel bez --privileged.
language: cs
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
---

## Rychlé řešení: Bezpečné spuštění `docker:dind` uvnitř nepriviligovaného sandboxu `gVisor` (`runsc`)

Platformy cloudových AI agentů, nasazení Kubernetes `agent-sandbox` (`RuntimeClass: gvisor`) a evaluační prostředí pro posilované učení izolují nedůvěryhodné úlohy uvnitř kontejnerů **gVisor (`runsc`)**, konkrétně za účelem ochrany jádra hostitele. Pokud se úloha uvnitř výchozího nepriviligovaného kontejneru `runsc` pokusí nainstalovat `docker.io` nebo `docker-ce` pomocí `apt-get` a spustit `dockerd`, selže, a to i tehdy, když má uvnitř kontejneru oprávnění `root` (`UID 0`).

Pomocí **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) můžete **bezpečně** spouštět `docker:dind`, `docker build` a `docker compose` přímo uvnitř nepriviligovaného kontejneru `gVisor` (`runsc`) (nebo vnořit `gvx` do `gvx`) **bez oslabení vnějšího `RuntimeClass`, zpřístupnění hostitelských socketů nebo udělení `--privileged`**.

> **Zachování a posílení izolace vnějšího sandboxu**: Spuštění `rungv` uvnitř `runsc` neobchází jediné omezení vynucené vnějším sandboxem `gVisor`. Místo toho `gvx` vytváří druhé, vnořené uživatelské jádro `Sentry` s vlastním `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), vlastním paměťovým `netstack` a svazkem `ext4lite` tvořeným jediným souborem. Vnitřní démon `dockerd` a kontejnery `runc` jsou dvojitě izolované: jejich systémová volání, připojení a surové pakety nikdy neopustí vnitřní uživatelské jádro `gvx`.

```sh
# Uvnitř existujícího kontejneru gVisor (runsc) nebo Kubernetes Pod:
# Nainstalujte rungv z archivu vydání na GitHubu ověřeného pomocí SLSA nebo pomocí 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 &

# Spouštějte standardní sestavení a kontejnery Docker přes vnitřní socket gvx
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}}'
# Výstup: overlayfs Alpine Linux ... (v kontejneru)

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

---

## Pět důvodů, proč přímá instalace Dockeru uvnitř `gVisor` (`runsc`) selhává

Když uživatel nebo AI agent pro programování spustí `apt-get install -y docker.io` (nebo `docker-ce`) uvnitř standardního kontejneru `runsc` a pokusí se spustit `dockerd`, narazí na pět po sobě jdoucích technických překážek.

### 1. Načítání modulů jádra (`modprobe`) proti virtuálnímu jádru `4.4.0`

Během instalace balíčku a spouštění `dockerd` inicializační skripty spouštějí `modprobe`, aby načetly `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` a `xt_conntrack`. Vnější Sentry gVisor blokuje `init_module(2)` a `finit_module(2)` a hlásí virtuální verzi jádra (`4.4.0`), pro kterou neexistuje adresář `/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. Selhání `iptables-nft` (netlink `nf_tables`) a mostu `docker0`

Na Ubuntu 22.04, Ubuntu 24.04 a Debianu 11/12 instalace `apt-get install docker.io` vybere `iptables-nft` (založený na netlink subsystému `nf_tables`) jako výchozí alternativu `/usr/sbin/iptables`. Ve standardním kontejneru gVisor bez síťových administrátorských oprávnění nebo podpory netlinku `nf_tables` `dockerd` při inicializaci řetězce NAT `DOCKER` a mostu `docker0` havaruje:

```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. Selhání `overlay2` na rootfs `gofer` / `9p` a vyčerpání disku při použití `vfs`

Pokud vnější administrátor `runsc` explicitně nenakonfiguroval vyhrazený připojený `tmpfs` nebo `filestore` na `/var/lib/docker`, je rootfs kontejneru podložen `gofer` gVisoru (`9p`/`lisafs`) nebo vnořeným připojením overlay. Připojení `overlay2` nad `/var/lib/docker` selže:

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

Když se uživatelé nebo agenti pokusí tuto chybu obejít spuštěním `dockerd --storage-driver=vfs`, Docker zcela zakáže sdílení vrstev Copy-on-Write. Každá instrukce `RUN` v `Dockerfile` pak provede úplnou rekurzivní kopii (`dirCopy`) celého kořenového souborového systému, takže obraz o velikosti 500 MB napříč 10 kroky sestavení vyprodukuje 5 GB zápisů a sandbox skončí chybou `no space left on device` nebo časovým limitem I/O.

### 4. Odmítnutí připojení cgroup a `/proc` ve vnořeném `runc`

I když je `dockerd` vynuceně spuštěn s `dockerd --iptables=false --bridge=none --storage-driver=vfs`, příkaz `docker run` nebo `docker build` volá `runc`, aby vytvořil vnitřní kontejner. Ve výchozím nepriviligovaném kontejneru gVisor `runc` selže při připojování cgroup, opětovném připojování `/proc` nebo načítání vnitřních filtrů 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. Vnořený `runsc` selhává s `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Pokud se uživatel pokusí spustit další instanci `runsc` uvnitř `runsc` s platformou `systrap`, inicializace se okamžitě přeruší:

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

Jak jádro Linuxu, tak Sentry gVisoru povolují **nejvýše jeden** listener `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) na hierarchii úloh. Protože vnější sandbox již tento slot listeneru obsazuje, vnitřní volání `seccomp(2)` vrací `EBUSY` (`device or resource busy`).

---

## Jak `gvx` řeší vnořené spouštění uvnitř `gVisor`

`gvx` řeší všech pět překážek uvnitř vlastního uživatelského jádra a platformní vrstvy, což umožňuje spouštět `rungv` uvnitř vnějšího kontejneru `gVisor` (`runsc`), nebo dokonce rekurzivně uvnitř dalšího kontejneru `gvx` (`gvx` v `gvx`).

![Vnořené spuštění gvx uvnitř vnějšího gVisoru](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Automatický přechod z `systrap` z `EBUSY` na smyčku futex `_RUN_SYSCALL_LOOP`

V Linux `systrap` každý podproces stubu spouští dva typy vláken:

- **Vlákna provádění hosta (`sysmsg`)**: Zachycují systémová volání hosta pomocí `SECCOMP_RET_TRAP` (`SIGSYS`) a binárně upravených trampolin `jmp rel32` (`syshandler`), přičemž si s Sentry vyměňují stav registrů přes sdílenou paměť a `SYS_FUTEX`.
- **Stub `syscallThread` (`syscall_thread.go`)**: Provádí systémová volání pro adresní prostor (`mmap`, `munmap`, `clone`) uvnitř podprocesu stubu jménem Sentry.

V upstream gVisoru podporuje `syscallThread` dva režimy probuzení v souborech `stub_amd64.s` a `stub_arm64.s`:

1. **Smyčka User-Notify seccomp (`_RUN_SECCOMP_LOOP = 6`, když `t.seccompNotify != nil`)**: Během `syscallThread.init()` volá `t.installSeccompNotify()` instalaci listeneru `SECCOMP_RET_USER_NOTIF` (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`), aby Sentry mohl odesílat požadavky přes `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` a `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Smyčka systémových volání přes futex (`_RUN_SYSCALL_LOOP = 5`, když `t.seccompNotify == nil`)**: `syscallThread` spouští `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`) a uspává se na `SYS_FUTEX` (`FUTEX_WAIT`) nad sdílenou oblastí o dvou stránkách, tvořenou `syscallSentryMessage` (`PROT_READ` ve stubu) a `syscallStubMessage` (`PROT_READ | PROT_WRITE` ve stubu).

Upstream gVisor bezpodmínečně volá `t.installSeccompNotify()` vždy, když je `seccompNotifyIsSupported` pravdivé, a přeruší vytvoření stubu, pokud `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` vrátí `EBUSY`.

V `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` a `syscall_thread.go`) volá `syscallThread.init()` funkci `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
}
```

Pokud `installSeccompNotify()` vrátí `unix.EBUSY`, protože vnější sandbox `gVisor` (`runsc`) nebo vnější `gvx` již obsazuje jediný slot listeneru v řetězci filtrů procesu:

- `installSeccompNotifyIfFree()` nastaví `seccompNotifyBusy` (`atomic.Bool`), pro proces zaznamená jediné varování a pro tento stub i pro všechny následující stuby ponechá `t.seccompNotify == nil`.
- V `syscallThread.detach()` (`syscall_thread_amd64.go` a `syscall_thread_arm64.go`) nastaví `t.seccompNotify == nil` hodnotu `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) na `amd64` (nebo `regs.Regs[9] = _RUN_SYSCALL_LOOP` na `arm64`) namísto `_RUN_SECCOMP_LOOP` (`6`).
- V `syscallThread.syscall()` (`syscall_thread.go`) vnitřní Sentry odesílá systémová volání stubu výhradně přes sdílenou paměť a `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Oznámí vláknu syscall o novém požadavku na systémové volání.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Čeká na odpověď.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Protože filtry `SECCOMP_RET_TRAP` na vláknech hosta fungují v libovolně hluboké zásobníkové struktuře a `syscallThread` přechází na `_RUN_SYSCALL_LOOP`, aniž by požadoval druhý listener pro uživatelské oznámení, `gvx` běží bez problémů uvnitř `runsc` a podporuje rekurzivní vnoření (`gvx` uvnitř `gvx`).

### 2. Nativní `overlayfs` na svazcích `ext4lite` tvořených jediným souborem

Aby se zabránilo připojení `overlayfs` nad kořenový souborový systém `gofer` (`9p`/`lisafs`) vnějšího kontejneru gVisor, `-v gvx-docker:/var/lib/docker` podkládá `/var/lib/docker` interním souborovým systémem **`ext4lite`** od `gvx`, uloženým v jediném běžném souboru (`gvx-docker.ext4`).

Pro vnější `runsc` je `gvx-docker.ext4` pouze jeden běžný soubor, ke kterému se přistupuje přes `pread64(2)` a `pwrite64(2)`. Uvnitř `gvx` poskytuje `ext4lite` souborový systém `ext4` s žurnálem JBD2, rozšířené atributy `trusted.overlay.*` a `security.capability`, whiteout uzly znakových zařízení (`0:0`), položky adresářů `d_type` a `RENAME_EXCHANGE`. To umožňuje vnitřnímu `dockerd` používat jeho nativní ovladač úložiště `overlayfs`.

### 3. Samostatný `netstack` s `docker0` a `iptables-legacy`

Uvnitř `gvx` neinteraguje `dockerd` se síťovými rozhraními ani tabulkami netfilter vnějšího kontejneru `runsc`. Namísto toho:

- `dockerd` vytváří svůj most `docker0` a dvojice `veth` uvnitř vlastního uživatelského `netstack` v rámci `gvx`.
- Všechna pravidla `iptables-legacy` (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) se provádějí proti interní implementaci netfilteru v `gvx`.
- Vnořené kontejnery `runc` připojují své hierarchie `/proc`, `/sys` a `/sys/fs/cgroup` uvnitř paměťové tabulky připojení `VFS2` v `gvx`, aniž by kdy volaly systémové volání `mount(2)` vnějšího `runsc`.

---

## Ověření vnořeného `docker:dind` uvnitř `gVisor` (nebo `gvx` v `gvx`)

Oba scénáře (`rungv` uvnitř `runsc` nebo `rungv` uvnitř `rungv`) můžete ověřit následujícími příkazy:

```sh
# Uvnitř vnějšího kontejneru gVisor (runsc) nebo vnějšího kontejneru 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

# Ověřte, že vnitřní démon Docker používá nativní overlayfs (nikoli vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Výstup: Storage Driver: overlayfs

# Spusťte vícevrstvé sestavení a vnořený kontejner
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
# Výstup: nested-build-ok

kill -INT $DIND_PID
```

---

## Další čtení v této sérii

1. **[gvx: Spouštění Linuxových kontejnerů, Dockeru a Kubernetes v jediném nepriviligovaném procesu](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Přehled `gvx` napříč systémy Windows (`whpx`, `systrap`), macOS (`hvf`) a Linux (`systrap`).
2. **[Spouštění Dockeru, docker build a docker compose bez root oprávnění nebo uživatelských jmenných prostorů](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Jak `gvx` spouští `docker:dind`, když jsou blokovány `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` a `/dev/fuse`.
3. **[Spouštění Docker-in-Docker uvnitř gVisor a vnořených sandboxů pomocí gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (tento příspěvek)
4. **[Rychlá izolovaná prostředí Dockeru pro rollouty AI agentů při posilovaném učení pomocí gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Jak použít `gvx` a copy-on-write snapshoty `ext4lite` tvořené jediným souborem ke spuštění stovek paralelních prostředí `SWE-bench` a RL rollout s resetováním za 100 milisekund.
