Contenido

Tutorial de fundamentos internos de Docker: experimentos con namespace, cgroup y overlayfs

Qué es Docker explica los contenedores con una analogía; en esta sección iremos directamente al host Docker para ver qué es realmente un contenedor en Linux. La respuesta cabe en una frase:

Un contenedor es un proceso Linux normal: los namespaces limitan «qué puede ver», los cgroups limitan «cuánto puede usar», overlayfs combina directorios para formar su directorio raíz y capabilities y seccomp limitan «qué puede hacer».

Un proceso Linux rodeado por namespaces, cgroups, overlayfs y mecanismos de seguridad
Contenedor = un proceso + namespaces + cgroups + overlayfs + restricciones de seguridad

En este tutorial, «host» se refiere al entorno Docker de prueba (kernel Linux 5.4, cgroup v1). Al ejecutarlo en tu ordenador, los ID, las rutas y otros valores serán distintos; en los sistemas que usan cgroup v2 también cambian los nombres de los archivos, como se indica en el texto. Si usas Docker Desktop, puedes entrar en la máquina virtual Linux de Docker Desktop para observarla con docker run -it --rm --privileged --pid=host alpine:3.23 nsenter -t 1 -m -u -i -n sh.

Un contenedor es un proceso del host

Inicia un contenedor nginx y limita la memoria a 256 MB y la CPU a 0.5 núcleos:

$ docker run -d --name demo --memory 256m --cpus 0.5 nginx:1.30-alpine
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
57dcd17505d4e7c71e57df55edef3e137c18249e6ad402c7e7b3f215341c47b4

$ docker exec demo ps
PID   USER     TIME  COMMAND
    1 root      0:00 nginx: master process nginx -g daemon off;
   29 nginx     0:00 nginx: worker process
   30 root      0:00 ps

$ docker inspect -f "{{.State.Pid}}" demo
12458
  • Dentro del contenedor, el PID del proceso principal de nginx es 1, y solo se ven sus propios procesos.
  • .State.Pid de docker inspect es el PID que tiene en el host: 12458.

Lista los procesos en el host:

$ ps -o pid,ppid,args | grep -v -E 'ps -o|grep|cut' | cut -c1-110
PID   PPID  COMMAND
    1     0 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --registry-mir
   19     1 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --registry-mirror=https://mir
12434     1 /usr/local/bin/containerd-shim-runc-v2 -namespace moby -id 57dcd17505d4e7c71e57df55edef3e137c18249
12458 12434 nginx: master process nginx -g daemon off;
12495 12458 nginx: worker process

En el host se ve todo el árbol de procesos: dockerd → containerd-shim-runc-v2 → nginx: master process (12458) → nginx: worker process.

  • containerd-shim-runc-v2: hay uno por contenedor y se encarga de supervisar sus procesos. Si dockerd se reinicia, el contenedor no se detiene; eso se debe a este proceso.
  • runc: es el programa que crea realmente el contenedor. Configura los namespaces, el cgroup y el directorio raíz; después ejecuta nginx y termina, por eso no aparece en el árbol de procesos.

El mismo proceso tiene PID 1 dentro del contenedor y PID 12458 en el host. Ese es el efecto del namespace PID.

namespace: decide qué puede ver un contenedor

lsns muestra el namespace al que pertenece un proceso:

$ lsns -p 12458 -o NS,TYPE,PID,COMMAND
        NS TYPE     PID COMMAND
4026531835 cgroup     1 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --registry-mirror=https://mirror.gcr.io --dns=1.1.1.1 --dns=8.8.8.8
4026531837 user       1 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --registry-mirror=https://mirror.gcr.io --dns=1.1.1.1 --dns=8.8.8.8
4026533161 mnt    12458 nginx: master process nginx -g daemon off;
4026533162 uts    12458 nginx: master process nginx -g daemon off;
4026533163 ipc    12458 nginx: master process nginx -g daemon off;
4026533164 pid    12458 nginx: master process nginx -g daemon off;
4026533166 net    12458 nginx: master process nginx -g daemon off;

El proceso nginx tiene 5 namespaces propios (con ID distintos de los del host), mientras que comparte otros 2 (cgroup y user) con el PID 1 del host:

namespaceElemento aisladoEfecto en el contenedor
pididentificadores de procesoDentro del contenedor solo se ven sus propios procesos; el proceso principal tiene el PID 1
netinterfaces de red, IP, puertos y rutasEl contenedor tiene su propio eth0 e IP, y puede escuchar en el puerto 80 de forma independiente
mntpuntos de montajeEl contenedor tiene su propio directorio raíz /
utsnombre del hostEl hostname del contenedor es su ID
ipcmemoria compartida y semáforosLos contenedores no pueden interferir entre sí mediante IPC
usercorrespondencia de ID de usuarioDe forma predeterminada, no está aislado: el usuario root del contenedor es el root del host. Solo se usa en el modo Rootless
cgroupvista del directorio cgroupEste entorno (cgroup v1) no está aislado; los sistemas con cgroup v2 lo aíslan de forma predeterminada

Efectos de los namespaces uts y net:

$ docker exec demo hostname
57dcd17505d4

$ docker exec demo ip -4 addr show eth0
2: eth0@if130: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
       valid_lft forever preferred_lft forever

El nombre del host del contenedor son los primeros 12 caracteres de su ID. eth0 tiene su propia IP (172.18.0.2), asignada por la red de Docker. Consulta Redes y comunicación entre contenedores.

cgroup: decide cuántos recursos puede usar un contenedor

--memory 256m --cpus 0.5 no lo calcula Docker; se escribe en la configuración de cgroup del kernel de Linux. Lee esta configuración desde el contenedor:

$ docker info --format "Cgroup Version: {{.CgroupVersion}}"
Cgroup Version: 1

$ docker exec demo cat /sys/fs/cgroup/memory/memory.limit_in_bytes
268435456

$ docker exec demo cat /sys/fs/cgroup/memory/memory.usage_in_bytes
10842112

$ docker exec demo cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us
50000
100000

$ docker stats --no-stream --format "{{.Name}}  {{.MemUsage}}  {{.CPUPerc}}" demo
demo  5.613MiB / 256MiB  0.00%
Valor leídoSignificado
memory.limit_in_bytes = 268435456268435456 bytes = 256 MiB, es decir, --memory 256m
memory.usage_in_bytes = 10842112Se están usando unos 10 MB (estadística del kernel, incluye la caché de archivos, por eso es mayor que lo que muestra docker stats)
cpu.cfs_quota_us = 50000, cpu.cfs_period_us = 100000Se puede usar un máximo de 50 ms de CPU cada 100 ms, es decir, 0,5 núcleos

El límite de 256MiB que muestra docker stats también se lee de cgroup. Si la memoria usada por un proceso supera el límite, el kernel lo termina (OOM) y el código de salida es 137.

En los sistemas con cgroup v2 (versiones recientes de Ubuntu, Debian, Fedora y Docker Desktop), los nombres de archivo son distintos: el límite de memoria está en /sys/fs/cgroup/memory.max y la cuota de CPU en /sys/fs/cgroup/cpu.max (el contenido es 50000 100000).

overlayfs: el sistema de archivos del contenedor

Capas de la imagen

Una imagen está compuesta por varias capas de solo lectura:

$ docker image inspect -f "{{range .RootFS.Layers}}{{println .}}{{end}}" nginx:1.30-alpine
sha256:74d97c428c51a828f9051a7a40a53ff1fc99e54fc30323ce36760701b0b7f711
sha256:2d3fbe0ca19dd389108f7e9a1d89ec5c1f1d7dc3c11fb2123bc425fed39c5f22
sha256:ed752e851ebe79a0e72be1fd48e90474d36ae2bb2ddccff2475f8e050c20a804
sha256:ec2b9a359ab960e2254545e79e9bca77edaeefb652d97b26855217fb8a9c80b3
sha256:267e83adf4c4e342b08cbe40814662493e030c88311c720fbce2f0d7c809f3f4
sha256:8fb5da5ad6fa936ba8c24999da33095b2b6c5fd2bcf4aa22acbf299b0e7294df
sha256:0d250d8db7c20b3c0bd3059b3431dd47f130cb81e7a4c7820950139daafb91d6
sha256:e46f4042ccf5f36d1605617ef6912bdaac437fa0bca12530e77529b191eb02fa


$ docker history --format "{{.Size}}\t{{.CreatedBy}}" nginx:1.30-alpine | cut -c1-90
51.9MB	RUN /bin/sh -c set -x     && apkArch="$(cat …
0B	ENV ACME_VERSION=0.4.1
0B	ENV NJS_RELEASE=1
0B	ENV NJS_VERSION=1.0.1
0B	CMD ["nginx" "-g" "daemon off;"]
0B	STOPSIGNAL SIGQUIT
0B	EXPOSE map[80/tcp:{}]
0B	ENTRYPOINT ["/docker-entrypoint.sh"]
16.4kB	COPY 30-tune-worker-processes.sh /docker-ent…
12.3kB	COPY 20-envsubst-on-templates.sh /docker-ent…
12.3kB	COPY 15-local-resolvers.envsh /docker-entryp…
12.3kB	COPY 10-listen-on-ipv6-by-default.sh /docker…
8.19kB	COPY docker-entrypoint.sh / # buildkit
5.62MB	RUN /bin/sh -c set -x     && addgroup -g 101…
0B	ENV DYNPKG_RELEASE=1
0B	ENV PKG_RELEASE=1
0B	ENV NGINX_VERSION=1.30.5
0B	LABEL maintainer=NGINX Docker Maintainers <d…
0B	CMD ["/bin/sh"]
9.08MB	ADD alpine-minirootfs-3.24.2-x86_64.tar.gz /…
  • RootFS.Layers muestra los hashes de las 8 capas.
  • docker history muestra qué línea del Dockerfile creó cada capa. Las que tienen un tamaño de 0B (ENV, CMD, EXPOSE) solo modificaron la configuración y no generaron archivos, así que no tienen una capa correspondiente. El ADD alpine-minirootfs... de la parte inferior es la imagen base de Alpine.

Cómo se combinan para formar el directorio raíz

Cuando se inicia el contenedor, Docker combina estas capas con overlayfs y añade una capa escribible. Consulta la información de montaje de este contenedor en el host:

$ grep overlay /proc/mounts | grep $(docker inspect -f '{{.Id}}' demo) | tr ',' '\n' | sed 's#/var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.overlayfs/snapshots#…/snapshots#g'
overlay /var/lib/docker/rootfs/overlayfs/57dcd17505d4e7c71e57df55edef3e137c18249e6ad402c7e7b3f215341c47b4 overlay rw
relatime
lowerdir=…/snapshots/260/fs:…/snapshots/21/fs:…/snapshots/20/fs:…/snapshots/19/fs:…/snapshots/18/fs:…/snapshots/17/fs:…/snapshots/16/fs:…/snapshots/15/fs:…/snapshots/1/fs
upperdir=…/snapshots/261/fs
workdir=…/snapshots/261/work
xino=off 0 0

(La ruta es demasiado larga, así que … sustituye a /var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.overlayfs.)

ElementoSignificado
lowerdirCapas de imagen de solo lectura, apiladas de arriba abajo y compartidas por todos los contenedores que usan esta imagen
upperdirCapa escribible exclusiva de este contenedor
workdirDirectorio temporal interno de overlayfs
Punto de montaje /var/lib/docker/rootfs/overlayfs/ID del contenedorEl resultado combinado: el / que ve el contenedor
overlayfs: capas de imagen de solo lectura más un upperdir escribible, combinados para formar el sistema de archivos del contenedor
overlayfs: varias capas de solo lectura + una capa escribible = el directorio raíz del contenedor

Dónde van los archivos que escribe el contenedor

Crea un archivo dentro del contenedor y luego búscalo en el upperdir del host:

$ docker exec demo sh -c "echo hello > /usr/share/nginx/html/new.html"

$ find /var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.overlayfs/snapshots/261/fs -type f | sed 's#.*/fs/#upperdir/#'
upperdir/usr/share/nginx/html/new.html
upperdir/run/nginx.pid
upperdir/etc/nginx/conf.d/default.conf

$ docker diff demo
C /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
A /usr/share/nginx/html/new.html
C /run
A /run/nginx.pid
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
  • El archivo nuevo new.html aparece en el upperdir.
  • nginx escribe nginx.pid al iniciarse; el script de inicio de la imagen modificó default.conf (añadió la configuración IPv6). Cuando se modifica un archivo que ya existe en la imagen, overlayfs primero lo copia al upperdir y luego lo modifica (copy-up).
  • docker diff muestra el contenido del upperdir: A indica que se añadió, C que se modificó (incluidas las carpetas de niveles superiores) y D que se eliminó.

Por tanto:

  • Eliminar un contenedor = eliminar su upperdir; los datos escritos dentro del contenedor desaparecen. Por eso debes guardar los datos en un Volume, que es un directorio montado directamente y no pasa por overlayfs.
  • Si una imagen inicia 100 contenedores, las capas de la imagen se almacenan una sola vez.
  • En un Dockerfile, «primero COPY y luego RUN rm» no reduce el tamaño de la imagen, porque el archivo sigue en la capa de solo lectura inferior. Las prácticas recomendadas de seguridad incluso lo eliminan.

capabilities y seccomp: determinan qué puede hacer un contenedor

$ docker info --format "{{.Driver}} {{json .SecurityOptions}}"
overlayfs ["name=apparmor,profile=default","name=seccomp,profile=builtin"]

$ docker exec demo grep CapEff /proc/1/status
CapEff:	00000000a80425fb

$ docker run --rm --cap-drop ALL alpine:3.23 grep CapEff /proc/1/status
CapEff:	0000000000000000
  • docker info muestra que este entorno tiene habilitadas las configuraciones predeterminadas de AppArmor y seccomp. seccomp bloquea algunas llamadas al sistema peligrosas que hacen los contenedores (por ejemplo, cargar módulos del kernel).
  • CapEff representa las capabilities que tiene actualmente el proceso, en forma de bits. De forma predeterminada, root dentro del contenedor solo tiene parte de los permisos (a80425fb), no todos; después de --cap-drop ALL, todos quedan a cero. Consulta el efecto real en las prácticas recomendadas de seguridad.
  • --privileged concede al contenedor todas las capabilities y desactiva seccomp y AppArmor, lo que elimina esta capa de protección.

Comparación con las máquinas virtuales

ContenedorMáquina virtual
KernelComparte el kernel Linux del hostCada máquina virtual tiene su propio kernel
AislamientoFunciones del kernel como namespace, cgroup y seccompVirtualización de hardware
InicioInicia un proceso, normalmente en menos de 1 segundoArranca el sistema, de varios a decenas de segundos
Sobrecarga adicionalPrácticamente ningunaHay que asignar memoria y CPU a cada máquina virtual
Nivel de aislamientoMenor; una vulnerabilidad del kernel puede afectar a todos los contenedoresMayor

Esto también explica varios comportamientos habituales:

  • En un host Linux, ps muestra los procesos del contenedor; en Windows y macOS no, porque el contenedor está dentro de la máquina virtual de Docker Desktop.
  • Un contenedor no puede ejecutar un sistema operativo con un kernel distinto. Por ejemplo, un host Linux no puede ejecutar directamente contenedores de Windows.
  • El error exec format error de las imágenes multiplataforma: el contenedor no tiene su propia CPU, así que el programa debe estar en un formato que pueda ejecutar la CPU del host.

Resumen

  • Un contenedor es un proceso Linux en el host, creado por runc y supervisado por containerd-shim.
  • namespace aísla los procesos, la red, los montajes, el nombre del host y otros «elementos visibles»; de forma predeterminada, no aísla a los usuarios.
  • cgroup implementa --memory, --cpus y --pids-limit; puedes leer directamente los valores configurados en los archivos de cgroup.
  • overlayfs combina las capas de imagen de solo lectura con un upperdir escribible para formar el directorio raíz del contenedor; al eliminar el contenedor, se elimina el upperdir.
  • capabilities, seccomp y AppArmor limitan lo que puede hacer root dentro del contenedor; --privileged elimina todas estas restricciones.

Ejercicios

Ejercicio 1: Ejecuta docker run -d --name t1 --pids-limit 50 alpine:3.23 sleep 600 y luego lee el límite de PID desde dentro del contenedor.

Respuesta

cgroup v1:

docker exec t1 cat /sys/fs/cgroup/pids/pids.max

cgroup v2:

docker exec t1 cat /sys/fs/cgroup/pids.max

Ambos deberían mostrar 50. Usa docker info --format "{{.CgroupVersion}}" para confirmar qué versión utiliza tu sistema.

Ejercicio 2: Elimina dentro del contenedor un archivo que ya existía en la imagen (por ejemplo, docker exec t1 rm /etc/motd) y luego ejecuta docker diff t1. ¿Qué verás? ¿Se ha eliminado realmente el archivo de la imagen?

Respuesta

docker diff mostrará D /etc/motd. Las capas de la imagen son de solo lectura, así que el archivo no se ha eliminado realmente: overlayfs coloca un «marcador de eliminación» (whiteout) en upperdir para que no aparezca en la vista combinada. Si inicias un contenedor nuevo con la misma imagen, /etc/motd seguirá existiendo.

Ejercicio 3: Inicia al mismo tiempo dos contenedores nginx:1.30-alpine y compara sus lowerdir y upperdir.

Pista

Ejecuta grep overlay /proc/mounts en el host Linux. Los lowerdir de los dos contenedores serán exactamente iguales (comparten las capas de la imagen), mientras que cada uno tendrá un upperdir diferente.

Preguntas frecuentes

¿Cuál es la diferencia fundamental entre un contenedor y una máquina virtual?

Una máquina virtual emula un ordenador completo y ejecuta su propio kernel de sistema operativo; un contenedor no tiene kernel propio: todos los contenedores usan directamente el kernel Linux del host y están aislados mediante namespaces y cgroups. Por eso, iniciar un contenedor solo requiere iniciar un proceso y prácticamente no lleva tiempo.

¿Por qué Windows y macOS también pueden ejecutar contenedores Linux?

Porque Docker Desktop inicia en segundo plano una pequeña máquina virtual Linux (en Windows es WSL 2), y los contenedores se ejecutan realmente sobre el kernel Linux de esa máquina virtual.

¿Qué diferencia hay entre cgroup v1 y v2?

Ambos son mecanismos del kernel Linux para limitar recursos; v2 es la versión más reciente y unifica la estructura de directorios. Los nombres de los archivos son distintos: por ejemplo, el límite de memoria se llama memory.limit_in_bytes en v1 y memory.max en v2; la cuota de CPU es cpu.max en v2. Las distribuciones recientes y Docker Desktop usan v2 de forma predeterminada; docker info muestra la versión en Cgroup Version.

Si una imagen inicia 100 contenedores, ¿ocuparán 100 veces más espacio?

No. Las capas de la imagen son el lowerdir de solo lectura y las comparten todos los contenedores; cada contenedor solo añade su propio upperdir, donde guarda los archivos que escribe o modifica.

Actualizado 14 de octubre de 2026