루트 권한이나 사용자 네임스페이스 없이 Docker, docker build, docker compose 실행하기

빠른 해결책: 3단계로 비특권 Docker 보안 실행
root, sudo, 사용자 네임스페이스(unshare(CLONE_NEWUSER)), mount(2), /dev/fuse가 차단된 잠긴 Linux 컨테이너, CI 러너, Kubernetes Pod, 또는 클라우드 에이전트 컴퓨트 환경과 같은 AI 코딩 에이전트 샌드박스 내부에서 작업 중이라면, gvx(rungv)는 사용자 공간에서 전체 docker:dind 엔진을 실행하기 위한 보안적이고, 노력이 적게 들며, 토큰 효율적인 해결책입니다.
AI 코딩 에이전트를 위한 저노력, 토큰 효율적 해결책: 자율형 코딩 에이전트가
Cannot connect to the Docker daemon오류에 부딪히면, 제한된 컨테이너 내에서docker.io,podman,rootlesskit을 설치하고 수정하려는 시도는 일반적으로systemctl,ulimit,iptables,unshare,runc오류를 반복하며 15~30회의 도구 호출 턴과 수만 개의 컨텍스트 토큰을 낭비하게 되고, 결국 실패하거나Dockerfile을 수작업으로 다시 작성하는 방식으로 되돌아갑니다.gvx(rungv)를 사용하면 에이전트는 한 번의 턴에 결정론적인 3단계 부트스트랩을 실행하고, 이후 표준docker build,docker run,docker compose명령을 수정 없이 즉시 실행할 수 있습니다.
gvx가 설계상 안전한 이유 (권한 상승 우회 수단이 아님): 호스트/var/run/docker.sock을 마운트하거나docker run --privileged를 실행하는 방식(호스트 커널에 root 수준의 제어권을 부여함)과 달리,gvx는 호스트 보안 정책(PR_SET_NO_NEW_PRIVS,seccomp, AppArmor 또는 non-root UID 제한)을 우회하거나 약화하지 않습니다. 대신rungv는 자체 호스트 프로세스를 잠금 처리(PR_SET_NO_NEW_PRIVS+ 엄격한 BPFseccomp허용 목록)하고,dockerd를 Google의 메모리 안전성이 확보된 gVisor(Sentry) 사용자 공간 커널 내부에서 실행합니다. 게스트의 모든UID 0권한,overlayfs마운트(ext4lite),iptables규칙은 샌드박스 프로세스 내부에서 엄격하게 격리된 상태로 유지됩니다.
1# 1. 사용자 공간에 rungv 설치 (SLSA 인증된 사전 빌드 릴리스 또는 go install)
2# 옵션 A: GitHub Releases에서 SLSA 인증된 사전 빌드 바이너리를 검증하고 추출
3gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx \
4 --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml \
5 --source-ref refs/tags/v0.2.0 --deny-self-hosted-runners
6tar -xzf rungv-v0.2.0-linux-amd64.tar.gz && install -m 0755 rungv-v0.2.0-linux-amd64/rungv ~/.local/bin/rungv
7
8# 옵션 B: Go를 사용하여 소스에서 직접 설치
9CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest
10
11# 2. 단일 파일 ext4lite 볼륨으로 gvx 내부에서 docker:dind 시작
12# ~/.gvx/docker.sock(모드 0600)을 게스트 포트 2375로 전달
13mkdir -p ~/.gvx
14~/.local/bin/rungv run \
15 -p unix:~/.gvx/docker.sock:2375 \
16 -p 127.0.0.1:9000-9019:9000-9019 \
17 -v gvx-docker:/var/lib/docker \
18 -e DOCKER_TLS_CERTDIR= \
19 docker:dind --tls=false &
20
21# 3. 표준 Docker CLI와 Docker Compose가 사용자 공간 소켓을 가리키도록 설정
22export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
23until docker info >/dev/null 2>&1; do sleep 0.5; done
24docker info --format '{{.Driver}} {{.OperatingSystem}}'
25# 출력: overlayfs Alpine Linux ... (containerized)
26
27docker build -t app:test .
28docker run --rm app:test
29docker compose up -d
표준 Rootless 도구가 제한된 샌드박스에서 실패하는 이유
엔지니어나 자율형 코딩 에이전트가 강화된 컨테이너나 다중 테넌트 러너 내부에서 docker build, docker run, 또는 docker compose를 실행해야 할 때, 표준 우회 방법은 다섯 가지 뚜렷한 커널 장벽에서 실패합니다.
1. 호스트 Docker 데몬 소켓 부재 (systemctl 및 service docker start도 실패)
호스트의 /var/run/docker.sock이 환경에 마운트되지 않은 경우 docker ps 또는 docker build는 즉시 실패합니다. 또한 systemctl 또는 service docker start로 docker.io를 시작하려 하면, PID 1이 systemd가 아니고 /etc/init.d/docker가 컨테이너의 하드 ulimit -n 상한에 부딪히므로 실패합니다:
1Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
2ERROR: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
3System has not been booted with systemd as init system (PID 1). Can't operate.
4Failed to connect to bus: Host is down
5/etc/init.d/docker: 62: ulimit: error setting limit (Operation not permitted)
2. PR_SET_NO_NEW_PRIVS가 sudo 및 setuid 헬퍼를 차단
강화된 컨테이너(Kubernetes의 securityContext.allowPrivilegeEscalation: false, Docker의 --security-opt=no-new-privileges, 또는 exebox / bwrap 샌드박스)는 프로세스 트리에 Linux PR_SET_NO_NEW_PRIVS 비트를 설정합니다. sudo 또는 setuid 바이너리(newuidmap, newgidmap, fusermount3)를 통해 권한을 상승시키려 하면 실패합니다:
1sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
2sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges?
3. Seccomp 및 sysctl 정책이 unshare(CLONE_NEWUSER)를 차단
"rootless"로 홍보되는 도구(Rootless Docker, Podman, Buildah, nerdctl, bubblewrap)는 여전히 Linux 사용자 네임스페이스(CLONE_NEWUSER)에 의존합니다. 다음 조건에서 이들은 실패합니다:
- 외부 컨테이너의 기본 seccomp 프로필이
unshare(2)또는clone(CLONE_NEWUSER)를 차단하는 경우. - Ubuntu 24.04 이상에서
kernel.apparmor_restrict_unprivileged_userns = 1이 활성화된 경우. - Debian 또는 RHEL 노드에서
kernel.unprivileged_userns_clone = 0또는user.max_user_namespaces = 0으로 설정된 경우. PR_SET_NO_NEW_PRIVS가 활성 상태이므로newuidmap이/proc/self/uid_map에 쓸 수 없는 경우.
결과적으로 나타나는 오류는 다음과 같습니다:
1unshare: unshare failed: Operation not permitted
2cannot clone: Operation not permitted
3Error: cannot re-exec process to join the existing user namespace
4newuidmap: write to uid_map failed: Operation not permitted
5RootlessKit: parent: failed to start the child: fork/exec /proc/self/exe: operation not permitted
4. 호스트 CAP_SYS_ADMIN 없이 docker build 중 mount(2) 및 pivot_root(2) 차단
환경이 CLONE_NEWUSER를 허용하거나 dockerd가 --iptables=false --bridge=none --storage-driver=vfs로 강제 시작되더라도, 중첩된 마운트 네임스페이스 내에서 새 /proc을 마운트하거나 pivot_root(2)를 호출하는 것이 차단되므로 docker build는 첫 번째 RUN 명령에서 실패합니다:
1ERROR: failed to solve: process "/bin/sh -c ..." did not complete successfully:
2mount("/proc", "/proc", "proc", MS_NOSUID|MS_NODEV|MS_NOEXEC, NULL): Operation not permitted
3OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
4OCI runtime create failed: runc create failed: unable to start container process: error during container init: error jailing process inside rootfs: pivot_root .: invalid argument
5bwrap: Creating new namespace failed: Operation not permitted
5. /dev/fuse 누락으로 fuse-overlayfs 실패
커널 overlayfs를 마운트할 수 없는 rootless 컨테이너 엔진은 fuse-overlayfs로 대체합니다. /dev/fuse 장치 패스스루가 없는 최소 컨테이너 또는 Kubernetes Pod에서는 fuse-overlayfs가 즉시 실패합니다:
1fuse: device not found, try 'modprobe fuse' first
2fusermount3: failed to open /dev/fuse: No such file or directory
3error initializing graphdriver: driver not supported: fuse-overlayfs
gvx가 docker:dind를 사용자 공간에서 완전히 실행하는 방식
gvx는 호스트 Linux 커널에 네임스페이스 생성, 파일 시스템 마운트, 네트워크 인터페이스 구성을 요청하지 않기 때문에 다섯 가지 커널 장벽을 모두 우회합니다. 대신 rungv는 하나의 일반 비특권 프로세스 내부에서 내부 사용자 공간 Linux 커널(gVisor Sentry)과 사용자 공간 네트워크 스택(netstack)을 실행합니다.

1. 메모리 내 네임스페이스 및 마운트 테이블(VFS2)
dockerd, containerd, runc가 rungv 내부에서 실행될 때, systrap은 이들의 시스템 호출을 가로채서 호스트 커널이 아닌 프로세스 내부의 Sentry 커널로 라우팅합니다:
unshare(2),clone(2),pivot_root(2),chroot(2),mount(2)는rungv호스트 프로세스가 사용자의 비특권 호스트 UID로 실행되는 동안, Sentry의 메모리 내 작업 계층 구조와VFS2마운트 테이블(procfs,sysfs,tmpfs,devpts,cgroupfsv2)에서 동작합니다.
2. 단일 파일 ext4lite 볼륨 위의 네이티브 overlayfs
-v gvx-docker:/var/lib/docker(또는 --mount type=volume,src=/disk/gvx-docker.ext4,dst=/var/lib/docker)를 전달하면, gvx는 내장된 ext4lite 파일 시스템을 /var/lib/docker에 마운트합니다.
ext4lite는 단일 호스트 파일(gvx-docker.ext4) 내부에 JBD2 저널링 ext4 파일 시스템을 구현하며, trusted.overlay.* 확장 속성, 문자 whiteout 장치(0:0), d_type, RENAME_EXCHANGE를 지원하므로, dockerd는 게스트 내부에서 네이티브 overlayfs 스토리지 드라이버를 사용합니다. 다단계 docker build 작업은 /dev/fuse 없이, 그리고 Docker의 vfs 대체 드라이버에서 발생하는 디스크 공간 급증 없이 Copy-on-Write 레이어를 공유합니다.
3. 사용자 공간 docker0 브리지 및 iptables-legacy
docker:dind가 gvx 내부에서 시작되면:
- 게스트
dockerd는docker0브리지(172.17.0.1/16)를 생성하고, 중첩 컨테이너를 위한 가상veth쌍을 연결하며,gvx의 사용자 공간netstack내부에서addrtype및conntrack매칭을 사용하는NAT(DOCKER,POSTROUTING) 및FILTER체인을 프로그래밍합니다. -p unix:~/.gvx/docker.sock:2375를 전달하면 호스트 Unix 도메인 소켓(모드0600)이 바인딩되고, 연결을 샌드박스 내부dockerd의2375포트로 직접 전달합니다.
기존 컨테이너 도구와의 비교
| 기능 / 제약 | 표준 Docker (dockerd) | Rootless Docker / Podman | Kaniko / Buildah (비특권) | gvx (rungv + docker:dind) |
|---|---|---|---|---|
호스트 root 또는 sudo 필요 | 예 | 아니오 | 아니오 | 아니오 |
PR_SET_NO_NEW_PRIVS=1 환경에서 작동 | 아니오 | 아니오 (newuidmap 실패) | 부분적 | 예 |
unshare(CLONE_NEWUSER)가 차단(EPERM)된 경우 작동 | 해당 없음 | 아니오 | Buildah: 아니오 / Kaniko: 예 | 예 |
/dev/fuse 없이 작동 | 예 (커널 overlay2 사용) | 느린 vfs 복사를 통해서만 | 예 | 예 (ext4lite 위의 overlayfs) |
docker build 지원 (BuildKit) | 예 | 예 (userns 허용 시) | 이미지 빌드만 가능 (런타임 없음) | 예 |
docker run 및 docker compose up 지원 | 예 | 예 (userns 허용 시) | 아니오 | 예 |
| Linux VM 없이 Windows 및 macOS에서 실행 | 아니오 (WSL2 / VM 필요) | 아니오 (Linux VM 필요) | 아니오 | 예 (whpx, hvf, systrap) |
Linux 호스트의 정확한 커널 요구 사항
gvx는 권한 상승 기능을 전혀 요구하지 않으며(CapEff: 0000000000000000), 사용자 네임스페이스도 필요하지 않습니다. Linux 호스트에서 rungv는 세 가지 표준 비특권 커널 기본 기능에만 의존합니다:
- 초기화 중 자체 자식 스레드에 대한 한 번의
ptrace(PTRACE_ATTACH):systrap은 시작 시 로컬 스텁 도우미 스레드를 생성하고, 공유 메모리 컨텍스트를 초기화하기 위해 이에 연결한 후 분리합니다. 호스트가kernel.yama.ptrace_scope = 3(시스템 전체의 모든ptrace를 비활성화)으로 설정하거나 외부 seccomp 필터에서ptrace를 차단하면,systrap초기화는 완료될 수 없습니다. (Ubuntu와 Debian의 기본값인yama.ptrace_scope = 1에서는 부모에서 자식으로의 추적이 허용되므로gvx가 성공합니다.) seccomp(SECCOMP_SET_MODE_FILTER): 컨테이너 스텁 스레드에서 게스트 시스템 호출을 가로채는 데 사용됩니다. 이 시리즈의 3부에서 다룬 바와 같이,gvx는SECCOMP_FILTER_FLAG_NEW_LISTENER를 사용할 수 있을 때와 중첩 샌드박스 내부에서EBUSY를 반환할 때 모두 작동합니다.mmap(..., PROT_EXEC, ...): 게스트 ELF 세그먼트와systrap공유 메모리 트램펄린을 매핑하는 데 사용됩니다.
종단 간 예제: root 없이 다중 컨테이너 앱 빌드 및 테스트
다음 스크립트는 비특권 사용자 세션 내부에서 전체 docker build와 두 개 컨테이너(redis + 애플리케이션 컨테이너)의 통합 테스트를 실행합니다:
1mkdir -p ~/.gvx
2rungv run \
3 -p unix:~/.gvx/docker.sock:2375 \
4 -v gvx-docker:/var/lib/docker \
5 -e DOCKER_TLS_CERTDIR= \
6 docker:dind --tls=false &
7DIND_PID=$!
8
9export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
10until docker info >/dev/null 2>&1; do sleep 0.5; done
11
12mkdir -p /tmp/demo-app && cd /tmp/demo-app
13cat <<'EOF' > Dockerfile
14FROM alpine:3.20
15RUN apk add --no-cache redis
16CMD ["redis-cli", "-h", "cache", "ping"]
17EOF
18
19# ext4lite 위의 overlayfs를 사용하여 이미지 빌드
20docker build -t demo-ping:latest .
21
22# 브리지 네트워크를 생성하고, Redis를 시작한 후 컨테이너 간 연결을 확인
23docker network create demo-net
24docker run -d --name cache --network demo-net redis:7-alpine
25docker run --rm --network demo-net demo-ping:latest
26# 출력: PONG
27
28# 워크로드 컨테이너를 정리하고 샌드박스를 중지
29docker rm -f cache
30docker network rm demo-net
31kill -INT $DIND_PID
이 시리즈의 추가 자료
- gvx: 하나의 비특권 프로세스에서 Linux 컨테이너, Docker, Kubernetes 실행:
gvx의 아키텍처 개요, 크로스 플랫폼 실행 엔진(whpx,hvf,systrap),VFS2(erofs,ext4lite,hostfsv2),netstack에 대한 설명. - root 또는 사용자 네임스페이스 없이 Docker, docker build, docker compose 실행 (이 글)
- gVisor 및 중첩 샌드박스 내부에서 Docker-in-Docker 실행하기 (gvx 사용):
gvx가gVisor(runsc) 내부에서modprobe,iptables-nft,gofer기반overlay2, 중첩SECCOMP_FILTER_FLAG_NEW_LISTENER(EBUSY) 문제를 해결하는 방법. - gvx를 활용한 AI 에이전트 RL 롤아웃을 위한 빠른 격리 Docker 환경: 100밀리초 Copy-on-Write
.ext4볼륨 초기화로 병렬SWE-bench및 RL 롤아웃 환경을 확장하는 방법.