0%
Introducción a Docker

Introducción

Los conceptos y la arquitectura antes de los comandos.

10-15 min

Introducción a Docker

«En mi máquina funciona». Esta frase resume por sí sola el problema al que se enfrenta Docker. Una aplicación nunca se reduce a su código: depende de una versión de intérprete, de una biblioteca del sistema, de una variable de entorno, de un archivo de configuración colocado en el lugar correcto, de un servicio de base de datos accesible en un puerto concreto. Este halo de dependencias casi nunca está escrito en ningún sitio. Vive en la máquina del desarrollador que hizo funcionar el proyecto por primera vez, y se reconstruye a mano, con más o menos suerte, en cada máquina nueva.

La consecuencia no es solo un mal rato instalando un proyecto. Es toda una clase de bugs que solo existen en un entorno concreto: una prueba que pasa en local y falla en integración, una corrección validada en preproducción que se rompe en producción porque la versión de OpenSSL no es la misma. Mientras el entorno de ejecución siga siendo implícito, seguirá siendo una variable no controlada de tu sistema, y depurarás diferencias en lugar de código.

Docker propone una respuesta sencilla de enunciar: hacer que el entorno sea explícito, versionado y transportable igual que el código. Se describe en un archivo de texto todo lo que la aplicación necesita para funcionar, se construye con ello un artefacto inmutable y se ejecuta ese artefacto tal cual en todas partes: puesto de desarrollo, servidor de integración, producción. Ya no es «instala estas siete cosas y luego ejecuta», sino «ejecuta este artefacto».

Esta primera lección no contiene, deliberadamente, casi nada de configuración. Instala el vocabulario y el modelo mental —imagen, contenedor, capa, registro— porque la práctica totalidad de los bloqueos que encuentran los principiantes con Docker proviene de una confusión sobre estas cuatro palabras, no de una opción de comando mal escrita.

Contenedor o máquina virtual: dónde está la verdadera diferencia

La analogía «un contenedor es una pequeña VM» es la más extendida y la más costosa. Es falsa en el punto que importa: qué es lo que se virtualiza.

Una máquina virtual virtualiza hardware. Un hipervisor presenta al invitado un procesador, memoria y discos simulados, y ese invitado arranca un kernel completo, su propio sistema operativo, sus servicios de arranque. Dos VM Linux en un mismo host ejecutan dos kernels Linux distintos.

Un contenedor no virtualiza nada de eso. Utiliza el kernel del host y se limita a pedirle una vista restringida del sistema, gracias a dos mecanismos del kernel de Linux:

  • los namespaces, que aíslan lo que el proceso ve: su propio árbol de procesos (se cree PID 1), su propia pila de red, sus propios puntos de montaje, su propio nombre de host;
  • los cgroups (control groups), que limitan lo que el proceso consume: cuota de CPU, tope de memoria, ancho de banda de entrada/salida.

Un contenedor es, por tanto, un proceso ordinario del host, aislado y limitado. Esto explica todas sus diferencias de comportamiento con una VM:

Máquina virtualContenedor
Qué se virtualizaEl hardwareLa vista del sistema (namespaces)
KernelUn kernel por VMEl del host, compartido
ArranqueDecenas de segundos (arranque del SO)De milisegundos a unos pocos segundos
Peso en discoVarios gigabytesDe unos pocos megabytes a cientos de MB
Frontera de seguridadFuerte (hipervisor)Más débil (kernel compartido)
Sistemas alojablesCualquier SOLos compatibles con el kernel del host

Las dos últimas filas merecen recordarse en lugar de leerse por encima. Como el kernel es compartido, un fallo de escape de contenedor da acceso al host: para ejecutar código no fiable y multiinquilino, la VM sigue siendo la frontera de referencia, y la industria suele combinar ambas (contenedores dentro de VM). Y, también porque el kernel es compartido, una imagen Linux necesita un kernel Linux. Cuando lanzas un contenedor Linux en macOS o Windows, Docker Desktop ejecuta discretamente una máquina virtual Linux por ti: el kernel viene de ahí.

Puedes comprobar tú mismo que el kernel se comparte, una vez instalado Docker:

# Version du noyau de votre hôte
uname -r

# Version du noyau vue depuis l'interieur d'un conteneur Ubuntu.
# Le resultat est identique a la ligne du dessus : c'est le meme noyau.
docker run --rm ubuntu:24.04 uname -r

En cambio, la distribución sí difiere: es la imagen quien la proporciona.

# L'hote est peut-etre une Debian, le conteneur se presente en Ubuntu 24.04
docker run --rm ubuntu:24.04 cat /etc/os-release

Una imagen no contiene, pues, un sistema operativo completo: ni kernel ni controladores. Contiene el espacio de usuario de una distribución: sus bibliotecas, sus binarios, su árbol de archivos. De ahí su ligereza.

Imagen, contenedor, registro: tres objetos que no hay que confundir

Tres palabras, tres naturalezas distintas. La confusión entre las dos primeras es, con diferencia, la primera causa de incomprensión.

Una imagen es una plantilla de solo lectura: una pila de archivos inmutable, más metadatos (comando a ejecutar, puerto expuesto, variables de entorno, usuario). Una imagen no se ejecuta; nunca «está corriendo». Es el equivalente de un ejecutable en disco, o de una clase en programación orientada a objetos.

Un contenedor es una instancia en ejecución de una imagen. Docker toma la imagen, le añade una fina capa modificable con permisos de escritura y arranca el proceso declarado. Es el equivalente de un proceso lanzado, o de un objeto instanciado. Una misma imagen puede dar lugar a cien contenedores independientes.

Un registro es un almacén de imágenes accesible por red. Docker Hub es el registro público por defecto; existen también privados (GitLab, GitHub, AWS ECR, Harbor). Es el equivalente de un repositorio de paquetes como npm o PyPI, pero para entornos completos.

# 1. Recuperer une image depuis un registre vers le cache local
docker pull nginx:1.27-alpine

# 2. Lister les images presentes localement
docker images

# 3. Instancier DEUX conteneurs a partir de la MEME image,
#    sur deux ports differents de l'hote
docker run -d --name web1 -p 8081:80 nginx:1.27-alpine
docker run -d --name web2 -p 8082:80 nginx:1.27-alpine

# 4. Les voir tourner cote a cote
docker ps

Dos contenedores, una sola imagen, ninguna duplicación de archivos en el disco. Modificar un archivo en web1 no afecta ni a web2 ni a la imagen: la escritura va a parar a la capa modificable propia de web1 y desaparece con él.

docker exec web1 sh -c 'echo "modifie" > /usr/share/nginx/html/index.html'

# web1 a change...
docker exec web1 cat /usr/share/nginx/html/index.html

# ...web2 n'a rien vu.
docker exec web2 cat /usr/share/nginx/html/index.html

Este detalle es fundamental: un contenedor es efímero por diseño. Todo lo que escribe en su propio sistema de archivos muere con él. No es una limitación que haya que sortear, es la propiedad que hace reproducibles los contenedores, y es la razón de ser de los volúmenes, que la lección 5 trata en detalle.

La referencia de una imagen: registro, repositorio, tag

Un nombre de imagen como nginx:1.27-alpine es una forma abreviada. La forma completa es:

[registre/][espace-de-noms/]depot[:tag]

docker.io/library/nginx:1.27-alpine
│         │       │     │
│         │       │     └── tag : la version
│         │       └──────── depot
│         └──────────────── espace de noms (library = images officielles)
└────────────────────────── registre (docker.io par defaut)

Conviene señalar de inmediato tres trampas.

El tag latest no es una promesa de frescura: es un tag por defecto como cualquier otro, el que se usa cuando no especificas ninguno. Es móvil: la imagen que hay detrás de latest cambia con el tiempo. Dos docker pull con seis meses de diferencia no traen el mismo contenido, lo que arruina la reproducibilidad buscada.

El sufijo -alpine designa imágenes basadas en Alpine Linux, muy pequeñas porque usan la biblioteca C musl en lugar de glibc. Este ahorro tiene un coste real: algunos binarios precompilados esperan glibc y fallan. Las variantes -slim (base Debian reducida) suelen ser el mejor compromiso.

Por último, para fijar totalmente una imagen, se puede referenciar por su digest, la huella criptográfica de su contenido, que nunca cambia:

# Un tag est mobile ; un digest est immuable
docker pull nginx@sha256:0c86dddac19f2ce4fb916fcd7015e1272ea67e3a3fbb6f88df3eb0e1c9a5c1d1

# Afficher le digest des images locales
docker images --digests

La arquitectura: cliente, daemon, registro

Cuando escribes docker run, no lanzas el contenedor. Ejecutas un cliente de línea de comandos, que envía una petición HTTP a una API REST expuesta por un daemon —dockerd—, y es este quien hace el trabajo real: resolver la imagen, descargarla si hace falta, crear los namespaces y los cgroups, arrancar el proceso.

   docker CLI  ──── API REST (socket UNIX) ────>  dockerd  ────>  conteneurs
  (votre shell)   /var/run/docker.sock            (le daemon)     (processus isoles)
                                                      │
                                                      └──── HTTPS ────> registre

Esta separación no es un detalle de implementación; tiene tres consecuencias prácticas.

En primer lugar, casi todos los errores del tipo «Cannot connect to the Docker daemon» no son errores de comando: el daemon no está arrancado, o tu usuario no tiene permiso para escribir en el socket. La lección siguiente resuelve este punto.

En segundo lugar, el cliente puede controlar un daemon remoto. Esto permite construir una imagen en un servidor potente desde un portátil:

# Piloter un daemon distant via SSH, sans rien installer de plus
docker --host ssh://deploy@build-01.exemple.com ps

# Ou en fixant le contexte de facon persistante
docker context create serveur --docker "host=ssh://deploy@build-01.exemple.com"
docker context use serveur
docker context ls        # affiche le contexte actif
docker context use default

Por último, hay que saber que el daemon se ejecuta con privilegios de superusuario. Dar a una cuenta acceso al socket de Docker equivale, en la práctica, a darle root en el host: un punto que conviene tener presente en cualquier servidor compartido.

Vista general de la instalación en un solo comando:

# Versions du client et du daemon, systeme de stockage, nombre de conteneurs
docker version
docker info

El sistema de capas: por qué las imágenes son ligeras y rápidas

Una imagen no es un archivo monolítico enorme. Es una pila de capas (layers) apiladas y fusionadas en lectura por un sistema de archivos union —overlay2 en Linux—. Cada instrucción de construcción produce una capa que solo registra su diferencia con la anterior.

# Afficher l'historique en couches d'une image, taille par couche
docker history nginx:1.27-alpine

De ello se derivan dos propiedades, y explican la mitad de las ganancias de rendimiento de Docker.

Las capas se comparten entre imágenes. Si diez de tus imágenes derivan de python:3.12-slim, esa base se almacena una sola vez en disco y se descarga una sola vez. Puedes observarlo directamente: el primer pull de una familia de imágenes es largo, y los siguientes indican Already exists en la mayoría de las capas.

docker pull python:3.12-slim
# Les couches communes s'affichent en "Already exists", pas retelechargees
docker pull python:3.12

Las capas son inmutables y se direccionan por contenido. Una capa sin cambios se reutiliza desde la caché en lugar de reconstruirse. Es la palanca central de la optimización de los Dockerfiles —tema de la lección 3— y la razón por la que el orden de las instrucciones en un Dockerfile cambia radicalmente el tiempo de build.

Las capas son también aditivas, nunca sustractivas: eliminar un archivo en una capa posterior lo oculta pero no lo quita de la imagen. Un secreto copiado y luego «eliminado» sigue pudiendo extraerse de la imagen. Volveremos sobre ello.

Lo que Docker no hace

Establecer los límites desde el principio evita expectativas frustradas.

Docker no orquesta un parque de máquinas. Gestiona los contenedores de un host. Repartir contenedores entre veinte servidores, reiniciarlos en otro lugar cuando una máquina cae, escalar automáticamente: ese es el trabajo de un orquestador como Kubernetes. Docker Compose, que veremos en la lección 4, coordina varios contenedores en un único host.

Docker no hace que una aplicación sea escalable ni sin estado. Contenerizar un software que escribe en una carpeta local y guarda sus sesiones en memoria da como resultado un contenedor que escribe en una carpeta local y guarda sus sesiones en memoria. Docker hace visibles esas decisiones de arquitectura; no las corrige.

Docker no es un entorno aislado de seguridad absoluto. Por defecto, un contenedor se ejecuta como root (dentro de su namespace), y varias opciones habituales —--privileged, el montaje del socket de Docker— abren de par en par la puerta hacia el host.

Por último, Docker ya no es la única implementación. El formato de las imágenes y el comportamiento de ejecución están normalizados por la Open Container Initiative. Herramientas como Podman o containerd consumen las mismas imágenes. Lo que aprendes aquí no está, por tanto, encerrado en un producto.

El ciclo de trabajo completo, a vista de pájaro

Antes de entrar en los comandos, este es el bucle que desarrollarán las cinco lecciones siguientes:

  Dockerfile  ──build──>  Image  ──push──>  Registre
                            │                   │
                            │                   pull
                            run                 │
                            v                   v
                        Conteneur  <────────  Serveur de production

Escribes un Dockerfile (lección 3), construyes con él una imagen, la ejecutas en local para comprobar, la subes a un registro, y un servidor la descarga para ejecutarla de forma idéntica (lección 6). Entretanto, Compose (lección 4) te permite hacer convivir varios contenedores, conectados por una red y respaldados por volúmenes (lección 5).

Este es ese bucle reducido a sus comandos esenciales, como referencia; cada uno se retomará en detalle:

docker build -t mon-app:1.0 .     # Dockerfile -> image
docker run -d -p 8080:8080 mon-app:1.0   # image -> conteneur
docker push registre.exemple.com/mon-app:1.0   # image -> registre

Conclusiones

  • Un contenedor no es una pequeña VM: es un proceso del host aislado mediante namespaces y limitado por cgroups, que utiliza el kernel del host. De ahí su ligereza, su rapidez de arranque y una frontera de seguridad más fina que la de un hipervisor.
  • Imagen = plantilla inmutable de solo lectura; contenedor = instancia en ejecución de esa plantilla; registro = almacén de imágenes. Una imagen no se ejecuta, un contenedor no se comparte.
  • Todo lo que un contenedor escribe en su sistema de archivos desaparece con él. Es intencionado, y es la razón de ser de los volúmenes.
  • Una imagen es una pila de capas inmutables compartidas entre imágenes y almacenadas en caché: la palanca principal para conseguir builds rápidos y transferencias ligeras.
  • El cliente docker no hace nada por sí mismo: se comunica con el daemon dockerd a través de una API. Este desacoplamiento explica los errores de conexión al socket y permite controlar un host remoto.
  • Evite la etiqueta latest siempre que importe la reproducibilidad: fije una versión, o un digest sha256: para un bloqueo total.

Referencia: la documentación oficial está en docs.docker.com. Es la referencia para las opciones de los comandos, que cambian de una versión a otra.

Comentarios

Los comentarios funcionan con GitHub Discussions

Inicia sesión con GitHub para participar en la discusión

¡Enlace copiado!