Docker fue presentado en marzo de 2013 por Solomon Hykes en PyCon como una herramienta para desarrolladores, la cual permite empaquetar e implementar fácilmente aplicaciones dentro de un contenedor.
Hykes describió a los contenedores como: unidades autónomas de software aisladas a nivel de proceso que empaquetan sus propias dependencias y sistema de archivos. Esto permite desplegarlos de forma coherente en diferentes servidores e infraestructuras compatibles, independientemente de las librerías instaladas en el sistema anfitrión.
Desde entonces, Docker se ha convertido en el estándar de facto para la contenerización, transformando la forma en que desarrollamos, probamos y desplegamos software.
¿Cómo funciona Docker?
Los contenedores ofrecen un mecanismo de aislamiento diferente al de las máquinas virtuales y, en muchos escenarios, pueden complementar o sustituir a las VMs. Y esto es debido a los múltiples beneficios que brindan, entre ellos está que los contenedores son mucho más livianos que las VMs. Docker es la opción más común para implementar contenedores, sin embargo, existen más opciones como Podman, LXD, entre otros.
En la siguiente imagen, podemos ver que cada máquina virtual tiene su propio Sistema Operativo (SO) invitado por encima del Host, a diferencia de los contenedores que comparten el SO del anfitrión (Host) y es por eso que son más livianos.
El hecho de que los contenedores no usen un SO completo ayuda a que sean más livianos, es decir, reduce de gran manera la carga que soporta la máquina anfitriona, además, el espacio de almacenamiento es menor ya que solamente utiliza lo mínimo para funcionar, y el tiempo que necesita para lanzar una aplicación es considerablemente menor a las máquinas virtuales.
Diferencias entre Contenedores y VMs
Las máquinas virtuales nos permiten crear entornos con sistemas operativos totalmente aislados del sistema operativo host, obteniendo así un control total sobre el sistema operativo invitado, de esta manera se puede mezclar diferentes sistemas operativos entre el host y los invitados.
Por otro lado, los contenedores además de permitirnos desplegar aplicaciones de manera más rápida también ayudan a crear entornos de desarrollo replicables entre los diferentes miembros de un equipo de desarrollo y así asegurar que se ejecutará de la misma manera en todos los equipos, independientemente del sistema operativo cada integrante del equipo tendrá el ambiente funcional.
En la siguiente tabla se presentan las diferencias entre contenedores y VMs:
| Característica | Contenedores | Máquinas Virtuales |
|---|---|---|
| Despliegue | En segundos | En minutos |
| Tamaño | Ligeros (KB – MB) | Pesadas (MB – GB) |
| Sistema Operativo | Compartido con el host | Independiente (SO invitado completo) |
| Recursos | Generalmente menor overhead | Mayor overhead |
| Tiempo de arranque | Segundos | Minutos |
| Replicabilidad | Fácil (imágenes versionables) | Moderada (requiere snapshots) |
| Aislamiento | Kernel compartido, aislamiento de procesos | Kernel y SO virtualizados |
Arquitectura de Docker
Docker utiliza la arquitectura cliente-servidor, en donde el cliente de Docker se comunica con el demonio de Docker para crear, ejecutar o distribuir contenedores. Además, mediante un API REST se pueden acceder a imágenes de contenedor ubicadas en repositorios de imágenes (Registry).
Client
El cliente de Docker (CLI) es la interfaz principal que usan los desarrolladores. A través de comandos como docker run, docker build, docker pull, el usuario envía instrucciones al demonio de Docker.
Docker Host
Es el entorno donde se ejecuta el demonio de Docker (dockerd). Aquí residen:
- Imágenes: Plantillas de solo lectura usadas para crear contenedores.
- Contenedores: Instancias en ejecución de una imagen.
- Volúmenes: Almacenamiento persistente para datos.
- Redes: Conectividad entre contenedores y con el exterior.
Imágenes
Los contenedores de Docker son creados a partir de imágenes, que son una especia de plantilla para la creación del contendor.
Registry
Los registros almacenan y distribuyen imágenes. Docker Hub es el registro público predeterminado, pero existen alternativas como GitHub Container Registry, Amazon ECR, Google Container Registry y registros privados.
Dockerfile: El corazón de Docker
Un Dockerfile es un archivo de texto plano con instrucciones para construir una imagen Docker. Las instrucciones que modifican el filesystem pueden generar capas reutilizables en el proceso de construcción, lo que permite aprovechar la caché.
Ejemplo básico de Dockerfile:
# Imagen base
FROM node:20-alpine
# Directorio de trabajo
WORKDIR /app
# Copiar dependencias
COPY package*.json ./
# Instalar dependencias
RUN npm install
# Copiar código fuente
COPY . .
# Exponer puerto
EXPOSE 3000
# Comando de inicio
CMD ["npm", "start"]
Instrucciones principales
| Instrucción | Propósito |
|---|---|
| FROM | Define la imagen base (ej: node:20-alpine, python:3.12) |
| WORKDIR | Establece el directorio de trabajo dentro del contenedor |
| COPY | Copia archivos del host al contenedor |
| RUN | Ejecuta comandos durante la construcción de la imagen |
| EXPOSE | Documenta el puerto que usa la aplicación |
| CMD | Define el comando por defecto al ejecutar el contenedor |
| ENV | Establece variables de entorno |
| ARG | Define variables de build-time |
Docker Compose
Docker Compose permite definir y ejecutar aplicaciones multi-contenedor usando un archivo YAML (compose.yaml o docker-compose.yml).
Ejemplo de una aplicación web con base de datos:
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:<version>-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Para levantar todos los servicios: docker compose up -d
Comandos esenciales de Docker
| Comando | Descripción |
|---|---|
| docker pull (imagen) | Descarga una imagen del registro |
| docker run (imagen) | Crea y ejecuta un contenedor |
| docker ps | Lista contenedores en ejecución |
| docker ps -a | Lista todos los contenedores |
| docker stop (id) | Detiene un contenedor |
| docker rm (id) | Elimina un contenedor |
| docker images | Lista imágenes locales |
| docker rmi (imagen) | Elimina una imagen |
| docker build -t (nombre) . | Construye una imagen desde un Dockerfile |
| docker logs (id) | Muestra los logs de un contenedor |
| docker exec -it (id) sh | Accede a un contenedor en ejecución |
| docker compose up -d | Levanta servicios definidos en compose.yaml |
| docker compose down | Detiene y elimina los servicios |
Buenas prácticas con Docker
Imágenes ligeras
Se puede utilizar imágenes base como Alpine (-alpine) para reducir el tamaño y la superficie de ataque. Una imagen node:<version>-alpine pesa menos que node:<version>.
Multi-stage builds
Permite construir la aplicación en una imagen con herramientas de desarrollo y copiar solo el artefacto final a una imagen más pequeña.
# Etapa de construcción
FROM node:<version>-alpine AS builder
WORKDIR /app
COPY . .
RUN npm run build
# Etapa de producción
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
No ejecutar como root
Crea un usuario no privilegiado dentro del contenedor:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
.dockerignore
Excluye archivos innecesarios para reducir el contexto de construcción:
node_modules
.git
.env
*.md
Un solo proceso por contenedor
Un proceso principal por contenedor cuando sea posible. Si necesitas varios servicios independientes, normalmente es preferible separarlos en contenedores diferentes y coordinarlos con Compose u otra plataforma.
Cuándo usar contenedores
Docker facilita el empaquetado y distribución de aplicaciones, incluyendo arquitecturas basadas en microservicios, por lo que es ideal para:
- Arquitecturas de microservicios: Cada servicio en su propio contenedor, comunicándose por red.
- Entornos de desarrollo replicables: Todo el equipo usa la misma configuración.
- Pipelines de CI/CD: Construir, probar y desplegar usando la misma imagen.
- Aislamiento de aplicaciones: Ejecutar aplicaciones con dependencias conflictivas en el mismo host.
- Pruebas de integración: Levantar servicios dependientes (bases de datos, colas, etc.) temporalmente.
Conclusiones
- Docker facilita preparar un nuevo ambiente de trabajo independientemente de la máquina anfitriona.
- Los contenedores permiten arrancar aplicaciones rápidamente aprovechando los recursos de hardware.
- Los Dockerfile y archivos Compose pueden mantenerse bajo control de versiones, facilitando la reproducibilidad y trazabilidad de los entornos.
- Las prácticas como multi-stage builds y el uso de imágenes Alpine ayudan a mantener contenedores seguros y eficientes.
- Docker Compose simplifica la gestión de aplicaciones multi-contenedor.