gVisor 및 중첩 샌드박스 내부에서 gvx로 Docker-in-Docker 실행하기

빠른 해결책: 권한이 없는 gVisor (runsc) 샌드박스 내부에서 docker:dind를 안전하게 실행하기
클라우드 AI 에이전트 컴퓨팅 환경(예: Google Agent Compute Engine 샌드박스), Kubernetes agent-sandbox 배포(RuntimeClass: gvisor), 강화학습 평가 하네스는 호스트 커널을 보호하기 위해 신뢰할 수 없는 워크로드를 gVisor (runsc) 컨테이너 내부에 격리합니다. 기본 권한 없는(unprivileged) runsc 컨테이너 내부의 워크로드가 apt-get으로 docker.io 또는 docker-ce를 설치하고, service docker start 또는 dockerd를 시작하며, docker build를 실행하려고 하면, 컨테이너 내부에서 root(UID 0) 권한을 가지고 있더라도 실패합니다.
gvx (rungv)를 사용하면 권한 없는 gVisor (runsc) 컨테이너 내부에서(또는 gvx 내부에 gvx를 중첩하여) docker:dind, docker build, docker compose를 안전하게 직접 실행할 수 있으며, 이 과정에서 외부 RuntimeClass를 약화시키거나, 호스트 소켓을 노출하거나, --privileged를 부여할 필요가 없습니다.
AI 에이전트를 위한 낮은 노력, 토큰 효율적 해결책: gVisor 기반 클라우드 에이전트 환경에서
docker.io를 설치하고/etc/init.d/docker의ulimit오류,iptables-nft실패,iptables-legacy의 raw 소켓 거부,overlay2마운트 오류,docker build의runc/pivot_root충돌을 수동으로 우회하려고 시도하면, 일반적으로 완전히 실패하기 전까지 20회 이상의 도구 호출 턴과 수만 토큰을 소모합니다.gvx(rungv)는 낮은 노력으로 토큰을 효율적으로 사용할 수 있는 즉시 적용 가능한 해결책입니다. 에이전트는 단 한 턴에 명령어 3개를 실행하여 작동하는 사용자 공간 Docker 데몬을 시작하고, 프로젝트 파일을 수정하지 않고도 즉시docker build와docker compose를 실행합니다.
외부 샌드박스 격리의 보존 및 강화:
runsc내부에서rungv를 실행한다고 해서 외부gVisor샌드박스가 적용하는 제한 중 어느 하나라도 우회되는 것은 아닙니다. 대신gvx는 자체hostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), 자체 인메모리netstack, 그리고 단일 파일ext4lite볼륨을 갖춘 두 번째 중첩 사용자 공간 gVisorSentry커널을 생성합니다. 내부dockerd와runc컨테이너는 이중으로 샌드박싱되며, 이들의 시스템 호출, 마운트, raw 패킷은 내부gvx사용자 공간 커널을 벗어나지 않습니다.
1# 기존 gVisor (runsc) 컨테이너 또는 Kubernetes Pod 내부에서:
2# SLSA 인증을 받은 GitHub 릴리스 아카이브에서 rungv를 설치하거나 go install을 통해 설치합니다:
3CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest
4
5mkdir -p ~/.gvx
6rungv run \
7 -p unix:~/.gvx/docker.sock:2375 \
8 -v gvx-docker:/var/lib/docker \
9 -e DOCKER_TLS_CERTDIR= \
10 docker:dind --tls=false &
11
12# 내부 gvx 소켓을 대상으로 표준 Docker 빌드 및 컨테이너를 실행합니다
13export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
14until docker info >/dev/null 2>&1; do sleep 0.5; done
15docker info --format '{{.Driver}} {{.OperatingSystem}}'
16# 출력: overlayfs Alpine Linux ... (컨테이너화됨)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test
gVisor (runsc) 내부에서 Docker를 직접 설치하면 실패하는 다섯 가지 이유
사용자 또는 AI 코딩 에이전트가 표준 runsc 컨테이너 내부에서 apt-get install -y docker.io(또는 docker-ce)를 실행하고 dockerd를 시작하려고 하면, 다섯 가지 연속적인 기술적 장벽에 부딪힙니다.
1. 초기화 스크립트(systemctl / service docker start ulimit) 및 커널 모듈(modprobe 4.4.0) 실패
apt-get install -y docker.io(또는 docker-ce)를 실행한 후, PID 1이 systemd가 아니고 /etc/init.d/docker가 샌드박스의 하드 RLIMIT_NOFILE 상한을 초과하여 ulimit -n을 올리려고 시도하기 때문에, systemctl start docker 또는 service docker start로 데몬을 시작하려는 시도는 즉시 실패합니다. 그 후 dockerd를 직접 호출하면, 초기화 스크립트가 modprobe를 호출하여 overlay, bridge, br_netfilter, ip_tables, nf_nat 모듈을 gVisor의 가상 커널 버전(4.4.0)에 대해 로드하려고 시도하는데, 이 버전에는 /lib/modules/4.4.0 디렉터리가 존재하지 않습니다:
1System has not been booted with systemd as init system (PID 1). Can't operate.
2Failed to connect to bus: Host is down
3/etc/init.d/docker: 62: ulimit: error setting limit (Operation not permitted)
4level=warning msg="Running modprobe bridge br_netfilter failed with message: modprobe: WARNING: Module bridge not found in directory /lib/modules/4.4.0\nmodprobe: WARNING: Module br_netfilter not found in directory /lib/modules/4.4.0\n, error: exit status 1"
5modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
6modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
7modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
8modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
2. iptables-nft(nf_tables Netlink), iptables-legacy raw 소켓 거부 및 docker0 브리지 실패
Ubuntu 22.04, Ubuntu 24.04, Debian 11/12에서 apt-get install docker.io는 기본 /usr/sbin/iptables 대안으로 iptables-nft(nf_tables netlink 서브시스템 기반)를 선택합니다. 나아가, 사용자 또는 에이전트가 iptables-legacy(update-alternatives --set iptables /usr/sbin/iptables-legacy)로 전환하더라도, 기본 권한 없는 gVisor 샌드박스(--net-raw=false)는 AF_INET, SOCK_RAW 소켓을 EPERM으로 거부하므로, UID 0으로 실행하는 경우에도 iptables-legacy가 Permission denied (you must be root)와 함께 실패합니다:
1failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to register "bridge" driver: failed to create NAT chain DOCKER: iptables failed: iptables --wait -t nat -N DOCKER: iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
2iptables v1.8.10 (legacy): can't initialize iptables table `nat': Permission denied (you must be root)
3Perhaps iptables or your kernel needs to be upgraded.
4iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
5iptables: No chain/target/match by that name.
6Unable 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.)
7Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
3. gofer / 9p 루트 파일 시스템에서의 overlay2 실패 및 vfs로 인한 디스크 고갈
외부 runsc 관리자가 /var/lib/docker에 전용 tmpfs 또는 filestore 마운트를 명시적으로 사전 구성하지 않는 한, 컨테이너 루트 파일 시스템은 gVisor의 gofer(9p/lisafs) 또는 중첩 overlay 마운트에 의해 지원됩니다. /var/lib/docker 위에 overlay2를 마운트하는 작업은 실패합니다:
1level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
2failed to mount overlay: invalid argument
3Error starting daemon: error initializing graphdriver: driver not supported: overlay2
사용자나 에이전트가 dockerd --storage-driver=vfs를 시작하여 이 오류를 우회하려고 시도하면, Docker는 Copy-on-Write 레이어 공유를 완전히 비활성화합니다. Dockerfile의 모든 RUN 명령은 전체 루트 파일 시스템을 재귀적으로 전체 복사(dirCopy)하게 되며, 이로 인해 10개의 빌드 단계에 걸친 500 MB 이미지가 5 GB의 쓰기 작업으로 늘어나고, 샌드박스는 no space left on device 또는 I/O 타임아웃으로 종료됩니다.
4. docker build(BuildKit) 및 중첩 runc 실패(pivot_root, /proc, Cgroups, DNS)
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs로 dockerd를 강제로 시작하더라도, BuildKit 또는 runc가 빌드 컨테이너를 생성할 때 docker build -t app . 또는 docker run을 실행하면 즉시 실패합니다:
1ERROR: failed to solve: process "/bin/sh -c ..." did not complete successfully:
2docker: 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:
3error during container init: error jailing process inside rootfs: pivot_root .: invalid argument
4error 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
5cgroups: cgroup mountpoint does not exist: unknown
6runc create failed: systemd not running on this host: cannot use systemd cgroup manager
7error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
8error during container init: error loading seccomp filter into kernel: operation not permitted
9Temporary failure resolving 'deb.debian.org'
10Temporary failure resolving 'archive.ubuntu.com'
5. 중첩 runsc가 SECCOMP_FILTER_FLAG_NEW_LISTENER(EBUSY)와 함께 실패
사용자가 systrap 플랫폼을 사용하여 runsc 내부에서 또 다른 runsc 인스턴스를 실행하려고 하면, 초기화가 즉시 중단됩니다:
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Linux 커널과 gVisor Sentry는 모두 태스크 계층 구조당 최대 하나의 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) 실행될 수 있도록 합니다.

1. EBUSY에서 _RUN_SYSCALL_LOOP futex 루프로의 자동 systrap 폴백
Linux systrap에서 각 stub 하위 프로세스는 두 종류의 스레드를 실행합니다:
- 게스트 실행 스레드(
sysmsg):SECCOMP_RET_TRAP(SIGSYS)과 바이너리 패치된jmp rel32트램펄린(syshandler)을 통해 게스트 시스템 호출을 트랩하고, 공유 메모리와SYS_FUTEX를 통해 Sentry와 레지스터 상태를 교환합니다. - stub
syscallThread(syscall_thread.go): Sentry를 대신하여 stub 하위 프로세스 내부에서 주소 공간 시스템 호출(mmap,munmap,clone)을 실행합니다.
업스트림 gVisor에서 syscallThread는 stub_amd64.s 및 stub_arm64.s에서 두 가지 깨우기 모드를 지원합니다:
- 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)를 통해 요청을 전달할 수 있도록 합니다. - Futex 시스템 호출 루프(
_RUN_SYSCALL_LOOP = 5,t.seccompNotify == nil인 경우):syscallThread는syscall_loop(stub_amd64.s/stub_arm64.s)을 실행하며, 공유된 2페이지syscallSentryMessage(stub에서PROT_READ)와syscallStubMessage(stub에서PROT_READ | PROT_WRITE) 영역에 대해SYS_FUTEX(FUTEX_WAIT)에서 대기합니다.
업스트림 gVisor는 seccompNotifyIsSupported가 참일 때 무조건 t.installSeccompNotify()를 호출하며, seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)가 EBUSY를 반환하면 stub 생성을 중단합니다.
gvx(pkg/sentry/platform/systrap/seccomp_notify_xplat.go 및 syscall_thread.go)에서는 syscallThread.init()가 t.installSeccompNotifyIfFree()를 호출합니다:
1// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
2var seccompNotifyBusy atomic.Bool
3
4func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
5 if seccompNotifyBusy.Load() {
6 return nil, nil
7 }
8 f, err := t.installSeccompNotify()
9 if errors.Is(err, unix.EBUSY) {
10 if seccompNotifyBusy.CompareAndSwap(false, true) {
11 log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
12 "The stub threads use the slower futex loop instead of seccomp-unotify.")
13 }
14 return nil, nil
15 }
16 return f, err
17}
외부 gVisor(runsc) 또는 외부 gvx 샌드박스가 이미 프로세스 필터 체인의 단일 리스너 슬롯을 점유하고 있어 installSeccompNotify()가 unix.EBUSY를 반환하는 경우:
installSeccompNotifyIfFree()는seccompNotifyBusy(atomic.Bool)를 설정하고, 프로세스당 한 번의 경고를 기록하며, 해당 stub과 이후의 모든 stub에 대해t.seccompNotify == nil을 유지합니다.syscallThread.detach()(syscall_thread_amd64.go및syscall_thread_arm64.go)에서t.seccompNotify == nil이면,_RUN_SECCOMP_LOOP(6) 대신amd64에서는regs.Rbx = _RUN_SYSCALL_LOOP(5)을,arm64에서는regs.Regs[9] = _RUN_SYSCALL_LOOP을 설정합니다.syscallThread.syscall()(syscall_thread.go)에서 내부 Sentry는 stub 시스템 호출을 오직 공유 메모리와SYS_FUTEX를 통해 디스패치합니다:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// 새로운 시스템 호출 요청을 syscall 스레드에 알립니다.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// 응답을 기다립니다.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
SECCOMP_RET_TRAP 필터가 게스트 스레드 스택에서 임의로 깊게 동작하고, syscallThread가 두 번째 사용자 알림 리스너를 요청하지 않고 _RUN_SYSCALL_LOOP으로 폴백하므로, gvx는 runsc 내부에서 문제없이 실행되며 재귀적 중첩(gvx 내부의 gvx)도 지원합니다.
2. 단일 파일 ext4lite 볼륨 위의 네이티브 overlayfs
외부 gVisor 컨테이너의 gofer(9p/lisafs) 루트 파일 시스템 위에 overlayfs를 마운트하는 것을 피하기 위해, -v gvx-docker:/var/lib/docker는 /var/lib/docker를 gvx의 내부 ext4lite 파일 시스템으로 뒷받침하며, 이는 단일 일반 파일(gvx-docker.ext4)에 저장됩니다.
외부 runsc에게 gvx-docker.ext4는 pread64(2) 및 pwrite64(2)로 접근되는 단순한 일반 파일 하나에 불과합니다. gvx 내부에서 ext4lite는 JBD2 저널링 ext4 파일 시스템을 제공하며, trusted.overlay.* 및 security.capability 확장 속성, 문자 장치 whiteout 노드(0:0), d_type 디렉터리 항목, RENAME_EXCHANGE를 지원하여, 내부 dockerd가 네이티브 overlayfs 스토리지 드라이버를 사용할 수 있도록 합니다.
3. docker0 및 iptables-legacy를 갖춘 자체 포함형 netstack
gvx 내부에서 dockerd는 외부 runsc 컨테이너의 네트워크 인터페이스나 netfilter 테이블과 상호작용하지 않습니다. 대신:
dockerd는gvx자체의 사용자 공간netstack내부에docker0브리지와veth쌍을 생성합니다.- 모든
iptables-legacy규칙(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype)은gvx의 내부 netfilter 구현을 대상으로 실행됩니다. - 중첩된
runc컨테이너는gvx의 인메모리VFS2마운트 테이블 내부에서/proc,/sys,/sys/fs/cgroup계층 구조를 마운트하며, 외부runsc의mount(2)시스템 호출을 결코 호출하지 않습니다.
gVisor 내부(또는 gvx 내부의 gvx)에서 중첩 docker:dind 검증하기
다음 명령어로 두 시나리오(runsc 내부의 rungv, 또는 rungv 내부의 rungv)를 모두 검증할 수 있습니다:
1# 외부 gVisor (runsc) 컨테이너 또는 외부 rungv 컨테이너 내부에서:
2mkdir -p ~/.gvx
3rungv run \
4 -p unix:~/.gvx/docker.sock:2375 \
5 -v gvx-docker:/var/lib/docker \
6 -e DOCKER_TLS_CERTDIR= \
7 docker:dind --tls=false &
8DIND_PID=$!
9
10export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
11until docker info >/dev/null 2>&1; do sleep 0.5; done
12
13# 내부 Docker 데몬이 vfs가 아닌 네이티브 overlayfs를 사용하는지 확인합니다
14docker info --format 'Storage Driver: {{.Driver}}'
15# 출력: Storage Driver: overlayfs
16
17# 다중 레이어 빌드를 실행하고 중첩 컨테이너를 실행합니다
18mkdir -p /tmp/nested-test && cd /tmp/nested-test
19cat <<'EOF' > Dockerfile
20FROM alpine:3.20
21RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
22USER appuser
23WORKDIR /app
24RUN echo "nested-build-ok" > status.txt
25CMD ["cat", "/app/status.txt"]
26EOF
27
28docker build -t nested-test:latest .
29docker run --rm nested-test:latest
30# 출력: nested-build-ok
31
32kill -INT $DIND_PID
이 시리즈의 추가 읽을거리
- gvx: 하나의 권한 없는 프로세스에서 Linux 컨테이너, Docker, Kubernetes 실행하기: Windows(
whpx,systrap), macOS(hvf), Linux(systrap) 전반에 걸친gvx개요. - root 또는 사용자 네임스페이스 없이 Docker, docker build, docker compose 실행하기:
root,sudo,unshare(CLONE_NEWUSER),mount(2),/dev/fuse가 차단된 경우gvx가docker:dind를 실행하는 방법. - gvx를 이용한 gVisor 및 중첩 샌드박스 내부의 Docker-in-Docker 실행하기 (본 게시물)
- gvx를 활용한 AI 에이전트 RL 롤아웃용 빠르고 격리된 Docker 환경:
gvx와 단일 파일ext4lite코피-온-라이트(copy-on-write) 스냅샷을 사용하여 100밀리초 리셋으로 수백 개의 병렬SWE-bench및 RL 롤아웃 환경을 실행하는 방법.