---
id: 40e9d44f33fa847eb498f09bac5bf144
author: Lemon Mint
title: 루트 권한이나 사용자 네임스페이스 없이 Docker, docker build, docker compose 실행하기
description: root, sudo, unshare(CLONE_NEWUSER), mount(2), 그리고 /dev/fuse가 차단된 상황에서, 권한이 없는 사용자로서 격리된 gVisor 사용자 공간 샌드박스 내부에 완전한 Docker Engine(dockerd, BuildKit, overlayfs, docker compose)을 안전하게 실행하는 방법
language: ko
date: 2026-11-06T12:02:56.050897958Z
path: /blog/posts/run-docker-without-root-or-userns-gvx-z48916d02
go_package: z.gfire.dev/gvx
go_repourl: https://github.com/gfire-sigs/gvx.git
category: ops
---

## 빠른 해결책: 세 단계로 구현하는 보안 비특권 Docker

`root`, `sudo`, user namespaces(`unshare(CLONE_NEWUSER)`), `mount(2)`, `/dev/fuse`가 차단된 제한된 Linux 컨테이너, CI 러너, Kubernetes Pod, 또는 AI 에이전트 샌드박스 내부에서 작업하는 경우, **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`)은 사용자 공간에서 완전한 `docker:dind` 엔진을 실행하기 위한 **보안성이 높은 다중 방어(defense-in-depth) 해결책**입니다.

> **`gvx`가 설계상 안전한 이유 (권한 상승 우회 기법이 아님)**: 호스트의 `/var/run/docker.sock`을 마운트하거나 `docker run --privileged`를 실행하는 방식(호스트 커널에 root 수준의 제어권을 부여함)과 달리, `gvx`는 호스트의 어떠한 보안 정책(`PR_SET_NO_NEW_PRIVS`, `seccomp`, AppArmor, 또는 비root UID 제한)도 우회하거나 약화하지 않습니다. 대신 `rungv`는 자체 호스트 프로세스를 잠금 처리(`PR_SET_NO_NEW_PRIVS` + 엄격한 BPF `seccomp` 허용 목록)하고, Google의 메모리 안전성을 갖춘 **gVisor (`Sentry`)** 사용자 공간 커널 내부에서 `dockerd`를 실행합니다. 게스트의 모든 `UID 0` 권한(capabilities), `overlayfs` 마운트(`ext4lite`), 그리고 `iptables` 규칙은 샌드박스 프로세스 내부에 엄격하게 격리된 상태로 유지됩니다.

```sh
# 1. 사용자 공간에 rungv 설치 (SLSA 인증된 사전 빌드 릴리스 또는 go install)
# 옵션 A: GitHub Releases에서 SLSA 인증된 사전 빌드 바이너리를 검증하고 추출
gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx \
  --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml \
  --source-ref refs/tags/v0.2.0 --deny-self-hosted-runners
tar -xzf rungv-v0.2.0-linux-amd64.tar.gz && install -m 0755 rungv-v0.2.0-linux-amd64/rungv ~/.local/bin/rungv

# 옵션 B: Go를 사용하여 소스에서 직접 설치
CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest

# 2. 단일 파일 ext4lite 볼륨을 사용하여 gvx 내부에서 docker:dind 시작
#    ~/.gvx/docker.sock (모드 0600)을 게스트 포트 2375로 전달
mkdir -p ~/.gvx
~/.local/bin/rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -p 127.0.0.1:9000-9019:9000-9019 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &

# 3. 표준 Docker CLI와 Docker Compose가 사용자 공간 소켓을 가리키도록 설정
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 app:test .
docker run --rm app:test
docker compose up -d
```

---

## 제한된 샌드박스에서 표준 Rootless 도구가 실패하는 이유

엔지니어나 자율 코딩 에이전트가 강화된 컨테이너 또는 다중 테넌트 러너 내부에서 `docker build`, `docker run`, 또는 `docker compose`를 실행해야 할 때, 표준 우회 방법은 다섯 가지 개별 커널 장벽에서 실패합니다.

### 1. 호스트 Docker 데몬 소켓 부재

호스트의 `/var/run/docker.sock`이 환경에 마운트되지 않은 경우 `docker ps` 또는 `docker build`는 즉시 실패합니다:

```text
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
```

### 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`)를 통해 권한을 상승하려고 하면 실패합니다:

```text
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
sudo: 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 **user namespaces**(`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`에 쓸 수 없는 경우

발생하는 오류는 다음과 같습니다:

```text
unshare: unshare failed: Operation not permitted
cannot clone: Operation not permitted
Error: cannot re-exec process to join the existing user namespace
newuidmap: write to uid_map failed: Operation not permitted
RootlessKit: parent: failed to start the child: fork/exec /proc/self/exe: operation not permitted
```

### 4. 호스트 `CAP_SYS_ADMIN` 없이는 `mount(2)` 차단

환경이 `CLONE_NEWUSER`를 허용하더라도, 외부 컨테이너가 `/proc` 하위 트리(`/proc/acpi`, `/proc/kcore`, `/proc/keys`)를 마스킹하거나 seccomp에서 `mount(2)`를 제거한 경우, 중첩된 마운트 네임스페이스 내부에서 새로운 `/proc`을 마운트하는 작업이 실패합니다:

```text
mount("/proc", "/proc", "proc", MS_NOSUID|MS_NODEV|MS_NOEXEC, NULL): Operation not permitted
OCI 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
bwrap: Creating new namespace failed: Operation not permitted
```

### 5. `/dev/fuse` 누락으로 인한 `fuse-overlayfs` 실패

커널 `overlayfs`를 마운트할 수 없는 rootless 컨테이너 엔진은 `fuse-overlayfs`로 대체됩니다. `/dev/fuse` 장치가 전달되지 않은 최소 컨테이너 또는 Kubernetes Pod에서는 `fuse-overlayfs`가 즉시 실패합니다:

```text
fuse: device not found, try 'modprobe fuse' first
fusermount3: failed to open /dev/fuse: No such file or directory
error initializing graphdriver: driver not supported: fuse-overlayfs
```

---

## `gvx`가 `docker:dind`를 사용자 공간에서 완전히 실행하는 방식

`gvx`는 호스트 Linux 커널에 네임스페이스 생성, 파일시스템 마운트, 네트워크 인터페이스 구성을 요청하지 않으므로 다섯 가지 커널 장벽을 모두 우회합니다. 대신 `rungv`는 하나의 일반적인 비특권 프로세스 내부에서 내부 사용자 공간 Linux 커널(gVisor `Sentry`)과 사용자 공간 네트워크 스택(`netstack`)을 실행합니다.

![gvx를 사용한 비특권 Docker-in-Docker 실행](/assets/images/gvx/gvx-unprivileged-dind.png)

### 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`, `cgroupfs` v2)에서 동작합니다.

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

`gvx` 내부에서 `docker:dind`가 시작되면:

- 게스트 `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`는 세 가지 표준 비특권 커널 기본 기능에만 의존합니다:

1. **초기화 중 한 번, 자체 자식 스레드에 대한 `ptrace(PTRACE_ATTACH)`**: `systrap`은 시작 시 로컬 스텁 헬퍼 스레드를 생성하고, 공유 메모리 컨텍스트를 초기화하기 위해 이에 연결한 후 분리합니다. 호스트가 `kernel.yama.ptrace_scope = 3`(시스템 전체에서 모든 `ptrace`를 비활성화)을 설정하거나 외부 seccomp 필터에서 `ptrace`를 차단하면, `systrap` 초기화가 완료되지 않습니다. (`yama.ptrace_scope = 1`인 경우, 즉 Ubuntu와 Debian의 기본값에서는 부모에서 자식으로의 추적이 허용되므로 `gvx`가 정상적으로 동작합니다.)
2. **`seccomp(SECCOMP_SET_MODE_FILTER)`**: 컨테이너 스텁 스레드에서 게스트 시스템 호출을 가로채는 데 사용됩니다. [이 시리즈의 3부](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)에서 다룬 바와 같이, `gvx`는 `SECCOMP_FILTER_FLAG_NEW_LISTENER`를 사용할 수 있는 경우뿐만 아니라 중첩 샌드박스 내부에서 `EBUSY`를 반환하는 경우에도 동작합니다.
3. **`mmap(..., PROT_EXEC, ...)`**: 게스트 ELF 세그먼트와 `systrap` 공유 메모리 트램펄린을 매핑하는 데 사용됩니다.

---

## 종단 간 예제: 루트 권한 없이 다중 컨테이너 앱 빌드 및 테스트

다음 스크립트는 비특권 사용자 세션 내부에서 완전한 `docker build`와 두 개의 컨테이너(`redis` + 애플리케이션 컨테이너)를 사용한 통합 테스트를 실행합니다:

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

mkdir -p /tmp/demo-app && cd /tmp/demo-app
cat <<'EOF' > Dockerfile
FROM alpine:3.20
RUN apk add --no-cache redis
CMD ["redis-cli", "-h", "cache", "ping"]
EOF

# ext4lite 위의 overlayfs를 사용하여 이미지 빌드
docker build -t demo-ping:latest .

# 브리지 네트워크를 생성하고, Redis를 시작한 후 컨테이너 간 연결 확인
docker network create demo-net
docker run -d --name cache --network demo-net redis:7-alpine
docker run --rm --network demo-net demo-ping:latest
# 출력: PONG

# 워크로드 컨테이너를 정리하고 샌드박스 중지
docker rm -f cache
docker network rm demo-net
kill -INT $DIND_PID
```

---

## 이 시리즈의 추가 읽을거리

1. **[gvx: 하나의 비특권 프로세스에서 Linux 컨테이너, Docker, Kubernetes 실행](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: `gvx`의 아키텍처 개요, 교차 플랫폼 실행 엔진(`whpx`, `hvf`, `systrap`), `VFS2`(`erofs`, `ext4lite`, `hostfs` v2), 그리고 `netstack`에 대한 설명입니다.
2. **[root 또는 User Namespace 없이 Docker, docker build, docker compose 실행](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)** (이 게시물)
3. **[gVisor 및 중첩 샌드박스 내부에서 Docker-in-Docker 실행 (gvx 사용)](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: `gvx`가 `gVisor`(`runsc`) 내부에서 `modprobe`, `iptables-nft`, `gofer` 기반 `overlay2`, 그리고 중첩 `SECCOMP_FILTER_FLAG_NEW_LISTENER`(`EBUSY`) 문제를 해결하는 방법입니다.
4. **[gvx를 활용한 AI 에이전트 RL 롤아웃을 위한 빠른 격리 Docker 환경](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: 100밀리초 Copy-on-Write `.ext4` 볼륨 재설정을 통해 병렬 `SWE-bench` 및 RL 롤아웃 환경을 확장하는 방법입니다.
