---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Docker-in-Docker-ympäristön ajaminen gVisorin ja sisäkkäisten Sandboxien sisällä gvx:n avulla
description: Miksi docker.io-paketin asentaminen tai runsc:n sisäkkäinen ajaminen gVisor-kontissa epäonnistuu modprobe-, iptables-nft-, overlay2-, cgroups- ja seccomp-komponenttien kohdalla (EBUSY), ja miten gvx ajaa sisäkkäisen docker:dind-ympäristön turvallisesti eristetyssä käyttäjätilan ytimessä ilman --privileged-asetusta.
language: fi
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
---

## Pikaratkaisu: `docker:dind`-kontin turvallinen suoritus etuoikeudettomassa `gVisor` (`runsc`) -Sandboxissa

Pilvipohjaiset tekoälyagenttialustat, Kubernetes-`agent-sandbox`-käyttöönotot (`RuntimeClass: gvisor`) ja vahvistusoppimisen arviointiympäristöt eristävät luottamattomat työkuormat **gVisor (`runsc`)** -kontteihin nimenomaan isäntäytimen suojaamiseksi. Kun oletusarvoisesti etuoikeudettomassa `runsc`-kontissa toimiva työkuorma yrittää asentaa paketin `docker.io` tai `docker-ce` komennolla `apt-get` ja käynnistää `dockerd`:n, se epäonnistuu, vaikka sillä olisi kontin sisällä `root` (`UID 0`).

**[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`):n avulla voit **turvallisesti** suorittaa `docker:dind`-, `docker build`- ja `docker compose` -komennot suoraan etuoikeudettomassa `gVisor` (`runsc`) -kontissa (tai sijoittaa `gvx`:n `gvx`:n sisälle) **heikentämättä ulomman `RuntimeClass`:n turvallisuutta, paljastamatta isännän socketeja tai myöntämättä `--privileged`-oikeutta**.

> **Ulomman Sandbox-eristyksen säilyttäminen ja vahvistaminen**: `rungv`:n suorittaminen `runsc`:n sisällä ei ohita yhtäkään ulomman `gVisor`-sandboxin asettamaa rajoitusta. Sen sijaan `gvx` luo toisen, sisäkkäisen käyttäjätilan gVisor `Sentry` -ytimen, jolla on oma `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), oma muistinvarainen `netstack` ja yksitiedostoinen `ext4lite`-volyymi. Sisäiset `dockerd`- ja `runc`-kontit ovat kaksinkertaisesti sandboxattuja: niiden järjestelmäkutsut, liitokset ja raakapaketit eivät koskaan poistu sisäisen `gvx`:n käyttäjätilaytimen sisältä.

```sh
# Olemassa olevan gVisor (runsc) -kontin tai Kubernetes Podin sisällä:
# Asenna rungv SLSA-todennetusta GitHub-julkaisuarkistosta tai go install -komennolla:
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 &

# Suorita tavalliset Docker-buildit ja -kontit sisäistä gvx-socketia vasten
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}}'
# Tulos: overlayfs Alpine Linux ... (containerized)

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

---

## Viisi syytä, miksi suora Docker-asennus epäonnistuu `gVisor`:n (`runsc`) sisällä

Kun käyttäjä tai tekoälyn koodausagentti suorittaa komennon `apt-get install -y docker.io` (tai `docker-ce`) tavallisen `runsc`-kontin sisällä ja yrittää käynnistää `dockerd`:n, se kohtaa viisi peräkkäistä teknistä estettä.

### 1. Ytimen moduulien lataaminen (`modprobe`) virtuaalista ydintä `4.4.0` vastaan

Paketin asennuksen ja `dockerd`:n käynnistyksen aikana alustuskomentosarjat suorittavat `modprobe`-komennon ladatakseen moduulit `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` ja `xt_conntrack`. Ulompi gVisor Sentry estää kutsut `init_module(2)` ja `finit_module(2)` ja raportoi virtuaalisen ytimen version (`4.4.0`), jolle ei ole hakemistoa `/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. `iptables-nft`:n (`nf_tables` Netlink) ja `docker0`-sillan virheet

Ubuntu 22.04:ssä, Ubuntu 24.04:ssä ja Debian 11/12:ssa `apt-get install docker.io` valitsee oletusvaihtoehdoksi polulle `/usr/sbin/iptables` vaihtoehdon `iptables-nft` (jota tukee `nf_tables`-netlink-alijärjestelmä). Tavallisessa gVisor-kontissa ilman verkon ylläpitäjän oikeuksia tai `nf_tables`-netlink-tukea `dockerd` kaatuu alustaessaan `DOCKER` NAT -ketjua ja `docker0`-siltaa:

```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`-virhe `gofer`- / `9p`-juurijärjestelmässä ja `vfs`-levytilan loppuminen

Ellei ulomman `runsc`:n ylläpitäjä ole nimenomaisesti esikonfiguroinut omistettua `tmpfs`- tai filestore-liitosta kohteeseen `/var/lib/docker`, kontin juurijärjestelmä perustuu gVisorin `gofer`- (`9p`/`lisafs`) tai sisäkkäiseen overlay-liitokseen. `overlay2`:n liittäminen kohteen `/var/lib/docker` päälle epäonnistuu:

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

Kun käyttäjät tai agentit yrittävät ohittaa tämän virheen käynnistämällä `dockerd --storage-driver=vfs`:n, Docker poistaa kokonaan Copy-on-Write-kerrosten jakamisen käytöstä. Jokainen `Dockerfile`:n `RUN`-ohje suorittaa koko juurijärjestelmän täyden rekursiivisen kopioinnin (`dirCopy`), mikä kertoo 500 MB:n kuvan 10 buildivaiheessa 5 GB:n kirjoitukseksi ja päättää sandboxin virheeseen `no space left on device` tai I/O-aikakatkaisuun.

### 4. Sisäkkäisten `runc`-cgroup- ja `/proc`-liitosten hylkäämiset

Vaikka `dockerd` pakotettaisiin käynnistymään komennolla `dockerd --iptables=false --bridge=none --storage-driver=vfs`, komennot `docker run` tai `docker build` kutsuvat `runc`:n luomaan sisäisen kontin. Oletusarvoisessa etuoikeudettomassa gVisor-kontissa `runc` epäonnistuu liittäessään cgroupeja, liittäessään `/proc`:n uudelleen tai ladatessaan sisäisiä seccomp-suodattimia:

```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. Sisäkkäinen `runsc` epäonnistuu virheeseen `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Jos käyttäjä yrittää suorittaa toisen `runsc`-instanssin `runsc`:n sisällä `systrap`-alustaa käyttäen, alustus keskeytyy välittömästi:

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

Sekä Linux-ydin että gVisor Sentry sallivat tehtävähierarkiaa kohden **enintään yhden** `SECCOMP_FILTER_FLAG_NEW_LISTENER`- (`SECCOMP_RET_USER_NOTIF`) -kuuntelijan. Koska ulompi sandbox on jo varannut kuuntelijapaikan, sisäinen `seccomp(2)`-kutsu palauttaa arvon `EBUSY` (`device or resource busy`).

---

## Miten `gvx` ratkaisee sisäkkäisen suorituksen `gVisor`:n sisällä

`gvx` ratkaisee kaikki viisi estettä omassa käyttäjätilaytimessään ja alustakerroksessaan, mikä mahdollistaa `rungv`:n suorittamisen ulomman `gVisor` (`runsc`) -kontin sisällä tai jopa rekursiivisesti toisen `gvx`-kontin sisällä (`gvx` `gvx`:ssä).

![Sisäkkäinen gvx-suoritus ulomman gVisorin sisällä](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Automaattinen `systrap`-varajärjestelmä `EBUSY`-tilanteesta `_RUN_SYSCALL_LOOP`-futex-silmukkaan

Linuxin `systrap`-alustassa jokainen stub-aliprosessi suorittaa kahdentyyppisiä säikeitä:

- **Vierasjärjestelmän suoritussäikeet (`sysmsg`)**: Kaappaavat vierasjärjestelmäkutsut `SECCOMP_RET_TRAP`-mekanismilla (`SIGSYS`) ja binääripatchatuilla `jmp rel32` -trampoliineilla (`syshandler`), vaihtaen rekisteritilan Sentryn kanssa jaetun muistin ja `SYS_FUTEX`:n kautta.
- **Stub-`syscallThread` (`syscall_thread.go`)**: Suorittaa osoiteavaruuden järjestelmäkutsut (`mmap`, `munmap`, `clone`) stub-aliprosessin sisällä Sentryn puolesta.

Upstream-gVisorissa `syscallThread` tukee kahta herätystilaa tiedostoissa `stub_amd64.s` ja `stub_arm64.s`:

1. **Seccomp User-Notify -silmukka (`_RUN_SECCOMP_LOOP = 6`, kun `t.seccompNotify != nil`)**: `syscallThread.init()`-vaiheen aikana `t.installSeccompNotify()` asentaa `SECCOMP_RET_USER_NOTIF`-kuuntelijan (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`), jotta Sentry voi lähettää pyynnöt `ioctl(SECCOMP_IOCTL_NOTIF_SEND)`- ja `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`-kutsujen kautta.
2. **Futex-järjestelmäkutsusilmukka (`_RUN_SYSCALL_LOOP = 5`, kun `t.seccompNotify == nil`)**: `syscallThread` suorittaa `syscall_loop`-silmukan (`stub_amd64.s` / `stub_arm64.s`) ja nukkuu `SYS_FUTEX`:n (`FUTEX_WAIT`) varassa jaetulla 2-sivun alueella `syscallSentryMessage` (stubissa `PROT_READ`) ja `syscallStubMessage` (stubissa `PROT_READ | PROT_WRITE`).

Upstream gVisor kutsuu ehdoitta `t.installSeccompNotify()`-funktiota aina, kun `seccompNotifyIsSupported` on tosi, ja keskeyttää stubin luonnin, jos `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` palauttaa arvon `EBUSY`.

`gvx`:ssä (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` ja `syscall_thread.go`) `syscallThread.init()` kutsuu funktiota `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
}
```

Kun `installSeccompNotify()` palauttaa arvon `unix.EBUSY`, koska ulompi `gVisor` (`runsc`) tai ulompi `gvx`-sandbox on jo varannut prosessin suodatinketjun ainoan kuuntelijapaikan:

- `installSeccompNotifyIfFree()` asettaa muuttujan `seccompNotifyBusy` (`atomic.Bool`), kirjaa prosessille yhden varoituksen ja jättää `t.seccompNotify == nil`-arvoksi kyseiselle stubille ja kaikille sitä seuraaville stubeille.
- `syscallThread.detach()`-funktiossa (`syscall_thread_amd64.go` ja `syscall_thread_arm64.go`) `t.seccompNotify == nil` asettaa `amd64`-arkkitehtuurilla arvon `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) (tai `arm64`-arkkitehtuurilla `regs.Regs[9] = _RUN_SYSCALL_LOOP`) arvon `_RUN_SECCOMP_LOOP` (`6`) sijaan.
- `syscallThread.syscall()`-funktiossa (`syscall_thread.go`) sisäinen Sentry välittää stub-järjestelmäkutsut yksinomaan jaetun muistin ja `SYS_FUTEX`:n kautta:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Ilmoita syscall-säikeelle uudesta järjestelmäkutsupyynnöstä.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Odota vastausta.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Koska `SECCOMP_RET_TRAP`-suodattimet vierasäikeillä voivat pinota mielivaltaisen syviksi ja `syscallThread` palaa `_RUN_SYSCALL_LOOP`-silmukkaan pyytämättä toista käyttäjäilmoituskuuntelijaa, `gvx` toimii moitteettomasti `runsc`:n sisällä ja tukee rekursiivista sisäkkäisyyttä (`gvx` `gvx`:n sisällä).

### 2. Natiivi `overlayfs` yksitiedostoisilla `ext4lite`-volyymeilla

Jotta `overlayfs`:ää ei liitettäisi ulomman gVisor-kontin `gofer`- (`9p`/`lisafs`) juurijärjestelmän päälle, `-v gvx-docker:/var/lib/docker` tukee kohdetta `/var/lib/docker` `gvx`:n sisäisellä **`ext4lite`**-tiedostojärjestelmällä, joka on tallennettu yhteen tavalliseen tiedostoon (`gvx-docker.ext4`).

Ulomman `runsc`:n näkökulmasta `gvx-docker.ext4` on vain yksi tavallinen tiedosto, johon päästään käsiksi `pread64(2)`- ja `pwrite64(2)`-kutsuilla. `gvx`:n sisällä `ext4lite` tarjoaa JBD2-lokitetun `ext4`-tiedostojärjestelmän, jossa on `trusted.overlay.*`- ja `security.capability`-laajennetut attribuutit, merkkilaitteen whiteout-solmut (`0:0`), `d_type`-hakemistomerkinnät ja `RENAME_EXCHANGE`, mikä mahdollistaa sisäisen `dockerd`:n käyttää natiivia `overlayfs`-tallennusajuriaan.

### 3. Itsenäinen `netstack`, jossa `docker0` ja `iptables-legacy`

`gvx`:n sisällä `dockerd` ei ole vuorovaikutuksessa ulomman `runsc`-kontin verkkoliittymien tai netfilter-taulukoiden kanssa. Sen sijaan:

- `dockerd` luo `docker0`-siltansa ja `veth`-parinsa `gvx`:n oman käyttäjätilan `netstack`-pinon sisällä.
- Kaikki `iptables-legacy`-säännöt (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) suoritetaan `gvx`:n sisäistä netfilter-toteutusta vastaan.
- Sisäkkäiset `runc`-kontit liittävät `/proc`-, `/sys`- ja `/sys/fs/cgroup`-hierarkiansa `gvx`:n muistissa olevaan `VFS2`-liitostauluun, eivätkä koskaan kutsu ulomman `runsc`:n `mount(2)`-järjestelmäkutsua.

---

## Sisäkkäisen `docker:dind`-kontin todentaminen `gVisor`:n sisällä (tai `gvx` `gvx`:ssä)

Voit todentaa molemmat skenaariot (`rungv` `runsc`:n sisällä tai `rungv` `rungv`:n sisällä) seuraavilla komennoilla:

```sh
# Ulomman gVisor (runsc) -kontin tai ulomman rungv-kontin sisällä:
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

# Varmista, että sisäinen Docker-daemon käyttää natiivia overlayfs-tiedostojärjestelmää (ei vfs:ää)
docker info --format 'Storage Driver: {{.Driver}}'
# Tulos: Storage Driver: overlayfs

# Suorita monikerroksinen build ja suorita sisäkkäinen kontti
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
# Tulos: nested-build-ok

kill -INT $DIND_PID
```

---

## Lisälukemista tästä sarjasta

1. **[gvx: Linux-konttien, Dockerin ja Kubernetesin ajaminen yhdessä etuoikeudettomassa prosessissa](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Yleiskatsaus `gvx`:stä Windowsissa (`whpx`, `systrap`), macOS:ssä (`hvf`) ja Linuxissa (`systrap`).
2. **[Docker-, docker build- ja docker compose -komentojen suorittaminen ilman juurioikeuksia tai käyttäjänimiavaruuksia](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Miten `gvx` suorittaa `docker:dind`-kontin, kun `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` ja `/dev/fuse` on estetty.
3. **[Docker-in-Docker-kontin suorittaminen gVisorin ja sisäkkäisten sandboxien sisällä gvx:llä](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (tämä kirjoitus)
4. **[Nopeat eristetyt Docker-ympäristöt tekoälyagenttien RL-rollouteihin gvx:llä](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Miten käyttää `gvx`:ää ja yksitiedostoisia `ext4lite` copy-on-write -tilannekuvia satojen rinnakkaisten `SWE-bench`- ja RL-rollout-ympäristöjen ajamiseen 100 millisekunnin nollauksilla.
