Saltar a contenido

Contenedores

Bibliografía

Docker


Logo de Docker

Docker es una plataforma de código abierto que facilita la creación, implementación y ejecución de aplicaciones mediante contenedores. Un contenedor empaqueta una aplicación junto con todas sus dependencias y configuraciones en una unidad estandarizada, lo que simplifica el desarrollo de software y garantiza la consistencia entre distintos entornos. Cabe mencionar que también existen alternativas de código abierto como Podman, que están ganando relevancia debido a los últimos cambios de licencia y uso de Docker en entornos empresariales.


Sistema basado en Microservicios

Este tipo de arquitectura se basa en el concepto de microservicios, ya que permite empaquetar cada servicio de forma independiente con sus propias dependencias, evitando así conflictos entre ellas. La comunicación entre contenedores, es decir, entre cada microservicio, se realiza habitualmente mediante APIs.

Entre sus características principales destacan la portabilidad, puesto que los contenedores se ejecutan en cualquier sistema que soporte Docker independientemente del sistema operativo del host. También destaca la ligereza, dado que comparten el kernel del sistema operativo del host, lo que los hace más rápidos de iniciar que las máquinas virtuales. La consistencia asegura que una aplicación se ejecute de la misma manera en cualquier entorno. El aislamiento garantiza que cada contenedor opera de manera independiente, mejorando la seguridad y evitando conflictos entre aplicaciones. Por último, la escalabilidad facilita la creación y eliminación rápida de instancias.

Contenedores frente a máquinas virtuales

Los contenedores y las máquinas virtuales son tecnologías de virtualización que permiten ejecutar múltiples aplicaciones en un solo servidor físico, lo que se conoce como host. Aunque comparten objetivos similares, como optimizar el uso de recursos y asegurar el aislamiento, difieren significativamente en su implementación y arquitectura subyacente.


Pasos para la creación de un contenedor en Docker

Los contenedores constituyen una forma de virtualización a nivel del sistema operativo, también conocida como virtualización ligera. A diferencia de las máquinas virtuales, que virtualizan un sistema operativo completo, los contenedores comparten el núcleo (kernel) del sistema operativo del host y ejecutan aplicaciones dentro de espacios de usuario completamente aislados.

Cada contenedor contiene únicamente la aplicación y sus dependencias (bibliotecas, archivos de configuración y variables de entorno), lo que lo hace extremadamente portátil y fácil de desplegar en diferentes entornos, desde la máquina local de un desarrollador hasta un clúster en la nube.

El aislamiento de los contenedores se logra mediante diferentes técnicas. Los namespaces (espacios de nombres) aíslan recursos del sistema operativo: pid aísla los identificadores de procesos, net proporciona pilas de red separadas, mnt aísla los puntos de montaje del sistema de archivos, ipc aísla recursos de comunicación entre procesos, uts aísla nombres de host y dominios, y user aísla identificadores de usuarios y grupos.

Por otra parte, los cgroups (grupos de control) gestionan el uso de recursos como CPU, memoria y disco, garantizando que los contenedores no consuman más recursos de los asignados. Además, el Union Filesystem (UFS) permite que los contenedores se construyan en capas. Las capas de solo lectura contienen archivos del sistema, mientras que las capas de escritura se mantienen en la parte superior, minimizando el uso de almacenamiento y facilitando el desarrollo iterativo.

Las máquinas virtuales, por su parte, representan una tecnología de virtualización más tradicional que permite ejecutar múltiples sistemas operativos en un servidor físico mediante un hipervisor, como VMware o VirtualBox. Un hipervisor puede ejecutarse directamente en el hardware del servidor (virtualización tipo 1) o sobre un sistema operativo (virtualización tipo 2), gestionando la creación y ejecución de múltiples máquinas virtuales y asignando recursos de hardware de forma eficiente. Cada máquina virtual dispone de su propio sistema operativo completo, lo que proporciona un aislamiento más fuerte que los contenedores, pero a costa de un mayor consumo de CPU, memoria y almacenamiento, así como tiempos de inicio más prolongados.

Los contenedores resultan ideales para desarrollo y pruebas, arquitecturas de microservicios y despliegue continuo (por ejemplo, en herramientas de CI/CD que ofrecen GitLab, GitHub o similares), mientras que las máquinas virtuales son más adecuadas para aplicaciones monolíticas que requieren aislamiento completo del sistema operativo, entornos con múltiples sistemas operativos y cargas de trabajo heredadas.

En la nube, proveedores como Amazon Web Services (AWS), Google Cloud Platform (GCP) y Microsoft Azure ofrecen servicios tanto de contenedores (AWS ECS/Fargate, EKS, Azure Kubernetes Service (AKS) y Google Kubernetes Engine (GKE)) como de máquinas virtuales (EC2 en AWS, VM Instances en GCP y Azure Virtual Machines). Para la gestión local de contenedores, herramientas como Docker Desktop o Docker CLI permiten desarrollar, gestionar y desplegar contenedores.

Arquitectura de Docker Engine

Docker Engine se compone de tres elementos fundamentales. El primero es Docker CLI, una interfaz de línea de comandos que puede ejecutarse incluso en una máquina remota. El segundo es la REST API, que actúa como canal de comunicación entre el CLI y el daemon. El tercero es el Docker Daemon, que gestiona imágenes, contenedores, redes y volúmenes. El CLI puede comunicarse con un daemon remoto a través de la REST API, lo que permite gestionar contenedores en servidores remotos de forma transparente.

Info

Un daemon es un tipo de programa que se ejecuta en segundo plano, en lugar de bajo el control directo de un usuario. Son procesos autónomos que inician durante el arranque del sistema y gestionan tareas recurrentes como servicios de red, impresión o sincronización.

Hay que tener en cuenta que los contenedores comparten el kernel del host. Si el host tiene un kernel Linux, no se pueden ejecutar contenedores Windows de forma nativa, y viceversa. Sin embargo, al instalar Docker en Windows, se crea una instancia de Linux mediante WSL sobre la que Docker ejecuta los contenedores.

Tags e imágenes

Las imágenes utilizan tags para identificar variantes según el sistema operativo base (Alpine, Debian, Ubuntu), la versión del paquete y otros criterios. Si no se especifica un tag, Docker utiliza latest por defecto, lo cual no se considera buena práctica, ya que no se tiene control sobre las versiones utilizadas y podrían aparecer nuevos problemas no contemplados previamente. Por ello, se recomienda fijar siempre la versión y actualizarla de forma controlada, por ejemplo, ante problemas de seguridad.

Las propias imágenes públicas en Docker Hub suelen disponer de un sistema de alertas que notifica cuando una imagen se ve comprometida o se detecta algún tipo de fallo o mejora relevante en el servicio al que corresponde.

Ejemplo
# Especificar versión con tag
docker run redis:4.0

Los tags soportados se consultan en la documentación de cada imagen en Docker Hub.

Recopilación de comandos

A continuación se muestra una tabla que recopila los comandos más utilizados para la gestión de contenedores con Docker:

Comando Uso/función
docker images Devuelve un listado de todas las imágenes descargadas en la máquina.
docker pull nombre_imagen Descarga una imagen de Docker desde Docker Hub.
docker image rm nombre_imagen:tag Elimina una imagen de Docker.
docker create nombre_imagen Crea un contenedor a partir de una imagen y devuelve el ID del contenedor creado.
docker create --name nombre_contenedor nombre_imagen Crea un contenedor con un nombre específico a partir de una imagen.
docker start ID_contenedor Inicia un contenedor mediante su ID.
docker start nombre_contenedor Inicia un contenedor mediante su nombre.
docker ps Muestra los contenedores activos con información sobre ID, imagen, estado y nombre.
docker ps -a Muestra todos los contenedores, tanto activos como detenidos.
docker stop nombre_contenedor Detiene un contenedor usando su nombre.
docker stop ID_contenedor Detiene un contenedor usando su ID.
docker rm nombre_contenedor Elimina un contenedor de Docker.
docker run -d -p 8080:80 -i --name Debian debian:latest Crea y ejecuta un contenedor mapeando puertos. -d: Ejecuta el contenedor en segundo plano. -p: Mapeo de puertos. -i: Acceso al terminal. --name: Nombre del contenedor.
docker exec -it nombre_contenedor bash Ejecuta un comando en el contenedor, en este caso, accede al terminal del contenedor para interactuar con él.
docker cp ruta_host nombre_contenedor:ruta_contenedor Copia archivos del host al contenedor.
docker stats nombre_contenedor Monitorea el uso de CPU, memoria y ancho de banda de un contenedor.
docker stats Monitorea el uso de recursos de todos los contenedores en ejecución.
docker network ls Muestra todas las redes configuradas en Docker.
docker network inspect nombre_red Obtiene detalles sobre una red específica, incluyendo direcciones IP y contenedores conectados.
docker network create nombre_red Crea una red personalizada.
docker network rm nombre_red Elimina una red.
docker volume create nombre_volumen Crea un volumen.
docker volume ls Lista todos los volúmenes.
docker volume rm nombre_volumen Elimina un volumen.
docker inspect nombre_contenedor Muestra detalles de un contenedor.
docker logs nombre_contenedor Muestra los registros del contenedor.
docker logs -f nombre_contenedor Muestra los registros del contenedor de manera continua.
docker tag nombre_imagen nueva_etiqueta Etiqueta una imagen.
docker login Inicia sesión en un registry.
docker push nombre_imagen Sube una imagen a Docker Hub.
docker logout Cierra sesión en un registry.
docker system prune --all Elimina todos los contenedores detenidos e imágenes que no estén en uso.
docker volume prune Elimina todos los volúmenes que no estén en uso.
docker network prune Elimina todas las redes que no estén en uso, excepto las predeterminadas (bridge, none, host).
docker update [OPTIONS] CONTAINER [CONTAINER...] Actualiza la configuración de uno o varios contenedores. Documentación
docker run --cpu-shares=512 -m 256m nombre_imagen Especifica recursos de sistema (CPU, memoria) para un contenedor.
docker stop $(docker ps -q) Detiene todos los contenedores en ejecución.
docker start $(docker ps -a -q) Inicia todos los contenedores.
docker rm $(docker ps -a -q) Elimina todos los contenedores.

Modo interactivo y attach/detach

Es importante tener en cuenta que un contenedor no es un sistema operativo completo, sino que está pensado para alojar servicios y aplicaciones. Si se ejecuta una imagen como Ubuntu sin ningún proceso activo, el contenedor se detiene inmediatamente al no tener ninguna tarea que mantener.

Los contenedores de Docker no leen la entrada estándar (stdin) de forma predeterminada. Para interactuar con un contenedor se utilizan las opciones -i (modo interactivo, que mapea stdin) y -t (que crea un pseudoterminal).

Ejemplo
1
2
3
4
5
# Modo interactivo con pseudoterminal
docker run -it <imagen> bash

# Ejemplo: acceder al bash de una imagen con uv preinstalado
docker run -it ghcr.io/astral-sh/uv:debian bash

Cuando se ejecuta un contenedor sin la opción -d, el terminal queda en modo attach (primer plano). Para ejecutar en segundo plano se utiliza -d (modo detached). Si se desea volver al primer plano de un contenedor que se encuentra en segundo plano debemos utilizar el comando attach junto con el ID del contenedor.

Ejemplo
1
2
3
4
5
# Obtener el ID del contenedor
docker ps

# Volver al primer plano
docker attach <id>

Acceso a contenedores mediante mapeo de puertos


Mapeo de puertos

El mapeo de puertos, o port mapping, asigna un puerto específico del host al puerto de un contenedor, lo que permite que una aplicación dentro del contenedor sea accesible desde el host o desde otros contenedores.

Ejemplo

El siguiente comando crea un contenedor de MongoDB y mapea el puerto 27017 del host al puerto 27017 del contenedor:

docker container create -p 27017:27017 --name mongodb mongo

En este comando, -p mapea un puerto del host al puerto del contenedor, mongodb es el nombre del contenedor y mongo es la imagen utilizada.

Crear e iniciar un contenedor

El comando docker run combina los comandos docker create y docker start, realizando los siguientes pasos:

  1. Busca la imagen especificada. Si no está disponible localmente, la descarga del repositorio.
  2. Crea un contenedor a partir de la imagen e inicia el contenedor.
Ejemplo

El siguiente ejemplo ejecuta un contenedor de MongoDB en segundo plano mapeando el puerto 27017:

docker run -d -p 27017:27017 --name mongodb mongo

Variables de entorno

Para conectar una base de datos con una aplicación dentro de Docker, se utilizan variables de entorno específicas para la imagen del contenedor.

Ejemplo

El siguiente ejemplo crea un contenedor de MongoDB con credenciales de administrador:

docker create -e MONGO_INITDB_ROOT_USERNAME=<usuario> -e MONGO_INITDB_ROOT_PASSWORD=<contraseña> mongo

Estas variables configuran el usuario y la contraseña del administrador de la base de datos durante la inicialización del contenedor. Es importante revisar la documentación de cada imagen, ya que las variables de entorno varían según la imagen utilizada.

Dockerfile

Un Dockerfile es un archivo de texto con instrucciones que permiten construir una imagen Docker personalizada. Cada imagen se construye sobre una imagen previa, que puede ser oficial de Docker o una personalizada.

Ejemplo
# Imagen base
FROM node:18

# Crear un directorio para el código
RUN mkdir -p /home/app

# Copiar los archivos del host al contenedor
COPY . /home/app

# Exponer el puerto de la aplicación
EXPOSE 3000

# Ejecutar la aplicación
CMD ["node", "/home/app/index.js"]

Para construir una imagen a partir de un Dockerfile se utiliza el siguiente comando:

docker build -t nombre-imagen:etiqueta ruta/dockerfile

ENTRYPOINT y CMD

ENTRYPOINT define el comando base del contenedor, mientras que CMD proporciona argumentos por defecto que pueden sobreescribirse al ejecutar el contenedor.

Ejemplo

Teniendo el siguiente Dockerfile:

1
2
3
FROM ubuntu
ENTRYPOINT ["sleep"]
CMD ["5"]

Podemos utilizar el siguiente comando para hacer un sleep de 5 segundos, que es el valor por defecto definido en la imagen:

docker run <imagen>

O podemos modificar el valor:

docker run <imagen> 10

Redes en Docker

Para permitir la comunicación entre contenedores, es necesario configurar una red interna. Docker permite crear redes personalizadas con el comando docker network create mi-nueva-red, y los contenedores que pertenecen a la misma red pueden comunicarse entre sí utilizando su nombre como dominio. Para crear un contenedor en una red específica podemos utilizar el siguiente comando:

docker create -p 27017:27017 --name mongodb --network mi-nueva-red mongo

Docker ofrece diferentes modos de red:

Tipo Descripción
bridge Red por defecto. Docker asigna IPs internas a cada contenedor. Los contenedores pueden comunicarse entre sí y se accede desde el host mediante mapeo de puertos.
host El contenedor usa directamente la red del host, sin aislamiento de red.
none Sin conectividad de red, completamente aislado.

Además, se pueden crear redes personalizadas que permiten especificar el rango de direcciones IP y otros parámetros. En lugar de usar --link (considerado legacy), se recomienda crear redes definidas por el usuario, que proporcionan resolución de nombres interna (los contenedores se referencian por nombre en lugar de por IP) y conectividad automática entre todos los contenedores de la misma red.

Docker Compose

Docker Compose es una herramienta que permite definir y gestionar múltiples contenedores como un conjunto de servicios interconectados. Utiliza un archivo de configuración docker-compose.yml en formato YAML para especificar la configuración de los servicios, redes, volúmenes y otros aspectos relacionados con los contenedores, simplificando la gestión de aplicaciones complejas compuestas por varios contenedores.

Ejemplo

En este ejemplo se definen dos servicios: uno para la aplicación (mi-app), que se construye a partir del contexto del directorio actual y mapea el puerto 3000, y otro para MongoDB (mongodb), que utiliza una imagen preexistente, mapea el puerto 27017 y establece las credenciales de acceso mediante variables de entorno.

version: "3.9"

services:
  mi-app:
    build: .
    ports:
      - "3000:3000"
    links:
      - mongodb

  mongodb:
    image: mongo
    ports:
      - "27017:27017"
    environment:
      - MONGO_INITDB_ROOT_USERNAME=<usuario>
      - MONGO_INITDB_ROOT_PASSWORD=<contraseña>

Para iniciar los servicios definidos en el archivo se ejecuta docker compose up, que descarga las imágenes necesarias, crea los contenedores y los pone en funcionamiento.

Para detener y eliminar los servicios, incluidos los contenedores, redes y volúmenes asociados, se utiliza docker compose down. Otros comandos útiles son docker-compose scale servicio=num_instancias para escalar servicios y docker-compose logs servicio para consultar los registros.

Se recomienda consultar el historial de versiones de Docker Compose para conocer las diferencias de sintaxis y mejoras entre versiones.

Persistencia de datos con volúmenes

En Docker, los volúmenes permiten la persistencia de datos en los contenedores. Esto significa que, incluso si un contenedor se elimina, los datos asociados a los volúmenes permanecen disponibles, lo cual resulta especialmente útil cuando se desea mantener información a través de reinicios o actualizaciones de contenedores.

Los volúmenes pueden ser de diferentes tipos. Los volúmenes anónimos carecen de nombre y no pueden referenciarse explícitamente desde otros contenedores. Los volúmenes de host permiten especificar qué carpeta del sistema anfitrión se monta dentro del contenedor. Los volúmenes nombrados disponen de un nombre y pueden referenciarse en otros contenedores o en múltiples servicios.

Para montar un volumen directamente desde la línea de comandos se utiliza la opción -v.

Ejemplo

Para mapear el directorio del host al directorio del contenedor, podemos utilizar el comando:

docker run -v ./Disco:/home/disco -it ghcr.io/astral-sh/uv:debian bash

Warning

Para añadir un volumen a un contenedor existente, es necesario recrear el contenedor con el comando del volumen.

También podemos definir docker-compose.yml con volúmenes.

Ejemplo

En este ejemplo, el servicio mongodb utiliza un volumen nombrado llamado mongo-data para almacenar los datos persistentes de la base de datos. Este volumen se monta en el directorio /data/db del contenedor, lo que asegura que los datos de MongoDB se conserven incluso si el contenedor es detenido o eliminado. Docker se encarga de gestionar la creación y almacenamiento de dicho volumen de forma completamente automatizada.

version: "3.9"

services:
  mi-app:
    build: .
    ports:
      - "3000:3000"
    links:
      - mongodb

  mongodb:
    image: mongo
    ports:
      - "27017:27017"
    environment:
      - MONGO_INITDB_ROOT_USERNAME=<usuario>
      - MONGO_INITDB_ROOT_PASSWORD=<contraseña>
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:

Todos los ficheros de Docker se encuentran en /var/lib/docker. Docker cachea las capas intermedias de las imágenes, incluso entre diferentes Dockerfiles, lo que ahorra espacio y tiempo de construcción. En ese mismo directorio podemos ver los volumenes disponibles.

Registros de imágenes

Existen registros públicos y privados para almacenar y gestionar imágenes de Docker:

Registro Tipo Características clave
Docker Hub Público/Privado El más grande, con imágenes oficiales y despliegues automatizados
Amazon ECR Gestionado (AWS) Integración con ECS, EKS y Fargate, con pago por uso
Azure Container Registry Gestionado (Azure) Geo-replicación y soporte de Helm charts
Google Artifact Registry Gestionado (GCP) Sucesor de GCR, con escaneo de vulnerabilidades y gestión IAM
GitHub Container Registry Gestionado Integrado con GitHub Actions para CI/CD
Harbor Open source RBAC y firmado de imágenes
JFrog Artifactory Universal Soporta Docker, Helm y otros formatos