Qué es Docker
Docker es una plataforma de código abierto que automatiza el despliegue de aplicaciones dentro de contenedores de software. Pero, ¿qué significa esto realmente?
Los contenedores son como cajas estandarizadas que pueden transportar cualquier tipo de carga. Esta analogía no es casual - Docker se inspiró en los contenedores de transporte marítimo, que revolucionaron la industria logística al proporcionar un formato estándar para mover mercancías.
En el mundo del software, los contenedores resuelven varios problemas críticos:
Problema | Solución con Docker |
---|---|
Inconsistencia entre entornos | Empaqueta todo lo necesario en un contenedor |
Conflictos de dependencias | Aislamiento completo entre aplicaciones |
Tiempo de configuración | Entornos reproducibles con un solo comando |
Recursos desperdiciados | Uso eficiente y compartido de recursos |
Conceptos básicos de Docker y contenedores
Qué son los contenedores
Un contenedor es una unidad estándar de software que empaqueta el código y todas sus dependencias. Imagina que cada aplicación viene en su propia “caja” con todo lo que necesita: sistema operativo, librerías, configuración, etc. Permíteme ilustrarlo con un ejemplo real:
En mi último proyecto, teníamos una aplicación Python que requería una versión específica de TensorFlow. El equipo de ciencia de datos necesitaba Python 3.8, mientras que el equipo de backend usaba Python 3.9. Sin contenedores, esto habría sido una pesadilla de configuración. Con Docker, cada equipo trabajaba en su propio contenedor, sin conflictos y con total independencia.
¿Qué problemas solucionamos con esto? Lo vemos en la siguiente tabla:
Dolor tradicional | Solución Docker |
---|---|
“En mi PC funciona” | Todo empaquetado en un contenedor |
“Se me rompió otra dependencia” | Aislamiento total entre apps |
Configurar entornos eternos | docker-compose up y listo |
Máquinas virtuales obesas | Contenedores ligeros y ágiles |
Cómo funciona Docker
Docker utiliza una arquitectura cliente-servidor donde:
-
Docker Daemon (
dockerd
) es el servidor que:- Gestiona contenedores
- Maneja imágenes
- Administra redes y almacenamiento
-
Docker Client (
docker
) es la interfaz de usuario que:- Acepta comandos
- Se comunica con el daemon
- Puede conectarse a múltiples daemons
Otra forma de ver estos primeros dos puntos es en la forma en que funciona un restaurante de lujo:
- El chef (
daemon
): Prepara los platos (contenedores), gestiona la cocina (recursos) y mantiene la despensa (imágenes) - El camarero (
cliente
): Toma tu pedido (comandos) y te sirve los resultados
Por otro lado, Docker también funciona con:
- Docker Hub: El registro público de imágenes Docker. ¿Necesitas una base de datos MongoDB?
docker pull mongo
- Docker Compose: Un archivo de configuración que define y ejecuta múltiples contenedores. ¿Tienes un backend, un frontend y una base de datos?
docker compose up
Veamos un ejemplo práctico de interacción:
- El cliente (docker) envía un comando al daemon (dockerd)
- El daemon ejecuta el comando y devuelve el resultado al cliente
$ docker version
Client: Docker Engine - Community
Version: 24.0.7
...
Server: Docker Engine - Community
Engine:
Version: 24.0.7
Aquí el cliente le pregunta al daemon: “Oye, ¿qué versión tienes?” Y el daemon responde. ¡Como un asistente virtual, pero para contenedores!
Elementos clave en Docker
Docker utiliza varios elementos clave para su funcionamiento:
-
Imágenes: Son plantillas de solo lectura que contienen:
- Sistema operativo base
- Runtime environment
- Dependencias de aplicación
- Configuración
-
Contenedores: Son instancias ejecutables de una imagen. Por ejemplo:
$ docker run -d nginx
6d7f219ed6f43c99b30f0df58d66b7d5c5513b61ac3b038f9d2e96c0a772ba22
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6d7f219ed6f4 nginx "/docker-entrypoint.…" 5 seconds ago Up 4 seconds 80/tcp friendly_euler
En este bloque de código, estamos creando un contenedor en segundo plano con la imagen de Nginx.
El comando docker run
nos permite crear un contenedor a partir de una imagen y ejecutar un comando en él. En este caso, estamos ejecutando el comando por defecto de Nginx, que es “/docker-entrypoint.sh” nginx -g ‘daemon off;’. Luego, estamos mostrando la lista de contenedores en ejecución con el comando docker ps
.
- Dockerfile: Define cómo construir una imagen. Ejemplo básico:
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
Ahora estamos definiendo un Dockerfile que construye una imagen personalizada para una aplicación Node.js. El Dockerfile comienza con una instrucción FROM
que indica que queremos partir de la imagen node:14
. Luego, creamos un directorio /app
en el contenedor y copiamos los archivos package.json
y package-lock.json
en él. Después, ejecutamos el comando npm install
para instalar las dependencias de la aplicación. Finalmente, copiamos el resto de los archivos de la aplicación en el contenedor y definimos el comando por defecto que se ejecutará cuando se inicie el contenedor, que es npm start
.
- Docker Hub: El registro público de imágenes Docker. ¿Necesitas una base de datos MongoDB? Tan simple como:
$ docker pull mongodb
Using default tag: latest
latest: Pulling from library/mongodb
a603fa5e3b41: Pull complete
c428f1a41b1c: Pull complete
... [más capas]
Status: Downloaded newer image for mongodb:latest
Por último, estamos descargando la imagen de MongoDB de Docker Hub. El comando docker pull
nos permite descargar una imagen de Docker Hub. En este caso, estamos descargando la imagen de MongoDB.
Para profundizar aún más en estos conceptos, te recomiendo nuestro Curso de introducción a Docker, donde aprenderás desde los fundamentos hasta las mejores prácticas.
Aprende a desarrollar webs optimizadas
Comienza 15 días gratis en OpenWebinars y accede cursos, talleres y laboratorios prácticos de JavaScript, React, Angular, HTML, CSS y más.
Ventajas del uso de Docker
Portabilidad
La portabilidad es uno de los beneficios más significativos de Docker. En mi experiencia como consultor DevOps, he visto cómo esta característica ha salvado innumerables horas de trabajo.
Por ejemplo, una empresa de e-commerce con la que trabajé tenía problemas para migrar su aplicación entre diferentes proveedores cloud. Con Docker, logramos hacer la migración de AWS a Google Cloud en cuestión de días, no semanas como inicialmente se había estimado.
Eficiencia
La eficiencia en Docker se manifiesta de varias maneras:
Aspecto | Beneficio | Ejemplo Real |
---|---|---|
Uso de recursos | Menor overhead que VMs | 50% menos uso de memoria |
Tiempo de inicio | Arranque casi instantáneo | De minutos a segundos |
Almacenamiento | Sistema de capas eficiente | Reducción del 60% en espacio |
Desarrollo | Entornos consistentes | Reducción de bugs en producción |
Despliegue rápido
En una startup de fintech, redujimos el tiempo de despliegue de 45 minutos a apenas 3 minutos utilizando Docker. Aquí está cómo lo logramos:
# Construir y etiquetar la imagen
docker build -t miapp . # Construye la imagen
docker service update --image miapp miservicio # Actualiza en vivo
Escalabilidad
La escalabilidad con Docker es excepcional. En un proyecto reciente para una plataforma de e-learning, pudimos escalar de 1,000 a 100,000 usuarios sin cambios en la arquitectura. La clave fue la capacidad de Docker para:
- Escalar horizontalmente
- Balancear carga automáticamente
- Manejar picos de tráfico
Un ejemplo de escalabilidad con Docker:
# Escalar el servicio
$ docker service scale miservicio=100
En este bloque de código, estamos escalando un servicio con el comando docker service scale
. En este caso, estamos escalando el servicio miservicio
a 100 instancias. Esto nos permite manejar picos de tráfico y escalar horizontalmente.
Pero ojo: escalar sin control es como añadir motores a un coche sin mejores frenos. Necesitas monitorización y balanceo inteligente.
Casos prácticos de uso de Docker
Desarrollo local
El desarrollo local con Docker ha transformado cómo los equipos trabajan.
Ejemplo real de un proyecto web:
# Estructura típica
myapp/
├── src/
├── tests/
├── Dockerfile
├── docker-compose.yml
└── .dockerignore
# Iniciar el entorno
$ docker-compose up -d
Creating network "myapp_default" with the default driver
Creating myapp_db_1 ... done
Creating myapp_redis_1 ... done
Creating myapp_web_1 ... done
# Verificar servicios
$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------
myapp_db_1 docker-entrypoint.sh mongod Up 27017/tcp
myapp_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
myapp_web_1 npm start Up 0.0.0.0:3000->3000/tcp
El Dockerfile
y el docker-compose.yml
definen la configuración del entorno de desarrollo. Se vería de la siguiente manera:
# Dockerfile
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
# docker-compose.yml
version: '3'
services:
web:
build: .
ports:
- "3000:3000"
db:
image: mongo
ports:
- "27017:27017"
redis:
image: redis
ports:
- "6379:6379"
Esto nos permite iniciar un entorno de desarrollo con docker-compose up -d
y detenerlo con docker-compose down
. Para poder acceder a los endpoints de la aplicación, podemos usar localhost:3000
.
Integración continua
La integración continua con Docker ha transformado cómo validamos y probamos nuestro código. En un proyecto reciente para una institución financiera, implementamos un pipeline de CI (con GitHub Actions) que redujo los falsos positivos en testing en un 80%. El secreto fue la consistencia del entorno de pruebas.
Ejemplo de un pipeline básico de CI:
- Esto está hecho en GitHub Actions pero puedes adaptarlo a cualquier otro sistema de CI.
name: CI Pipeline
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build test image
run: docker build -t myapp:test -f Dockerfile.test .
- name: Run unit tests
run: |
docker run --rm myapp:test npm run test
- name: Run integration tests
run: |
docker-compose -f docker-compose.test.yml up \
--abort-on-container-exit \
--exit-code-from test
Básicamente lo que hace es:
- Construir una imagen de pruebas
- Ejecutar pruebas unitarias
- Ejecutar pruebas de integración
Si sale todo bien, el pipeline se detiene. Si sale mal, el pipeline se detiene y envía un correo de alerta.
Resultado típico de la ejecución:
$ docker run --rm myapp:test npm run test
> myapp@1.0.0 test
> jest --coverage
PASS tests/api.test.js
PASS tests/auth.test.js
PASS tests/user.test.js
Test Suites: 3 passed, 3 total
Tests: 24 passed, 24 total
Snapshots: 0 total
Time: 3.45 s
Esto significa que todo está bien, el pipeline se detiene.
Despliegue en la nube
El despliegue en la nube con Docker es uno de los casos de uso más potentes. En un proyecto reciente, necesitábamos desplegar la misma aplicación en múltiples regiones geográficas. Docker hizo esto posible con un esfuerzo mínimo.
Ejemplo de despliegue en diferentes proveedores cloud:
Proveedor | Servicio | Comando de despliegue |
---|---|---|
AWS | ECS | aws ecs update-service --force-new-deployment |
Google Cloud | Cloud Run | gcloud run deploy --image gcr.io/myapp |
Azure | ACI | az container create --image myapp:latest |
Hay muchos más que se pueden usar, como Heroku, DigitalOcean, etc.
Script de despliegue automatizado:
#!/bin/bash
# deploy.sh
# Variables de entorno
DOCKER_IMAGE="myapp:${VERSION:-latest}"
DEPLOY_ENV="${ENV:-staging}"
# Construir y pushear imagen
echo "🏗️ Construyendo imagen: $DOCKER_IMAGE"
docker build -t $DOCKER_IMAGE .
docker push $DOCKER_IMAGE
# Desplegar en el ambiente correspondiente
echo "🚀 Desplegando en $DEPLOY_ENV"
case $DEPLOY_ENV in
"staging")
docker stack deploy -c docker-compose.staging.yml myapp-staging
;;
"production")
docker stack deploy -c docker-compose.prod.yml myapp-prod
;;
esac
# Verificar despliegue
echo "✅ Verificando despliegue..."
docker service ls --filter name=myapp
Esto lo que hace es desplegar la aplicación en el ambiente correspondiente, donde staging
es el ambiente de pruebas y production
es el ambiente de producción.
Microservicios
La arquitectura de microservicios es donde Docker realmente brilla. Permíteme compartir un caso real: transformamos un monolito de comercio electrónico en microservicios, reduciendo el tiempo de despliegue de nuevas características de semanas a horas.
Estructura típica de un proyecto de microservicios:
ecommerce/
├── services/
│ ├── users/
│ │ ├── Dockerfile
│ │ └── src/
│ ├── products/
│ │ ├── Dockerfile
│ │ └── src/
│ └── orders/
│ ├── Dockerfile
│ └── src/
├── docker-compose.yml
└── nginx/
└── nginx.conf
Docker Compose para orquestar los servicios:
version: '3.8'
services:
users:
build: ./services/users
environment:
- DB_HOST=users_db
depends_on:
- users_db
products:
build: ./services/products
environment:
- CACHE_HOST=redis
depends_on:
- redis
orders:
build: ./services/orders
environment:
- KAFKA_BROKERS=kafka:9092
depends_on:
- kafka
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
Esto lo que hace a detalle es desplegar los microservicios en contenedores, donde users
es el servicio de usuarios, products
es el servicio de productos y orders
es el servicio de pedidos, con nginx
como proxy inverso.
Además, podemos ver que en el docker-compose.yml
se definen las variables de entorno, las dependencias, los puertos y los volúmenes.
Mejores prácticas al usar Docker
A continuación, detallo las mejores prácticas al usar Docker. Donde ahondamos en el uso eficiente de imágenes, la gestión de volúmenes y la optimización del despliegue.
Uso eficiente de imágenes
La optimización de imágenes es crucial para el rendimiento. En un proyecto de IoT, redujimos el tamaño de nuestras imágenes de 1.2GB a 85MB siguiendo estas prácticas:
- Multi-stage builds:
Este es un patrón donde se divide el proceso de construcción en dos etapas: una para construir y otra para producir. La ventaja es que la imagen final es más ligera y contiene solo los archivos necesarios.
# Etapa de construcción
FROM node:14 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Etapa de producción
FROM node:14-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm install --production
CMD ["npm", "start"]
- Optimización de capas:
Es importante optimizar las capas de las imágenes para reducir el tiempo de construcción y el tamaño final.
Práctica | Impacto |
---|---|
Combinar comandos RUN | Reduce el número de capas |
.dockerignore | Excluye archivos innecesarios |
Ordenar operaciones | Mejora el uso de caché |
La optimización siempre es importante para el rendimiento de las imágenes.
Gestión de volúmenes
La gestión efectiva de volúmenes es esencial para la persistencia de datos. En producción, implementamos esta estrategia:
version: '3.8'
services:
postgres:
image: postgres:13
volumes:
- pgdata:/var/lib/postgresql/data # Volumen nombrado
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro # Volumen bind
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
secrets:
- pg_password
volumes:
pgdata: # Docker gestiona este volumen
Esto hace que los datos persistan entre las ejecuciones de los contenedores, lo que es esencial para la persistencia de datos. Además, los volúmenes nombrados son manejados por Docker, lo que facilita la gestión y la limpieza.
Comandos útiles para la g