---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Menjalankan Docker-in-Docker di Dalam gVisor dan Nested Sandboxes dengan gvx
description: Mengapa instalasi docker.io atau penyarangan runsc di dalam kontainer gVisor gagal pada modprobe, iptables-nft, overlay2, cgroups, dan seccomp EBUSY, serta bagaimana gvx menjalankan docker:dind bersarang secara aman di dalam kernel ruang pengguna yang terisolasi tanpa --privileged.
language: id
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
---

## Solusi Cepat: Menjalankan `docker:dind` secara Aman di Dalam Sandbox `gVisor` (`runsc`) Tanpa Hak Istimewa

Platform agen AI berbasis cloud, deployment Kubernetes `agent-sandbox` (`RuntimeClass: gvisor`), dan harness evaluasi reinforcement learning mengisolasi beban kerja yang tidak tepercaya di dalam kontainer **gVisor (`runsc`)** secara khusus untuk melindungi kernel host. Ketika beban kerja di dalam kontainer `runsc` tanpa hak istimewa standar mencoba memasang `docker.io` atau `docker-ce` melalui `apt-get` dan menjalankan `dockerd`, proses tersebut gagal meskipun memiliki `root` (`UID 0`) di dalam kontainer.

Dengan **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`), Anda dapat **secara aman** menjalankan `docker:dind`, `docker build`, dan `docker compose` langsung di dalam kontainer `gVisor` (`runsc`) tanpa hak istimewa (atau menyarangkan `gvx` di dalam `gvx`) **tanpa melemahkan `RuntimeClass` luar, membuka soket host, atau memberikan `--privileged`**.

> **Menjaga dan Memperkuat Isolasi Sandbox Luar**: Menjalankan `rungv` di dalam `runsc` tidak melewati satu pun pembatasan yang diberlakukan oleh sandbox `gVisor` luar. Sebaliknya, `gvx` membuat kernel `Sentry` gVisor bersarang kedua yang berjalan di ruang pengguna dengan `hostlockdown` sendiri (`PR_SET_NO_NEW_PRIVS` + `seccomp`), `netstack` dalam memori sendiri, dan volume `ext4lite` berkas tunggal. `dockerd` dan kontainer `runc` internal berada dalam sandbox ganda: panggilan sistem, mount, dan paket mentah mereka tidak pernah keluar dari kernel ruang pengguna `gvx` internal.

```sh
# Di dalam kontainer gVisor (runsc) yang sudah ada atau Kubernetes Pod:
# Pasang rungv dari arsip rilis GitHub yang teratestasi SLSA atau melalui 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 &

# Jalankan build dan kontainer Docker standar terhadap soket gvx internal
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}}'
# Keluaran: overlayfs Alpine Linux ... (dalam kontainer)

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

---

## Lima Alasan Mengapa Instalasi Docker Langsung Gagal di Dalam `gVisor` (`runsc`)

Ketika pengguna atau agen pengodean AI menjalankan `apt-get install -y docker.io` (atau `docker-ce`) di dalam kontainer `runsc` standar dan mencoba menjalankan `dockerd`, proses tersebut menghadapi lima hambatan teknis yang berurutan.

### 1. Pemuatan Modul Kernel (`modprobe`) Terhadap Kernel Virtual `4.4.0`

Selama instalasi paket dan startup `dockerd`, skrip inisialisasi menjalankan `modprobe` untuk memuat `overlay`, `br_netfilter`, `ip_tables`, `nf_nat`, dan `xt_conntrack`. Sentry gVisor luar memblokir `init_module(2)` dan `finit_module(2)` serta melaporkan versi kernel virtual (`4.4.0`) yang tidak memiliki direktori `/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. Kegagalan `iptables-nft` (Netlink `nf_tables`) dan Bridge `docker0`

Pada Ubuntu 22.04, Ubuntu 24.04, dan Debian 11/12, `apt-get install docker.io` memilih `iptables-nft` (didukung oleh subsistem netlink `nf_tables`) sebagai alternatif `/usr/sbin/iptables` default. Dalam kontainer gVisor standar tanpa hak administrasi jaringan atau dukungan netlink `nf_tables`, `dockerd` mengalami crash saat menginisialisasi rantai NAT `DOCKER` dan bridge `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. Kegagalan `overlay2` pada Rootfs `gofer` / `9p` dan Kehabisan Disk `vfs`

Kecuali administrator `runsc` luar telah mengonfigurasi terlebih dahulu mount `tmpfs` atau filestore khusus di `/var/lib/docker`, rootfs kontainer didukung oleh `gofer` (`9p`/`lisafs`) gVisor atau mount overlay bersarang. Mount `overlay2` di atas `/var/lib/docker` gagal:

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

Ketika pengguna atau agen mencoba melewati kesalahan ini dengan menjalankan `dockerd --storage-driver=vfs`, Docker sepenuhnya menonaktifkan berbagi lapisan Copy-on-Write. Setiap instruksi `RUN` dalam `Dockerfile` melakukan penyalinan rekursif penuh (`dirCopy`) dari seluruh root filesystem, sehingga image 500 MB yang tersebar di 10 langkah build berubah menjadi 5 GB penulisan dan menghentikan sandbox dengan `no space left on device` atau timeout I/O.

### 4. Penolakan Mount Cgroup dan `/proc` pada `runc` Bersarang

Bahkan jika `dockerd` dipaksa untuk berjalan dengan `dockerd --iptables=false --bridge=none --storage-driver=vfs`, menjalankan `docker run` atau `docker build` memanggil `runc` untuk membuat kontainer internal. Dalam kontainer gVisor tanpa hak istimewa standar, `runc` gagal saat melakukan mount cgroup, me-mount ulang `/proc`, atau memuat filter seccomp internal:

```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` Bersarang Gagal dengan `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Jika pengguna mencoba menjalankan instans `runsc` lain di dalam `runsc` menggunakan platform `systrap`, inisialisasi langsung dibatalkan:

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

Baik kernel Linux maupun Sentry gVisor hanya mengizinkan **paling banyak satu** listener `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) per hierarki task. Karena sandbox luar sudah menempati slot listener tersebut, pemanggilan `seccomp(2)` internal mengembalikan `EBUSY` (`device or resource busy`).

---

## Cara `gvx` Menyelesaikan Eksekusi Bersarang di Dalam `gVisor`

`gvx` mengatasi kelima hambatan tersebut di dalam kernel ruang pengguna dan lapisan platformnya sendiri, sehingga `rungv` dapat berjalan di dalam kontainer `gVisor` (`runsc`) luar, bahkan secara rekursif di dalam kontainer `gvx` lain (`gvx` dalam `gvx`).

![Eksekusi gvx Bersarang di Dalam gVisor Luar](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Fallback `systrap` Otomatis dari `EBUSY` ke Loop Futex `_RUN_SYSCALL_LOOP`

Dalam `systrap` Linux, setiap subproses stub menjalankan dua jenis thread:

- **Thread eksekusi tamu (`sysmsg`)**: Menjebak panggilan sistem tamu melalui `SECCOMP_RET_TRAP` (`SIGSYS`) dan trampolin `jmp rel32` yang telah dipatch secara biner (`syshandler`), bertukar state register dengan Sentry melalui memori bersama dan `SYS_FUTEX`.
- **Satu `syscallThread` stub (`syscall_thread.go`)**: Menjalankan panggilan sistem ruang alamat (`mmap`, `munmap`, `clone`) di dalam subproses stub atas nama Sentry.

Dalam gVisor hulu, `syscallThread` mendukung dua mode wake-up pada `stub_amd64.s` dan `stub_arm64.s`:

1. **Loop User-Notify Seccomp (`_RUN_SECCOMP_LOOP = 6`, ketika `t.seccompNotify != nil`)**: Selama `syscallThread.init()`, `t.installSeccompNotify()` memasang listener `SECCOMP_RET_USER_NOTIF` (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`) sehingga Sentry dapat mengirim permintaan melalui `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` dan `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Loop Panggilan Sistem Futex (`_RUN_SYSCALL_LOOP = 5`, ketika `t.seccompNotify == nil`)**: `syscallThread` menjalankan `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`), tertidur pada `SYS_FUTEX` (`FUTEX_WAIT`) di atas region bersama `syscallSentryMessage` berukuran 2 halaman (`PROT_READ` di stub) dan `syscallStubMessage` (`PROT_READ | PROT_WRITE` di stub).

gVisor hulu secara tanpa syarat memanggil `t.installSeccompNotify()` setiap kali `seccompNotifyIsSupported` bernilai true, dan membatalkan pembuatan stub jika `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` mengembalikan `EBUSY`.

Di `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` dan `syscall_thread.go`), `syscallThread.init()` memanggil `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
}
```

Ketika `installSeccompNotify()` mengembalikan `unix.EBUSY` karena sandbox `gVisor` (`runsc`) luar atau sandbox `gvx` luar sudah menempati slot listener tunggal pada rantai filter proses:

- `installSeccompNotifyIfFree()` mengatur `seccompNotifyBusy` (`atomic.Bool`), mencatat satu peringatan untuk proses tersebut, dan membiarkan `t.seccompNotify == nil` untuk stub tersebut dan semua stub berikutnya.
- Dalam `syscallThread.detach()` (`syscall_thread_amd64.go` dan `syscall_thread_arm64.go`), `t.seccompNotify == nil` menetapkan `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) pada `amd64` (atau `regs.Regs[9] = _RUN_SYSCALL_LOOP` pada `arm64`) alih-alih `_RUN_SECCOMP_LOOP` (`6`).
- Dalam `syscallThread.syscall()` (`syscall_thread.go`), Sentry internal mengirim panggilan sistem stub semata-mata melalui memori bersama dan `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Beri tahu thread syscall tentang permintaan panggilan sistem baru.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Tunggu balasan.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Karena filter `SECCOMP_RET_TRAP` pada thread tamu ditumpuk secara sembarang dalam kedalaman, dan `syscallThread` kembali ke `_RUN_SYSCALL_LOOP` tanpa meminta listener notifikasi pengguna kedua, `gvx` berjalan dengan bersih di dalam `runsc` dan mendukung penyarangan rekursif (`gvx` di dalam `gvx`).

### 2. `overlayfs` Native pada Volume `ext4lite` Berkas Tunggal

Untuk menghindari mount `overlayfs` di atas root filesystem `gofer` (`9p`/`lisafs`) kontainer gVisor luar, `-v gvx-docker:/var/lib/docker` mendukung `/var/lib/docker` dengan filesystem internal `ext4lite` milik `gvx` yang disimpan dalam satu berkas reguler (`gvx-docker.ext4`).

Bagi `runsc` luar, `gvx-docker.ext4` hanyalah satu berkas reguler yang diakses melalui `pread64(2)` dan `pwrite64(2)`. Di dalam `gvx`, `ext4lite` menyediakan filesystem `ext4` dengan jurnal JBD2, atribut terperluas `trusted.overlay.*` dan `security.capability`, node whiteout perangkat karakter (`0:0`), entri direktori `d_type`, dan `RENAME_EXCHANGE`, sehingga `dockerd` internal dapat menggunakan driver penyimpanan `overlayfs` aslinya.

### 3. `netstack` Mandiri dengan `docker0` dan `iptables-legacy`

Di dalam `gvx`, `dockerd` tidak berinteraksi dengan antarmuka jaringan atau tabel netfilter kontainer `runsc` luar. Sebaliknya:

- `dockerd` membuat bridge `docker0` dan pasangan `veth` di dalam `netstack` ruang pengguna milik `gvx` sendiri.
- Semua aturan `iptables-legacy` (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) dieksekusi terhadap implementasi netfilter internal `gvx`.
- Kontainer `runc` bersarang me-mount hierarki `/proc`, `/sys`, dan `/sys/fs/cgroup` mereka di dalam tabel mount `VFS2` dalam memori milik `gvx`, tanpa pernah memanggil syscall `mount(2)` `runsc` luar.

---

## Memverifikasi `docker:dind` Bersarang di Dalam `gVisor` (atau `gvx` dalam `gvx`)

Anda dapat memverifikasi kedua skenario (`rungv` di dalam `runsc`, atau `rungv` di dalam `rungv`) dengan perintah berikut:

```sh
# Di dalam kontainer gVisor (runsc) luar atau kontainer rungv luar:
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

# Verifikasi bahwa daemon Docker internal menggunakan overlayfs asli (bukan vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Keluaran: Storage Driver: overlayfs

# Jalankan build multi-lapisan dan eksekusi kontainer bersarang
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
# Keluaran: nested-build-ok

kill -INT $DIND_PID
```

---

## Bacaan Lanjutan dalam Seri Ini

1. **[gvx: Menjalankan Kontainer Linux, Docker, dan Kubernetes dalam Satu Proses Tanpa Hak Istimewa](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Gambaran umum `gvx` di Windows (`whpx`, `systrap`), macOS (`hvf`), dan Linux (`systrap`).
2. **[Menjalankan Docker, docker build, dan docker compose Tanpa Root atau User Namespace](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Cara `gvx` menjalankan `docker:dind` ketika `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)`, dan `/dev/fuse` diblokir.
3. **[Menjalankan Docker-in-Docker di Dalam gVisor dan Sandbox Bersarang dengan gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (artikel ini)
4. **[Lingkungan Docker Terisolasi yang Cepat untuk Rollout RL Agen AI dengan gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Cara menggunakan `gvx` dan snapshot copy-on-write `ext4lite` berkas tunggal untuk menjalankan ratusan lingkungan rollout paralel `SWE-bench` dan RL dengan reset 100 milidetik.
