---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Uruchamianie Docker-in-Docker wewnątrz gVisor i zagnieżdżonych piaskownic za pomocą gvx
description: Dlaczego instalacja docker.io lub zagnieżdżanie runsc wewnątrz kontenera gVisor kończy się błędem w przypadku modprobe, iptables-nft, overlay2, cgroups oraz seccomp EBUSY, oraz w jaki sposób gvx bezpiecznie uruchamia zagnieżdżony docker:dind wewnątrz odizolowanego jądra działającego w przestrzeni użytkownika, bez użycia --privileged.
language: pl
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
---

## Szybkie rozwiązanie: bezpieczne uruchamianie `docker:dind` wewnątrz nieuprzywilejowanej piaskownicy `gVisor` (`runsc`)

Chmurowe platformy agentów AI, wdrożenia `agent-sandbox` w Kubernetes (`RuntimeClass: gvisor`) oraz środowiska ewaluacyjne reinforcement learning izolują niezaufane obciążenia w kontenerach **gVisor (`runsc`)**, aby chronić jądro hosta. Gdy obciążenie wewnątrz domyślnego, nieuprzywilejowanego kontenera `runsc` próbuje zainstalować `docker.io` lub `docker-ce` za pomocą `apt-get` i uruchomić `dockerd`, operacja kończy się niepowodzeniem, nawet jeśli proces działa jako `root` (`UID 0`) wewnątrz kontenera.

Za pomocą **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) można **bezpiecznie** uruchomić `docker:dind`, `docker build` i `docker compose` bezpośrednio wewnątrz nieuprzywilejowanego kontenera `gVisor` (`runsc`) (lub zagnieździć `gvx` w `gvx`), **bez osłabiania zewnętrznego `RuntimeClass`, udostępniania gniazd hosta ani nadawania `--privileged`**.

> **Zachowanie i wzmocnienie izolacji zewnętrznej piaskownicy**: Uruchomienie `rungv` wewnątrz `runsc` nie omija ani jednego ograniczenia egzekwowanego przez zewnętrzną piaskownicę `gVisor`. Zamiast tego `gvx` tworzy drugie, zagnieżdżone jądro `Sentry` gVisor działające w przestrzeni użytkownika, z własnym `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), własnym działającym w pamięci `netstack` oraz pojedynczym wolumenem `ext4lite` w postaci pliku. Wewnętrzne kontenery `dockerd` i `runc` są podwójnie odizolowane: ich wywołania systemowe, montowania i surowe pakiety nigdy nie opuszczają wewnętrznego jądra `gvx` działającego w przestrzeni użytkownika.

```sh
# Wewnątrz istniejącego kontenera gVisor (runsc) lub Poda Kubernetes:
# Zainstaluj rungv z archiwum wydania GitHub z atestacją SLSA lub za 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 &

# Uruchamiaj standardowe buildy i kontenery Docker za pomocą wewnętrznego gniazda 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}}'
# Wynik: overlayfs Alpine Linux ... (containerized)

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

---

## Pięć powodów, dla których bezpośrednia instalacja Dockera kończy się niepowodzeniem wewnątrz `gVisor` (`runsc`)

Gdy użytkownik lub agent programistyczny AI uruchamia `apt-get install -y docker.io` (lub `docker-ce`) w standardowym kontenerze `runsc` i próbuje uruchomić `dockerd`, napotyka pięć kolejnych barier technicznych.

### 1. Ładowanie modułów jądra (`modprobe`) względem wirtualnego jądra `4.4.0`

Podczas instalacji pakietu i uruchamiania `dockerd` skrypty inicjalizacyjne wywołują `modprobe`, aby załadować `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` i `xt_conntrack`. Zewnętrzny Sentry gVisor blokuje `init_module(2)` oraz `finit_module(2)` i raportuje wirtualną wersję jądra (`4.4.0`), dla której nie istnieje katalog `/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. Błędy `iptables-nft` (netlink `nf_tables`) oraz mostu `docker0`

Na Ubuntu 22.04, Ubuntu 24.04 oraz Debianie 11/12 polecenie `apt-get install docker.io` wybiera `iptables-nft` (oparte na podsystemie netlink `nf_tables`) jako domyślną alternatywę `/usr/sbin/iptables`. W standardowym kontenerze gVisor, bez uprawnień administracyjnych sieci ani obsługi netlink `nf_tables`, `dockerd` ulega awarii podczas inicjalizacji łańcucha NAT `DOCKER` oraz mostu `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. Awaria `overlay2` na rootfs `gofer` / `9p` oraz wyczerpanie miejsca przy `vfs`

Chyba że zewnętrzny administrator `runsc` jawnie wstępnie skonfigurował dedykowany montaż `tmpfs` lub filestore w `/var/lib/docker`, rootfs kontenera jest oparty na `gofer` (`9p`/`lisafs`) gVisor lub zagnieżdżonym montażu overlay. Montowanie `overlay2` na `/var/lib/docker` kończy się niepowodzeniem:

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

Gdy użytkownicy lub agenci próbują ominąć ten błąd, uruchamiając `dockerd --storage-driver=vfs`, Docker całkowicie wyłącza współdzielenie warstw Copy-on-Write. Każda instrukcja `RUN` w `Dockerfile` wykonuje pełne, rekurencyjne kopiowanie (`dirCopy`) całego systemu plików root, co mnoży obraz o rozmiarze 500 MB w 10 krokach budowania do 5 GB zapisów i kończy działanie piaskownicy błędem `no space left on device` lub przekroczeniem czasu operacji I/O.

### 4. Odrzucenia montowania cgroup i `/proc` w zagnieżdżonym `runc`

Nawet jeśli `dockerd` zostanie wymuszony do uruchomienia z `dockerd --iptables=false --bridge=none --storage-driver=vfs`, uruchomienie `docker run` lub `docker build` wywołuje `runc` w celu utworzenia wewnętrznego kontenera. W domyślnym, nieuprzywilejowanym kontenerze gVisor `runc` kończy się niepowodzeniem przy montowaniu cgroup, ponownym montowaniu `/proc` lub ładowaniu wewnętrznych filtrów 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. Zagnieżdżony `runsc` kończy się błędem `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Jeśli użytkownik próbuje uruchomić kolejną instancję `runsc` wewnątrz `runsc` przy użyciu platformy `systrap`, inicjalizacja przerywa się natychmiast:

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

Zarówno jądro Linux, jak i Sentry gVisor dopuszczają **co najwyżej jednego** listenera `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) na hierarchię zadań. Ponieważ zewnętrzna piaskownica już zajmuje ten slot listenera, wewnętrzne wywołanie `seccomp(2)` zwraca `EBUSY` (`device or resource busy`).

---

## Jak `gvx` rozwiązuje zagnieżdżone wykonywanie wewnątrz `gVisor`

`gvx` eliminuje wszystkie pięć barier we własnym jądrze działającym w przestrzeni użytkownika oraz w warstwie platformy, umożliwiając `rungv` działanie wewnątrz zewnętrznego kontenera `gVisor` (`runsc`), a nawet rekurencyjnie wewnątrz innego kontenera `gvx` (`gvx` w `gvx`).

![Zagnieżdżone wykonanie gvx wewnątrz zewnętrznego gVisor](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Automatyczny awaryjny przełącznik `systrap` z `EBUSY` na pętlę futex `_RUN_SYSCALL_LOOP`

W Linux `systrap` każdy podproces stub uruchamia dwa rodzaje wątków:

- **Wątki wykonawcze gościa (`sysmsg`)**: przechwytują wywołania systemowe gościa za pomocą `SECCOMP_RET_TRAP` (`SIGSYS`) oraz binarnie łatanych trampolin `jmp rel32` (`syshandler`), wymieniając stan rejestrów z Sentry przez pamięć współdzieloną i `SYS_FUTEX`.
- **Wątek stub `syscallThread` (`syscall_thread.go`)**: wykonuje wywołania systemowe przestrzeni adresowej (`mmap`, `munmap`, `clone`) wewnątrz podprocesu stub w imieniu Sentry.

W upstreamowym gVisor `syscallThread` obsługuje dwa tryby wybudzania w `stub_amd64.s` i `stub_arm64.s`:

1. **Pętla powiadomień użytkownika seccomp (`_RUN_SECCOMP_LOOP = 6`, gdy `t.seccompNotify != nil`)**: podczas `syscallThread.init()` wywołanie `t.installSeccompNotify()` instaluje listener `SECCOMP_RET_USER_NOTIF` (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`), dzięki czemu Sentry może przesyłać żądania przez `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` i `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Pętla wywołań systemowych futex (`_RUN_SYSCALL_LOOP = 5`, gdy `t.seccompNotify == nil`)**: `syscallThread` uruchamia `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`), śpiąc na `SYS_FUTEX` (`FUTEX_WAIT`) nad współdzielonym dwustronicowym obszarem `syscallSentryMessage` (`PROT_READ` w stub) oraz `syscallStubMessage` (`PROT_READ | PROT_WRITE` w stub).

Upstreamowy gVisor bezwarunkowo wywołuje `t.installSeccompNotify()`, gdy `seccompNotifyIsSupported` ma wartość true, i przerywa tworzenie stub, jeśli `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` zwróci `EBUSY`.

W `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` oraz `syscall_thread.go`) `syscallThread.init()` wywołuje `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
}
```

Gdy `installSeccompNotify()` zwraca `unix.EBUSY`, ponieważ zewnętrzna piaskownica `gVisor` (`runsc`) lub zewnętrzne `gvx` już zajmuje jedyny slot listenera w łańcuchu filtrów procesu:

- `installSeccompNotifyIfFree()` ustawia `seccompNotifyBusy` (`atomic.Bool`), zapisuje jedno ostrzeżenie dla procesu i pozostawia `t.seccompNotify == nil` dla tego stub oraz wszystkich kolejnych stubów.
- W `syscallThread.detach()` (`syscall_thread_amd64.go` oraz `syscall_thread_arm64.go`) warunek `t.seccompNotify == nil` ustawia `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) na `amd64` (lub `regs.Regs[9] = _RUN_SYSCALL_LOOP` na `arm64`) zamiast `_RUN_SECCOMP_LOOP` (`6`).
- W `syscallThread.syscall()` (`syscall_thread.go`) wewnętrzny Sentry przesyła wywołania systemowe stub wyłącznie przez pamięć współdzieloną i `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Powiadom wątek wywołań systemowych o nowym żądaniu wywołania systemowego.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Czekaj na odpowiedź.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Ponieważ filtry `SECCOMP_RET_TRAP` na wątkach gościa mogą mieć dowolnie dużą głębokość stosu, a `syscallThread` przechodzi do `_RUN_SYSCALL_LOOP` bez żądania drugiego listenera powiadomień użytkownika, `gvx` działa poprawnie wewnątrz `runsc` i obsługuje rekurencyjne zagnieżdżanie (`gvx` w `gvx`).

### 2. Natywny `overlayfs` na wolumenach `ext4lite` w postaci pojedynczego pliku

Aby uniknąć montowania `overlayfs` na zewnętrznym rootfs kontenera gVisor opartym na `gofer` (`9p`/`lisafs`), `-v gvx-docker:/var/lib/docker` opiera `/var/lib/docker` na wewnętrznym systemie plików **`ext4lite`** `gvx`, przechowywanym w pojedynczym zwykłym pliku (`gvx-docker.ext4`).

Dla zewnętrznego `runsc` plik `gvx-docker.ext4` jest po prostu jednym zwykłym plikiem, do którego uzyskuje się dostęp przez `pread64(2)` i `pwrite64(2)`. Wewnątrz `gvx` `ext4lite` udostępnia system plików `ext4` z dziennikiem JBD2, z rozszerzonymi atrybutami `trusted.overlay.*` i `security.capability`, węzłami whiteout urządzeń znakowych (`0:0`), wpisami katalogów `d_type` oraz `RENAME_EXCHANGE`, co pozwala wewnętrznemu `dockerd` korzystać z natywnego sterownika pamięci masowej `overlayfs`.

### 3. Samodzielny `netstack` z `docker0` i `iptables-legacy`

Wewnątrz `gvx` `dockerd` nie komunikuje się z interfejsami sieciowymi ani tablicami netfilter zewnętrznego kontenera `runsc`. Zamiast tego:

- `dockerd` tworzy most `docker0` oraz pary `veth` wewnątrz własnego, działającego w przestrzeni użytkownika `netstack` w `gvx`.
- Wszystkie reguły `iptables-legacy` (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) są wykonywane względem wewnętrznej implementacji netfilter `gvx`.
- Zagnieżdżone kontenery `runc` montują swoje hierarchie `/proc`, `/sys` i `/sys/fs/cgroup` wewnątrz działającej w pamięci tablicy montowań `VFS2` w `gvx`, nigdy nie wywołując zewnętrznego wywołania systemowego `mount(2)` w `runsc`.

---

## Weryfikacja zagnieżdżonego `docker:dind` wewnątrz `gVisor` (lub `gvx` w `gvx`)

Oba scenariusze (`rungv` wewnątrz `runsc` lub `rungv` wewnątrz `rungv`) można zweryfikować za pomocą poniższych poleceń:

```sh
# Wewnątrz zewnętrznego kontenera gVisor (runsc) lub zewnętrznego kontenera 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

# Sprawdź, czy wewnętrzny demon Docker używa natywnego overlayfs (a nie vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Wynik: Storage Driver: overlayfs

# Uruchom wielowarstwowy build i wykonaj zagnieżdżony kontener
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
# Wynik: nested-build-ok

kill -INT $DIND_PID
```

---

## Dalsza lektura w tej serii

1. **[gvx: Uruchamianie kontenerów Linux, Docker i Kubernetes w jednym nieuprzywilejowanym procesie](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Przegląd `gvx` w systemach Windows (`whpx`, `systrap`), macOS (`hvf`) i Linux (`systrap`).
2. **[Uruchamianie Dockera, docker build i docker compose bez roota i przestrzeni nazw użytkownika](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Jak `gvx` uruchamia `docker:dind`, gdy `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` i `/dev/fuse` są zablokowane.
3. **[Uruchamianie Docker-in-Docker wewnątrz gVisor i zagnieżdżonych piaskownic z gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (ten wpis)
4. **[Szybkie, izolowane środowiska Docker dla rolloutów RL agentów AI z gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Jak używać `gvx` i jednoplikowych migawek copy-on-write `ext4lite` do uruchamiania setek równoległych środowisk `SWE-bench` oraz rolloutów RL z resetami trwającymi 100 milisekund.
