---
id: 40e9d44f33fa847eb498f09bac5bf144
author: Lemon Mint
title: Ejecución de Docker, docker build y docker compose sin privilegios de root ni espacios de nombres de usuario
description: Cómo ejecutar de forma segura un Docker Engine completo (dockerd, BuildKit, overlayfs y docker compose) dentro de un sandbox aislado de espacio de usuario de gVisor, como usuario sin privilegios, cuando root, sudo, unshare(CLONE_NEWUSER), mount(2) y /dev/fuse están bloqueados.
language: es
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
---

## Solución rápida: Docker sin privilegios de forma segura en tres pasos

Si trabajas dentro de un contenedor Linux restringido, un runner de CI, un Pod de Kubernetes o un sandbox de agentes de IA donde `root`, `sudo`, los user namespaces (`unshare(CLONE_NEWUSER)`), `mount(2)` y `/dev/fuse` están bloqueados, **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) es la **solución segura y con defensa en profundidad** para ejecutar un motor `docker:dind` completo en espacio de usuario.

> **Por qué `gvx` es seguro por diseño (no es una solución alternativa para la escalada de privilegios)**: A diferencia de montar el `/var/run/docker.sock` del host o de ejecutar `docker run --privileged` (que otorgan un control equivalente a root sobre el kernel del host), `gvx` no omite ni debilita ninguna política de seguridad del host (`PR_SET_NO_NEW_PRIVS`, `seccomp`, AppArmor, ni los límites de UID no root). En su lugar, `rungv` bloquea su propio proceso del host (`PR_SET_NO_NEW_PRIVS` más una lista blanca estricta de BPF `seccomp`) y ejecuta `dockerd` dentro del kernel de espacio de usuario **gVisor (`Sentry`)** de Google, con seguridad de memoria. Todas las capacidades de `UID 0` del invitado, los montajes `overlayfs` (`ext4lite`) y las reglas de `iptables` permanecen estrictamente aisladas dentro del proceso del sandbox.

```sh
# 1. Instalar rungv en espacio de usuario (binario precompilado con atestación SLSA o go install)
# Opción A: Verificar y extraer el binario precompilado con atestación SLSA desde GitHub Releases
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

# Opción B: O instalar directamente desde el código fuente con Go
CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest

# 2. Iniciar docker:dind dentro de gvx con un volumen ext4lite de un solo archivo
#    Reenvía ~/.gvx/docker.sock (modo 0600) al puerto 2375 del invitado
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. Apuntar la CLI estándar de Docker y Docker Compose al socket de espacio de usuario
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}}'
# Salida: overlayfs Alpine Linux ... (en contenedor)

docker build -t app:test .
docker run --rm app:test
docker compose up -d
```

---

## Por qué fallan las herramientas estándar sin root en sandboxes restringidos

Cuando un ingeniero o un agente autónomo de programación necesita ejecutar `docker build`, `docker run` o `docker compose` dentro de un contenedor endurecido o de un runner multiinquilino, los workarounds estándar fallan en cinco barreras distintas del kernel.

### 1. No hay socket del daemon Docker del host

Ejecutar `docker ps` o `docker build` falla de inmediato cuando el `/var/run/docker.sock` del host no está montado en el entorno:

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

### 2. `PR_SET_NO_NEW_PRIVS` bloquea `sudo` y los helpers setuid

Los contenedores endurecidos (`securityContext.allowPrivilegeEscalation: false` en Kubernetes, `--security-opt=no-new-privileges` en Docker, o los sandboxes `exebox` / `bwrap`) activan el bit `PR_SET_NO_NEW_PRIVS` de Linux en el árbol de procesos. Intentar escalar privilegios mediante `sudo` o binarios setuid (`newuidmap`, `newgidmap`, `fusermount3`) falla:

```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. Las políticas de seccomp y sysctl bloquean `unshare(CLONE_NEWUSER)`

Las herramientas comercializadas como "sin root" (`Rootless Docker`, `Podman`, `Buildah`, `nerdctl`, `bubblewrap`) siguen dependiendo de los **user namespaces** de Linux (`CLONE_NEWUSER`). Fallan siempre que:

- El perfil seccomp predeterminado del contenedor externo bloquea `unshare(2)` o `clone(CLONE_NEWUSER)`.
- Ubuntu 24.04 o posterior activa `kernel.apparmor_restrict_unprivileged_userns = 1`.
- Los nodos Debian o RHEL establecen `kernel.unprivileged_userns_clone = 0` o `user.max_user_namespaces = 0`.
- `newuidmap` no puede escribir en `/proc/self/uid_map` porque `PR_SET_NO_NEW_PRIVS` está activo.

Los errores resultantes tienen este aspecto:

```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. `mount(2)` está bloqueado sin `CAP_SYS_ADMIN` del host

Incluso si un entorno permite `CLONE_NEWUSER`, montar un `/proc` nuevo dentro de un mount namespace anidado falla si el contenedor externo ha enmascarado subárboles de `/proc` (`/proc/acpi`, `/proc/kcore`, `/proc/keys`) o ha eliminado `mount(2)` de seccomp:

```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. La falta de `/dev/fuse` rompe `fuse-overlayfs`

Los motores de contenedores sin privilegios que no pueden montar el `overlayfs` del kernel recurren a `fuse-overlayfs`. En contenedores mínimos o en Pods de Kubernetes sin paso del dispositivo `/dev/fuse`, `fuse-overlayfs` falla de inmediato:

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

---

## Cómo `gvx` ejecuta `docker:dind` completamente en espacio de usuario

`gvx` evita las cinco barreras del kernel porque no pide al kernel Linux del host que cree namespaces, monte sistemas de archivos ni configure interfaces de red. En su lugar, `rungv` ejecuta un kernel Linux interno en espacio de usuario (el `Sentry` de gVisor) y una pila de red en espacio de usuario (`netstack`) dentro de un único proceso ordinario sin privilegios.

![Ejecución de Docker-in-Docker sin privilegios con gvx](/assets/images/gvx/gvx-unprivileged-dind.png)

### 1. Namespaces en memoria y tabla de montajes (`VFS2`)

Cuando `dockerd`, `containerd` y `runc` se ejecutan dentro de `rungv`, `systrap` intercepta sus llamadas al sistema y las dirige al kernel `Sentry` en proceso, en lugar de al kernel del host:

- `unshare(2)`, `clone(2)`, `pivot_root(2)`, `chroot(2)` y `mount(2)` operan sobre la jerarquía de tareas en memoria del Sentry y sobre la tabla de montajes `VFS2` (`procfs`, `sysfs`, `tmpfs`, `devpts`, `cgroupfs` v2), mientras el proceso del host `rungv` se ejecuta bajo tu UID del host sin privilegios.

### 2. `overlayfs` nativo sobre volúmenes `ext4lite` de un solo archivo

Cuando pasas `-v gvx-docker:/var/lib/docker` (o `--mount type=volume,src=/disk/gvx-docker.ext4,dst=/var/lib/docker`), `gvx` monta su sistema de archivos integrado **`ext4lite`** en `/var/lib/docker`.

Dado que `ext4lite` implementa un sistema de archivos `ext4` con journal JBD2 dentro de un único archivo del host (`gvx-docker.ext4`), con soporte para atributos extendidos `trusted.overlay.*`, dispositivos whiteout de caracteres (`0:0`), `d_type` y `RENAME_EXCHANGE`, `dockerd` utiliza su controlador de almacenamiento nativo **`overlayfs`** dentro del invitado. Las operaciones `docker build` de múltiples etapas comparten capas Copy-on-Write sin `/dev/fuse` y sin la explosión de espacio en disco del controlador de respaldo `vfs` de Docker.

### 3. Bridge `docker0` en espacio de usuario e `iptables-legacy`

Cuando `docker:dind` se inicia dentro de `gvx`:

- El `dockerd` del invitado crea el bridge `docker0` (`172.17.0.1/16`), conecta pares `veth` virtuales para los contenedores anidados y programa las cadenas `NAT` (`DOCKER`, `POSTROUTING`) y `FILTER` con coincidencias `addrtype` y `conntrack` dentro del `netstack` de espacio de usuario de `gvx`.
- Pasar `-p unix:~/.gvx/docker.sock:2375` vincula un socket de dominio Unix del host (modo `0600`) y reenvía las conexiones directamente al puerto `2375` de `dockerd` dentro del sandbox.

---

## Comparación con las herramientas de contenedores existentes

| Capacidad / Restricción                                      | Docker estándar (`dockerd`)  | Docker sin root / Podman  | Kaniko / Buildah (sin privilegios) | `gvx` (`rungv` + `docker:dind`)     |
| :----------------------------------------------------------- | :--------------------------- | :------------------------ | :--------------------------------- | :---------------------------------- |
| **Requiere `root` o `sudo` en el host**                      | Sí                           | No                        | No                                 | **No**                              |
| **Funciona con `PR_SET_NO_NEW_PRIVS=1`**                     | No                           | No (`newuidmap` falla)    | Parcial                            | **Sí**                              |
| **Funciona cuando `unshare(CLONE_NEWUSER)` está bloqueado (`EPERM`)** | N/A                 | No                        | Buildah: No / Kaniko: Sí           | **Sí**                              |
| **Funciona sin `/dev/fuse`**                                 | Sí (usa `overlay2` del kernel) | Solo mediante copia `vfs` lenta | Sí                           | **Sí (`overlayfs` sobre `ext4lite`)** |
| **Admite `docker build` (BuildKit)**                         | Sí                           | Sí (si `userns` está permitido) | Solo construcción de imágenes (sin runtime) | **Sí**                    |
| **Admite `docker run` y `docker compose up`**                | Sí                           | Sí (si `userns` está permitido) | **No**                          | **Sí**                              |
| **Funciona en Windows y macOS sin una VM de Linux**          | No (requiere WSL2 / VM)      | No (requiere VM de Linux) | No                                 | **Sí (`whpx`, `hvf`, `systrap`)**   |

---

## Requisitos exactos del kernel del host en Linux

`gvx` no requiere capacidades elevadas (`CapEff: 0000000000000000`) ni user namespaces. En un host Linux, `rungv` depende únicamente de tres primitivas estándar del kernel sin privilegios:

1. **`ptrace(PTRACE_ATTACH)` sobre su propio hilo hijo, una sola vez durante la inicialización**: `systrap` crea un hilo auxiliar stub local al iniciarse, se adjunta a él para inicializar el contexto de memoria compartida y luego se desadjunta. Si el host establece `kernel.yama.ptrace_scope = 3` (lo que deshabilita todo `ptrace` en el sistema) o bloquea `ptrace` en un filtro seccomp externo, la inicialización de `systrap` no puede completarse. (Con `yama.ptrace_scope = 1`, el valor predeterminado en Ubuntu y Debian, `gvx` funciona porque se permite el trazado de padre a hijo).
2. **`seccomp(SECCOMP_SET_MODE_FILTER)`**: se usa para interceptar las llamadas al sistema del invitado desde los hilos stub del contenedor. Como se explica en [la parte 3 de esta serie](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03), `gvx` funciona tanto cuando `SECCOMP_FILTER_FLAG_NEW_LISTENER` está disponible como cuando devuelve `EBUSY` dentro de un sandbox anidado.
3. **`mmap(..., PROT_EXEC, ...)`**: se usa para mapear los segmentos ELF del invitado y el trampolín de memoria compartida de `systrap`.

---

## Ejemplo de extremo a extremo: construir y probar una aplicación multicontenedor sin root

El siguiente script ejecuta un `docker build` completo y una prueba de integración de dos contenedores (`redis` más el contenedor de la aplicación) dentro de una sesión de usuario sin privilegios:

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

# Construir la imagen usando overlayfs sobre ext4lite
docker build -t demo-ping:latest .

# Crear una red bridge, iniciar Redis y verificar la conectividad entre contenedores
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
# Salida: PONG

# Limpiar los contenedores de carga de trabajo y detener el sandbox
docker rm -f cache
docker network rm demo-net
kill -INT $DIND_PID
```

---

## Más lectura en esta serie

1. **[gvx: Ejecutar contenedores Linux, Docker y Kubernetes en un único proceso sin privilegios](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Visión general de la arquitectura de `gvx`, sus motores de ejecución multiplataforma (`whpx`, `hvf`, `systrap`), `VFS2` (`erofs`, `ext4lite`, `hostfs` v2) y `netstack`.
2. **[Ejecutar Docker, docker build y docker compose sin root ni user namespaces](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)** (esta entrada)
3. **[Ejecutar Docker-in-Docker dentro de gVisor y sandboxes anidados con gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: Cómo `gvx` resuelve `modprobe`, `iptables-nft`, `overlay2` sobre `gofer` y `SECCOMP_FILTER_FLAG_NEW_LISTENER` anidado (`EBUSY`) dentro de `gVisor` (`runsc`).
4. **[Entornos Docker aislados y rápidos para rollouts de RL de agentes de IA con gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Escalado de entornos paralelos de `SWE-bench` y de rollouts de RL mediante reinicios de volúmenes `.ext4` con copy-on-write en 100 milisegundos.
