---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Запуск Docker-in-Docker внутри gVisor и вложенных песочниц с помощью gvx
description: Почему установка docker.io или вложенный запуск runsc внутри контейнера gVisor завершаются сбоем из-за modprobe, iptables-nft, overlay2, cgroups и EBUSY в seccomp, и как gvx безопасно запускает вложенный docker:dind внутри изолированного пользовательского ядра без --privileged.
language: ru
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`)

Облачные платформы AI-агентов, развертывания Kubernetes `agent-sandbox` (`RuntimeClass: gvisor`) и среды оценки для обучения с подкреплением изолируют недоверенные рабочие нагрузки внутри контейнеров **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`**.

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

```sh
# Внутри существующего контейнера gVisor (runsc) или пода Kubernetes:
# Установите rungv из архива релиза GitHub с аттестацией 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 через внутренний сокет 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 ... (containerized)

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

---

## Пять причин, по которым прямая установка Docker завершается ошибкой внутри `gVisor` (`runsc`)

Когда пользователь или ИИ-агент для написания кода выполняет `apt-get install -y docker.io` (или `docker-ce`) внутри стандартного контейнера `runsc` и пытается запустить `dockerd`, он сталкивается с пятью последовательными техническими барьерами.

### 1. Загрузка модулей ядра (`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` (Netlink `nf_tables`) и моста `docker0`

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

Если внешний администратор `runsc` явно не настроил выделенное монтирование `tmpfs` или filestore в `/var/lib/docker`, корневая файловая система контейнера опирается на `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`) всей корневой файловой системы, превращая образ размером 500 МБ на 10 шагах сборки в 5 ГБ записей и завершая работу песочницы с ошибкой `no space left on device` или тайм-аутом ввода-вывода.

### 4. Отказы вложенных монтирований cgroup и `/proc` в `runc`

Даже если `dockerd` принудительно запущен командой `dockerd --iptables=false --bridge=none --storage-driver=vfs`, выполнение `docker run` или `docker build` вызывает `runc` для создания внутреннего контейнера. В контейнере gVisor без дополнительных прав `runc` завершается ошибкой при монтировании cgroup, повторном монтировании `/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, и Sentry gVisor допускают **не более одного** слушателя `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) в иерархии задач. Поскольку внешняя песочница уже занимает этот слот слушателя, внутренний вызов `seccomp(2)` возвращает `EBUSY` (`device or resource busy`).

---

## Как `gvx` решает задачу вложенного выполнения внутри `gVisor`

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

![Вложенное выполнение gvx внутри внешнего gVisor](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Автоматический переход `systrap` с `EBUSY` на цикл futex `_RUN_SYSCALL_LOOP`

В Linux `systrap` каждый подпроцесс-заглушка (stub) запускает два типа потоков:

- **Потоки выполнения гостевых процессов (`sysmsg`)**: перехватывают системные вызовы гостя через `SECCOMP_RET_TRAP` (`SIGSYS`) и бинарно пропатченные трамплины `jmp rel32` (`syshandler`), обмениваясь состоянием регистров с Sentry через разделяемую память и `SYS_FUTEX`.
- **Поток заглушки `syscallThread` (`syscall_thread.go`)**: выполняет системные вызовы адресного пространства (`mmap`, `munmap`, `clone`) внутри подпроцесса-заглушки от имени 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` в заглушке) и `syscallStubMessage` (`PROT_READ | PROT_WRITE` в заглушке).

Upstream gVisor безусловно вызывает `t.installSeccompNotify()` всякий раз, когда `seccompNotifyIsSupported` равно true, и прерывает создание заглушки, если `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`) или внешний `gvx` уже занимает единственный слот слушателя в цепочке фильтров процесса:

- `installSeccompNotifyIfFree()` устанавливает `seccompNotifyBusy` (`atomic.Bool`), записывает одно предупреждение для процесса и оставляет `t.seccompNotify == nil` для этой заглушки и всех последующих.
- В `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 отправляет системные вызовы заглушки исключительно через разделяемую память и `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Уведомить поток системных вызовов о новом запросе системного вызова.
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` поверх корневой файловой системы `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 использует нативный overlayfs (а не vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Вывод: Storage Driver: overlayfs

# Выполнить многослойную сборку и запустить вложенный контейнер
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 и пользовательских пространств имен](/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 и вложенных песочниц с gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (этот пост)
4. **[Быстрые изолированные среды Docker для RL-развертываний AI-агентов с gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: как использовать `gvx` и снимки copy-on-write на основе одного файла `ext4lite` для запуска сотен параллельных сред `SWE-bench` и RL-развертываний со сбросом за 100 миллисекунд.
