Tiempo de lectura: 5 minutos
Imagen: Mario Hinojosa Freire

El secreto de Docker es que Linux hace todo el trabajo sucio

Docker se ha consolidado como una herramienta fundamental en el sector IT, especialmente en roles técnicos. Es común observar el clásico diagrama que compara las máquinas virtuales con los contenedores, aunque raras veces se profundiza en la tecnología subyacente.

Las máquinas virtuales, solución predominante en la infraestructura antes de la estandarización de los contenedores, resultan costosas en términos de recursos (overhead) debido a su propia naturaleza. La adopción masiva de contenedores responde principalmente a dos factores: el auge de las arquitecturas de microservicios y su bajo consumo de cómputo en comparación con las máquinas virtuales.

La virtualización emplea un modelo de traducción y emulación. Se crea un entorno que simula hardware real para la máquina invitada (guest); el hipervisor intercepta las instrucciones, las traduce al sistema operativo anfitrión y devuelve los resultados al guest.

Imagen: Mario Hinojosa Freire

Este proceso conlleva un alto gasto computacional. Por el contrario, los contenedores son eficientes porque comparten el kernel del anfitrión. Esto permite crear entornos aislados con un consumo de recursos significativamente menor, optimizando la capacidad del sistema para otras tareas.

Imagen: Mario Hinojosa Freire

La arquitectura de Docker

A nivel de arquitectura, Docker es sencillo. Con modelo cliente-servidor, el cliente de Docker se comunica con el daemon de Docker (dockerd), que a su vez se encarga montar, correr y distribuir los contenedores. Ambos se comunican mediante una API REST, a través de sockets UNIX (si cliente y servidor están en la misma máquina) o una interfaz de red (para gestión remota).

Es decir, el daemon está a la escucha actuando como servidor. Cuando recibe peticiones a la API de Docker realiza las acciones solicitadas en contenedores, imágenes, redes o volúmenes. El cliente es habitualmente la aplicación CLI (docker), pero puede ser una SDK o una interfaz gráfica (Docker Desktop).

Imagen: Mario Hinojosa Freire

Los conceptos fundamentales son las imágenes y los contenedores, habitualmente almacenados en registros (registries). Las imágenes funcionan como plantillas con instrucciones de creación, mientras que el contenedor es una instancia funcional de una imagen. A través de la API, es posible gestionar su ejecución, conectarlos a redes o persistir su estado, aunque la migración de contenedores activos entre diferentes hosts suele requerir herramientas adicionales de orquestación.

Imagen: Mario Hinojosa Freire

Los namespaces

En esencia, los contenedores son entornos delimitados que ejecutan sistemas operativos diversos sin necesidad de virtualización. Para lograr este aislamiento, Docker emplea una característica del kernel de Linux denominada namespaces.

Un namespace «envuelve» un recurso global del sistema en un abstracción que hace parecer al proceso contenido en el namespace que está en un entorno aislado. Otros procesos en el mismo namespace podrían interactuar pero no «ver» procesos fuera de ese namespace. Hay varios tipos de namespace, cada uno con su función.

Espacio de Nombres (Namespace)Flag (Bandera)Página de ManualAísla / Separa
CgroupCLONE_NEWCGROUPcgroup_namespaces(7)Directorio raíz de cgroups
IPCCLONE_NEWIPCipc_namespaces(7)IPC de System V, colas de mensajes POSIX
Red (Network)CLONE_NEWNETnetwork_namespaces(7)Dispositivos de red, stacks, puertos, etc.
Montaje (Mount)CLONE_NEWNSmount_namespaces(7)Puntos de montaje
PIDCLONE_NEWPIDpid_namespaces(7)IDs de proceso
Tiempo (Time)CLONE_NEWTIMEtime_namespaces(7)Relojes de arranque y monotónicos
Usuario (User)CLONE_NEWUSERuser_namespaces(7)IDs de usuario y de grupo
UTSCLONE_NEWUTSuts_namespaces(7)Nombre de host (hostname) y dominio NIS

Docker aprovecha estas funcionalidades para garantizar que los procesos no interfieran con el host u otros contenedores. Los Control Groups (cgroups), por su parte, limitan el consumo de recursos (memoria, CPU, E/S) para evitar el colapso del sistema anfitrión.

También se utilizan las capabilities, una funcionalidad de Linux que pretende hacer más robusta la seguridad definiendo una serie de permisos granulares, frente al modelo privilegiado vs. no privilegiado, tradicional el Linux. Esto permite crear contenedores con algunas capacidades privilegiadas sin darles privilegios elevados.

Imagen: Mario Hinojosa Freire

Los namespaces en acción

Al examinar los procesos en un host Linux, se observa el sistema de inicio (como systemd) y diversos hilos del kernel (kernel threads).

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND  
root           1  0.0  0.0  28520 16288 ?        Ss   19:40   0:02 /usr/lib/systemd/systemd --switched-root --system --deserialize=56 splash  
root           2  0.0  0.0      0     0 ?        S    19:40   0:00 [kthreadd] 
root           3  0.0  0.0      0     0 ?        S    19:40   0:00 [pool_workqueue_release]  
root           4  0.0  0.0      0     0 ?        I<   19:40   0:00 [kworker/R-rcu_gp]  
root           5  0.0  0.0      0     0 ?        I<   19:40   0:00 [kworker/R-sync_wq]  
root           6  0.0  0.0      0     0 ?        I<   19:40   0:00 [kworker/R-kvfree_rcu_reclaim]  
root           7  0.0  0.0      0     0 ?        I<   19:40   0:00 [kworker/R-slub_flushwq]  
root           8  0.0  0.0      0     0 ?        I<   19:40   0:00 [kworker/R-netns]  
root          10  0.0  0.0      0     0 ?        I<   19:40   0:00 [kworker/0:0H-kblockd]  
root          13  0.0  0.0      0     0 ?        I<   19:40   0:00 [kworker/R-mm_percpu_wq]  
root          15  0.0  0.0      0     0 ?        S    19:40   0:00 [ksoftirqd/0]  
root          16  0.0  0.0      0     0 ?        I    19:40   0:00 [rcu_preempt]  
root          17  0.0  0.0      0     0 ?        S    19:40   0:00 [rcub/0]  
root          18  0.0  0.0      0     0 ?        S    19:40   0:00 [rcu_exp_par_gp_kthread_worker/0]  
root          19  0.0  0.0      0     0 ?        S    19:40   0:00 [rcu_exp_gp_kthread_worker]  
root          20  0.0  0.0      0     0 ?        S    19:40   0:00 [migration/0]  
root          21  0.0  0.0      0     0 ?        S    19:40   0:00 [kprobe-optimizer]  
root          22  0.0  0.0      0     0 ?        S    19:40   0:00 [idle_inject/0]  
root          23  0.0  0.0      0     0 ?        S    19:40   0:00 [cpuhp/0]

En un contenedor, sin embargo, no existen procesos como init o systemd, visualizándose únicamente las tareas en ejecución.

/ # ps aux  
PID   USER     TIME  COMMAND  
    1 root      0:00 /bin/sh  
    7 root      0:00 ps aux

Lo que sucede por debajo es algo así:

Imagen: Mario Hinojosa Freire

El contenedor utiliza los recursos de la máquina anfitriona comunicándose directamente con el kernel de forma segura. En entornos de producción, esta arquitectura se gestiona mediante orquestadores como Kubernetes, que automatizan la monitorización y el escalado de miles de instancias.

Profundizando en la práctica

docker run --rm -it alpine:latest echo "Araintel"

Al ejecutar este comando, Docker crea un contenedor basado en la distribución Alpine. Tras ejecutar echo "Araintel", el contenedor se detiene y se elimina. El flujo interno es el siguiente:

    sequenceDiagram         participant H as Host Shell         participant C as Docker CLI         participant D as Dockerd (Daemon)         participant S as Containerd-Shim         participant R as RunC         participant K as Kernel (Namespace/Cgroup)         participant P as Container Process (echo)            Note over H,C: 1. Invocación Binario        H->>C: execve(«/usr/bin/docker», [«run», …])        C->>C: socket(AF_UNIX) & connect(«/var/run/docker.sock»)        C->>D: HTTP POST /containers/create (via write syscall)           Note over D,S: 2. Creación Procesos        D->>S: Spawn containerd-shim (fork/exec)        S->>R: execve(«runc», [«create», …])           Note over R,K: 3. Aislamiento (Syscalls clave)        R->>K: unshare(CLONE_NEWPID|CLONE_NEWNS|CLONE_NEWNET|…)        R->>K: mount(«none», «/», NULL, MS_REC|MS_PRIVATE, NULL)        R->>K: pivot_root(new_root, old_root)        R->>K: chdir(«/»)        R->>K: capset(…) [Restringir privilegios]        R->>K: prctl(PR_SET_SECCOMP, …) [Filtro syscalls]           Note over R,P: 4. Ejecución y Retorno        R->>P: execve(«/bin/sh», [«-c», «echo Araintel»])        P->>P: write(1, «Araintel\n»)        P->>S: (Pipes/PTY capturan stdout)        S->>D: stream data (gRPC)        D->>C: HTTP Response Chunked        C->>H: write(1, «Araintel\n») [Host Terminal]
  1. Se invoca el binario docker, iniciando la comunicación vía socket UNIX.
  2. El daemon (dockerd) recibe la petición en el endpoint /containers/create e inicia la creación del entorno.
  3. RunC realiza una serie de llamadas al sistema (syscalls) para configurar el aislamiento:
    • unshare: Desasocia partes del contexto de ejecución (namespaces) que antes compartía con su proceso padre.
    • mount: Monta el sistema de archivos de la imagen en el directorio de destino del contenedor.
    • pivot_root: Cambia el directorio raíz del proceso a la nueva ubicación, desplazando la raíz antigua a un subdirectorio.
    • chdir: Cambia el directorio de trabajo actual a la nueva raíz (/).
    • capset: Aplica privilegios granulares (capabilities) específicos para el proceso.
    • prctl: Establece filtros de seguridad (como Seccomp) para limitar las syscalls disponibles.
  4. Una vez aislado, se ejecuta el comando especificado. La salida se captura y se redirige a la terminal del host a través de la pseudoterminal creada por la bandera -it.

En definitiva

Docker se cimenta en funcionalidades de Linux para ser lo que es. De hecho, Windows soporta Docker sólo después de implementar WSL (Windows Subsystem for Linux). Utiliza estas funcionalidades de forma inteligente para crear los contenedores que cada vez más potencian arquitecturas de aplicaciones modernas, y entender la implementación puede ser muy útil para utilizarlo con más criterio.