¿Cómo aísla Docker realmente un proceso — qué es en verdad distinto de una VM?
A los contenedores se los llama "VMs livianas", pero comparten el kernel del host — el aislamiento real viene de los namespaces y cgroups de Linux, no de hardware virtualizado.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo aísla Docker realmente un proceso — qué es en verdad distinto de una VM?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#"Es como una VM liviana" es una mentira útil
A los contenedores de Docker se los presenta tan seguido como "máquinas virtuales livianas" que la mayoría de los ingenieros repite la frase sin que nadie los obligue nunca a defenderla en una entrevista. Es un buen modelo mental inicial y uno peligroso como modelo final, porque ambas tecnologías aíslan un proceso con mecanismos completamente distintos, con implicancias de seguridad y rendimiento también distintas. Entender la diferencia es la forma más rápida de sonar senior en una conversación de DevOps.
#Qué virtualiza realmente una VM
Una máquina virtual virtualiza hardware. Un hipervisor (Tipo 1 como ESXi/KVM, o Tipo 2 como VirtualBox) le presenta a cada VM CPUs virtuales, memoria virtual y dispositivos virtuales. Cada VM arranca luego su propio kernel independiente sobre ese hardware virtual — su propio kernel de Linux o Windows, sus propios drivers, su propia tabla de procesos, completamente ajena a que el "hardware" debajo es emulado. Dos VMs en el mismo host físico están tan aisladas entre sí como dos máquinas físicas separadas, porque ninguna comparte kernel con la otra.
#Qué aísla realmente un contenedor
Un contenedor no virtualiza nada a nivel de hardware. Todo contenedor en un host corre como un proceso común, planificado por el mismo kernel de Linux que cualquier otro contenedor y que el propio host. Lo que lo hace parecer una máquina aislada es una combinación de funciones del kernel que cambian qué puede ver y usar ese único proceso:
| Namespace | Qué aísla |
|---|---|
| PID | Su propio árbol de procesos — su primer proceso es el PID 1 dentro del namespace, aunque tenga un PID completamente distinto en el host |
| NET | Sus propias interfaces de red, direcciones IP, tabla de ruteo y espacio de puertos — por esto dos contenedores pueden escuchar ambos en el puerto 8080 sin chocar |
| MNT | Su propia vista de la tabla de montajes del sistema de archivos — esto es lo que hace que / dentro del contenedor parezca una raíz fresca de Alpine o Ubuntu en vez del sistema de archivos del host |
| UTS | Su propio hostname y dominio |
| IPC | Sus propios objetos IPC de System V y colas de mensajes POSIX, para que contenedores no relacionados no puedan comunicarse por memoria compartida |
| USER | Mapea los UID del lado del contenedor (incluyendo el UID 0, "root") a UIDs sin privilegios en el host, para que "ser root dentro del contenedor" no signifique ser root en el host |
Los namespaces responden "¿qué puede ver este proceso?". Un proceso dentro de un namespace de PID genuinamente no puede ver, señalizar ni hacer ps sobre los procesos de un contenedor vecino o del host — el kernel simplemente los filtra de la vista. Pero sigue siendo el mismo kernel, planificando los mismos hilos subyacentes en los mismos núcleos de CPU.
#cgroups: la otra mitad de la ecuación
Los namespaces controlan la visibilidad; los cgroups (control groups) controlan el consumo. Un cgroup le permite al kernel limitar y contabilizar cuánta CPU, memoria, I/O de disco, e incluso cuántos IDs de proceso puede usar un grupo de procesos. Esto es lo que hay detrás de docker run --memory=512m --cpus=1.5: el controlador cgroup v2 del kernel impone un techo duro de memoria (un OOM kill acotado a ese cgroup, no a todo el host) y una cuota de CPU (una fracción de un núcleo por período de planificación), y expone números de uso en vivo que docker stats lee directamente de /sys/fs/cgroup.
Juntando todo: los namespaces hacen que un proceso crea que está solo; los cgroups se aseguran de que no pueda acaparar la máquina aunque no lo esté. Docker también depende de un sistema de archivos por capas (OverlayFS) para apilar capas de imagen de solo lectura bajo una capa delgada de escritura por contenedor — por eso descargar una imagen es rápido (capas compartidas y cacheadas) y por eso las escrituras en disco de un contenedor desaparecen al eliminarlo, salvo que montes un volumen.
#Un Dockerfile real, y dónde aparece cada pieza de aislamiento
# Etapa 1: compilar el binario con el toolchain completo de GoFROM golang:1.22-alpine AS builderWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -o /out/api ./cmd/api# Etapa 2: enviar solo el binario en una imagen mínima de runtimeFROM alpine:3.19RUN apk add --no-cache ca-certificates \&& addgroup -S app && adduser -S app -G appWORKDIR /appCOPY --from=builder /out/api /app/apiUSER appEXPOSE 8080ENTRYPOINT ["/app/api"]
El build multi-etapa mantiene la imagen final pequeña descartando por completo el compilador de Go — la imagen de runtime nunca ve go build, solo el binario compilado. USER app importa más de lo que parece: sin esa línea, el proceso dentro del contenedor corre como UID 0. Gracias a los namespaces MNT y PID, ese UID 0 no puede ver el sistema de archivos ni los procesos del host, pero si alguna vez aparece un bug de escape de contenedor en el runtime, "root adentro" mapeado uno a uno con "root en el host" (sin remapeo por namespace de usuario) convierte un bug contenido en un compromiso del host. EXPOSE 8080 no abre ningún puerto por sí solo — es documentación que consume docker run -p; el binding real del puerto ocurre en el namespace NET propio del contenedor.
#VM vs. contenedor, lado a lado
| Máquina virtual | Contenedor | |
|---|---|---|
| Aísla | Hardware (vía hipervisor) | La vista de un único kernel (vía namespaces) + el consumo de recursos (vía cgroups) |
| Kernel | Uno propio por VM | Kernel del host compartido |
| Tiempo de arranque | Segundos a minutos (arranca un SO completo) | Milisegundos (arranca un proceso) |
| Tamaño de imagen | Gigabytes (SO completo) | Megabytes a pocas centenas de MB |
| Densidad por host | Decenas | Cientos a miles |
| Fuerza del aislamiento | Fuerte — kernels separados, frontera madura asistida por hardware | Más débil — una vulnerabilidad de kernel puede afectar a todos los contenedores del host |
🧠 Check your understanding
0/3 · 0/3 answered1. Un contenedor y el host donde corre muestran ambos IDs de proceso que empiezan desde un número bajo para su propio "primer" proceso. ¿Qué función del kernel lo hace posible?
2. Corrés un contenedor con `--memory=256m` y empieza a reservar mucho más que eso. ¿Qué pasa en realidad?
3. ¿Por qué se considera generalmente que un contenedor es una frontera de seguridad más débil que una VM?