---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Стартиране на Docker-in-Docker вътре в gVisor и Nested Sandboxes с gvx
description: Защо инсталирането на docker.io или вграждането на runsc в контейнер на gVisor се проваля при modprobe, iptables-nft, overlay2, cgroups и seccomp EBUSY, и как gvx сигурно стартира вложен docker:dind в изолирано user-space kernel без --privileged.
language: bg
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
---

## Бързо решение: Сигурно изпълнение на `docker:dind` в непривилегирован `gVisor` (`runsc`) sandbox

Платформите за AI агенти в облака, deployment-ите на Kubernetes `agent-sandbox` (`RuntimeClass: gvisor`) и harness-ите за оценка при обучение с подсилване изолират недоверени работни натоварвания в контейнери **gVisor (`runsc`)**, именно за да защитят ядрото на хоста. Когато работно натоварване в стандартен непривилегирован контейнер `runsc` се опита да инсталира `docker.io` или `docker-ce` чрез `apt-get` и да стартира `dockerd`, операцията се проваля, дори ако процесът има `root` (`UID 0`) в контейнера.

С **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) можете **сигурно** да изпълнявате `docker:dind`, `docker build` и `docker compose` директно в непривилегирован контейнер `gVisor` (`runsc`) (или да вложите `gvx` в `gvx`) **без да отслабвате външния `RuntimeClass`, без да излагате хостови сокети и без да предоставяте `--privileged`**.

> **Запазване и засилване на изолацията на външния sandbox**: Изпълнението на `rungv` в `runsc` не заобикаля нито едно ограничение, наложено от външния sandbox на `gVisor`. Вместо това `gvx` създава втори, вложен потребителски `gVisor` `Sentry` kernel със собствен `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), собствен `netstack` в паметта и единичен `ext4lite` том във файл. Вложените `dockerd` и `runc` контейнери са с двойна изолация: техните системни извиквания, монтирания и сурови пакети никога не напускат вътрешното потребителско пространство на ядрото на `gvx`.

```sh
# Във вече съществуващ контейнер gVisor (runsc) или Kubernetes Pod:
# Инсталирайте rungv от архив на GitHub release с SLSA атестация или чрез 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 &

# Изпълнявайте стандартни Docker build-ове и контейнери срещу вътрешния 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}}'
# Изход: overlayfs Alpine Linux ... (контейнеризиран)

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

---

## Пет причини, поради които директната инсталация на Docker се проваля в `gVisor` (`runsc`)

Когато потребител или AI агент за кодиране изпълни `apt-get install -y docker.io` (или `docker-ce`) в стандартен контейнер `runsc` и се опита да стартира `dockerd`, той се сблъсква с пет последователни технически бариери.

### 1. Зареждане на kernel модули (`modprobe`) спрямо виртуалното ядро `4.4.0`

По време на инсталиране на пакети и стартиране на `dockerd` скриптовете за инициализация изпълняват `modprobe`, за да заредят `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` и `xt_conntrack`. Външният Sentry на gVisor блокира `init_module(2)` и `finit_module(2)` и докладва виртуална версия на ядрото (`4.4.0`), за която няма директория `/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` (`nf_tables` Netlink) и моста `docker0`

На Ubuntu 22.04, Ubuntu 24.04 и Debian 11/12 `apt-get install docker.io` избира `iptables-nft` (базиран на подсистемата `nf_tables` netlink) като алтернатива по подразбиране за `/usr/sbin/iptables`. В стандартен контейнер gVisor без административни мрежови привилегии или поддръжка на `nf_tables` netlink `dockerd` се срива при инициализиране на NAT веригата `DOCKER` и моста `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. Провал на `overlay2` върху `gofer` / `9p` rootfs и изчерпване на диска при `vfs`

Освен ако външният администратор на `runsc` изрично не е предконфигурирал специален `tmpfs` или filestore монтаж в `/var/lib/docker`, rootfs на контейнера се поддържа от `gofer` на gVisor (`9p`/`lisafs`) или от вложен overlay монтаж. Монтирането на `overlay2` върху `/var/lib/docker` се проваля:

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

Когато потребители или агенти се опитат да заобиколят тази грешка чрез стартиране на `dockerd --storage-driver=vfs`, Docker напълно деактивира споделянето на слоеве с Copy-on-Write. Всяка инструкция `RUN` във `Dockerfile` извършва пълно рекурсивно копиране (`dirCopy`) на целия root файлов system, като умножава образ от 500 MB през 10 стъпки на build в 5 GB записи и прекратява sandbox-а с `no space left on device` или I/O timeout.

### 4. Отхвърляне на вложени `runc` cgroup и `/proc` монтирания

Дори ако `dockerd` бъде принуден да стартира с `dockerd --iptables=false --bridge=none --storage-driver=vfs`, изпълнението на `docker run` или `docker build` извиква `runc`, за да създаде вътрешния контейнер. В стандартен непривилегирован контейнер gVisor `runc` се проваля при монтиране на cgroups, при ремонтиране на `/proc` или при зареждане на вътрешни 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. Вложеният `runsc` се проваля с `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Ако потребител се опита да стартира друга инстанция на `runsc` вътре в `runsc`, използвайки платформата `systrap`, инициализацията прекъсва незабавно:

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

Както Linux kernel, така и Sentry на gVisor позволяват **най-много един** слушател `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) на йерархия от задачи. Тъй като външният sandbox вече държи този слот за слушател, вътрешното извикване `seccomp(2)` връща `EBUSY` (`device or resource busy`).

---

## Как `gvx` решава вложеното изпълнение в `gVisor`

`gvx` се справя с всичките пет бариери в собствения си потребителски kernel и слой на платформата, което позволява `rungv` да работи във външен контейнер `gVisor` (`runsc`) или дори рекурсивно в друг контейнер `gvx` (`gvx` в `gvx`).

![Вложено изпълнение на gvx във външен gVisor](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Автоматичен fallback от `systrap` от `EBUSY` към futex цикъла `_RUN_SYSCALL_LOOP`

В Linux `systrap` всеки stub подпроцес изпълнява два вида нишки:

- **Нишки за изпълнение на гост (`sysmsg`)**: прихващат системни извиквания на госта чрез `SECCOMP_RET_TRAP` (`SIGSYS`) и бинарно пачнати `jmp rel32` трамплини (`syshandler`), като обменят състояние на регистрите със Sentry чрез споделена памет и `SYS_FUTEX`.
- **Stub `syscallThread` (`syscall_thread.go`)**: изпълнява системни извиквания за адресното пространство (`mmap`, `munmap`, `clone`) в stub подпроцеса от името на Sentry.

В upstream gVisor `syscallThread` поддържа два режима на събуждане в `stub_amd64.s` и `stub_arm64.s`:

1. **Seccomp User-Notify цикъл (`_RUN_SECCOMP_LOOP = 6`, когато `t.seccompNotify != nil`)**: По време на `syscallThread.init()` `t.installSeccompNotify()` инсталира слушател `SECCOMP_RET_USER_NOTIF` (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`), така че Sentry да може да изпраща заявки чрез `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` и `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Цикъл за futex системни извиквания (`_RUN_SYSCALL_LOOP = 5`, когато `t.seccompNotify == nil`)**: `syscallThread` изпълнява `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`), като спи върху `SYS_FUTEX` (`FUTEX_WAIT`) над споделената двустранична област `syscallSentryMessage` (`PROT_READ` в stub) и `syscallStubMessage` (`PROT_READ | PROT_WRITE` в stub).

В upstream gVisor `t.installSeccompNotify()` се извиква безусловно, когато `seccompNotifyIsSupported` е true, а създаването на stub се прекратява, ако `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` върне `EBUSY`.

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

Когато `installSeccompNotify()` върне `unix.EBUSY`, защото външен `gVisor` (`runsc`) или външен sandbox `gvx` вече държи единствения слот за слушател във веригата от филтри на процеса:

- `installSeccompNotifyIfFree()` задава `seccompNotifyBusy` (`atomic.Bool`), записва едно предупреждение за процеса и оставя `t.seccompNotify == nil` за този stub и за всички следващи stub-ове.
- В `syscallThread.detach()` (`syscall_thread_amd64.go` и `syscall_thread_arm64.go`) при `t.seccompNotify == nil` се задава `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) на `amd64` (или `regs.Regs[9] = _RUN_SYSCALL_LOOP` на `arm64`) вместо `_RUN_SECCOMP_LOOP` (`6`).
- В `syscallThread.syscall()` (`syscall_thread.go`) вътрешният Sentry изпраща системните извиквания на stub-а изцяло чрез споделена памет и `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Уведомяване на syscall нишката за нова заявка за системно извикване.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Изчакване на отговор.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Тъй като филтрите `SECCOMP_RET_TRAP` върху нишките на госта могат да бъдат произволно дълбоко в стека, а `syscallThread` преминава към `_RUN_SYSCALL_LOOP`, без да изисква втори слушател за потребителски нотификации, `gvx` работи чисто в `runsc` и поддържа рекурсивно вложване (`gvx` в `gvx`).

### 2. Нативен `overlayfs` върху единични `ext4lite` томове

За да се избегне монтиране на `overlayfs` върху root файловата система `gofer` (`9p`/`lisafs`) на външния контейнер gVisor, `-v gvx-docker:/var/lib/docker` поддържа `/var/lib/docker` с вътрешната файлова система **`ext4lite`** на `gvx`, съхранена в единичен обикновен файл (`gvx-docker.ext4`).

За външния `runsc` `gvx-docker.ext4` е просто един обикновен файл, достъпван чрез `pread64(2)` и `pwrite64(2)`. Вътре в `gvx`, `ext4lite` осигурява файлова система `ext4` с JBD2 журнал, с разширени атрибути `trusted.overlay.*` и `security.capability`, възли whiteout от тип символно устройство (`0:0`), записи в директории `d_type` и `RENAME_EXCHANGE`, което позволява на вътрешния `dockerd` да използва нативния си драйвер за съхранение `overlayfs`.

### 3. Самостоятелен `netstack` с `docker0` и `iptables-legacy`

Вътре в `gvx`, `dockerd` не взаимодейства с мрежовите интерфейси или netfilter таблиците на външния контейнер `runsc`. Вместо това:

- `dockerd` създава моста `docker0` и `veth` двойките вътре в собствения потребителски `netstack` на `gvx`.
- Всички правила `iptables-legacy` (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) се изпълняват срещу вътрешната netfilter имплементация на `gvx`.
- Вложените `runc` контейнери монтират своите йерархии `/proc`, `/sys` и `/sys/fs/cgroup` във вътрешната таблица на монтиранията `VFS2` в паметта на `gvx`, без никога да извикват системното извикване `mount(2)` на външния `runsc`.

---

## Проверка на вложен `docker:dind` в `gVisor` (или `gvx` в `gvx`)

Можете да проверите и двата сценария (`rungv` в `runsc` или `rungv` в `rungv`) със следните команди:

```sh
# Във външен контейнер gVisor (runsc) или външен контейнер 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

# Проверете, че вътрешният Docker daemon използва нативен overlayfs (а не vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Изход: Storage Driver: overlayfs

# Изпълнете многослоен build и стартирайте вложен контейнер
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
# Изход: nested-build-ok

kill -INT $DIND_PID
```

---

## Допълнително четене в тази серия

1. **[gvx: Стартиране на Linux контейнери, Docker и Kubernetes в един непривилегирован процес](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Преглед на `gvx` в Windows (`whpx`, `systrap`), macOS (`hvf`) и Linux (`systrap`).
2. **[Изпълнение на Docker, docker build и docker compose без root или User Namespaces](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Как `gvx` изпълнява `docker:dind`, когато `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` и `/dev/fuse` са блокирани.
3. **[Docker-in-Docker в gVisor и вложени sandbox среди с gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (тази публикация)
4. **[Бързи изолирани Docker среди за RL rollouts на AI агенти с gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Как да използвате `gvx` и единични `ext4lite` моментни снимки copy-on-write, за да стартирате стотици паралелни среди за `SWE-bench` и RL rollouts с нулиране за 100 милисекунди.
