Blog.

Docker: Rapido y Furioso

Cover Image for Docker: Rapido y Furioso
Adrian Castillo
Adrian Castillo

TL;DR

Docker te permite empaquetar aplicaciones en contenedores ligeros que pueden ejecutarse en cualquier sistema sin preocuparte por dependencias. Con Docker Compose, puedes definir múltiples servicios (como una API y su base de datos) en un solo archivo docker-compose.yml. Este blogpost es una referencia rápida para recordarme cómo usar Docker de manera efectiva sin perder tiempo buscando documentación.

🐳 Introducción rápida a Docker

🏗 Containerización vs Virtualización

La containerización no es lo mismo que la virtualización. Mientras que una máquina virtual (VM) necesita un sistema operativo completo, un contenedor comparte el mismo kernel del host, haciendo que sea más liviano y rápido. Básicamente, las VMs son como casas independientes, mientras que los contenedores son departamentos en el mismo edificio, compartiendo algunos recursos pero aislados entre sí.

🔑 Docker: La Magia de la Containerización

Imagina que estás desarrollando una aplicación con dependencias específicas. Configurar el entorno correcto en cada máquina puede ser un dolor de cabeza. Aquí es donde Docker brilla, empaquetando todo en contenedores que aseguran que tu aplicación se ejecute de manera idéntica en cualquier lugar.

🔗 Complemento con notdefined-workbench

Si quieres ejemplos listos para levantar y probar diferentes tecnologías con Docker, revisa el repositorio notdefined-workbench. Ahí encontrarás configuraciones listas con docker-compose.yml para múltiples stacks.

🔑 Problemas que Docker resuelve

¿Aún sigues confundido? Hagamos esto más práctico. ¿Has pasado por alguno de estos problemas?

  • "En mi máquina funciona, pero en producción no" → Docker encapsula todo, asegurando consistencia en cualquier entorno.
  • "Actualizar dependencias rompe otros servicios" → Cada contenedor tiene su propio entorno, sin interferencias, y puedes probar versiones antes de actualizar.
  • "Dificultad para compartir y mantener entornos de desarrollo" → Con solo compartir un Dockerfile o docker-compose.yml, cualquier equipo puede replicar tu entorno.
  • "Cada vez que agregan un nuevo servicio, tengo que reconfigurar mi máquina" → Docker Compose gestiona múltiples contenedores fácilmente, manteniendo todo sincronizado.

La clave de Docker es la containerización, empaquetando tu aplicación y todas sus dependencias en un contenedor. Esto garantiza que la aplicación se ejecute de manera consistente en cualquier máquina con Docker instalado.

No confundir con la virtualización, que crea múltiples entornos virtuales en una sola máquina física. Docker no virtualiza un SO, sino que usa el mismo kernel del sistema host para crear contenedores ligeros.

🔑 Conceptos clave en Docker

  • Imagen: Plantilla con código, librerías y configuraciones necesarias para crear un contenedor.
  • Contenedor: Instancia en ejecución de una imagen. Son aislados y predecibles.
  • Docker Engine: Servicio que gestiona la ejecución de contenedores.
  • Docker Hub: Registro público de imágenes de Docker.
  • Dockerfile: Archivo con instrucciones para construir una imagen Docker.
  • Volumen: Almacén de datos persistente para evitar pérdida de información al eliminar contenedores.
  • Red: Permite la comunicación entre contenedores y con el host.

🏗 Instalación de Docker (Mac/Linux)

Para instalar Docker en Linux o macOS, simplemente ejecuta:

curl -fsSL https://get.docker.com | bash

Para verificar que Docker se instaló correctamente:

docker --version

# Tambien puedes correr el hello-world
docker run hello-world

Ejemplo rápido para correr un contenedor de PostgreSQL:

docker run --name postgres -e POSTGRES_PASSWORD=secret -d postgres

Para ver los contenedores en ejecución:

docker ps

🎉 ¡Felicidades, tu primer contenedor está corriendo! ¿Ahora qué?

Puedes conectarte a PostgreSQL dentro del contenedor, ver logs o persistir los datos usando volúmenes.

Para conectarte desde la terminal:

docker exec -it postgres psql -U postgres

Para ver logs:

docker logs postgres

Para evitar perder datos cuando detengas el contenedor:

docker stop postgres

docker rm postgres

docker run --name postgres -e POSTGRES_PASSWORD=secret -v pg_data:/var/lib/postgresql/data -d postgres

Si quieres conectarte con un cliente como psql desde tu máquina:

docker run --name postgres -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres

Luego puedes acceder con:

psql -h localhost -U postgres

📌 Ejemplo Completo

¿Quieres ensuciarte las manos aún más? Ahora hagamos un ejemplo más completo: vamos a crear una API en Node.js.

Este será nuestro proyecto:

mi-app/
├── app/
│   ├── Dockerfile
│   ├── index.js
│   ├── package.json

index.js

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello from Docker!');
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});

package.json

{
  "name": "app",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "express": "^4.21.2"
  }
}

Dockerfile

# Usa la imagen oficial de Node.js
FROM node:18

# Establece el directorio de trabajo dentro del contenedor
WORKDIR /usr/src/app

# Copia los archivos necesarios
COPY package*.json ./

# Instala las dependencias
RUN npm install

# Copia el resto del código de la aplicación
COPY . .

# Expone el puerto 3000 para la API
EXPOSE 3000

# Comando de inicio
CMD ["node", "index.js"]

🔹 Construcción y Ejecución del Contenedor

Para comenzar, primero tenemos que generar la imagen. Para ello, ejecutamos los siguientes comandos desde el directorio app (sin docker-compose por ahora):

docker build -t mi-app .
docker run -p 3000:3000 mi-app

Ahora puedes visitar http://localhost:3000 para ver el servidor en funcionamiento. Para detener el contenedor:

docker stop <CONTAINER_ID>

🔹 Usando docker-compose

Ahora probemos usando docker-compose. Vamos a crear el archivo docker-compose.yml fuera del directorio app, así:

mi-app/
├── docker-compose.yml
├── app/
│   ├── Dockerfile
│   ├── index.js
│   ├── package.json

docker-compose.yml

services:
  app:
    build: ./app
    container_name: node_app
    ports:
      - "3000:3000"
    networks:
      - backend
    volumes:
      - ./app:/usr/src/app

networks:
  backend:

volumes:
  pg_data:

Ahora podemos iniciar todo con un solo comando mucho más fácil de recordar:

docker-compose up

Muy bonito, pero nada impresionante... Bueno, veamos una configuración más avanzada con base de datos y caché:

services:
  db:
    image: postgres
    container_name: postgres_db
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydatabase
    volumes:
      - pg_data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "user"]
      interval: 10s
      retries: 5
      timeout: 5s

  redis:
    image: redis:latest
    container_name: redis_cache
    networks:
      - backend

  app:
    build: ./app
    container_name: node_app
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://user:password@db:5432/mydatabase
    ports:
      - "3000:3000"
    networks:
      - backend
    volumes:
      - ./app:/usr/src/app

networks:
  backend:

volumes:
  pg_data:

Después de unos segundos de magia:

docker-compose up

¡Voilà! Ahora tenemos tres contenedores corriendo y comunicándose entre sí (docker ps):

CONTAINER ID   IMAGE          COMMAND
4c82eb429297   mi-app-app     "docker-entrypoint.s…"
9a37acac283f   postgres       "docker-entrypoint.s…"
7b0c27402fb5   redis:latest   "docker-entrypoint.s…"

Empiezas a ver los beneficios, ¿verdad? ¡Estupendo! Ahora imagina tus proyectos corriendo con un solo comando. Alucinante. No olvides detener docker-compose, y si quieres jugar con los archivos, puedes obtenerlos desde el repositorio notdefined-benchmark.

📦 Volúmenes en Docker

Los volúmenes en Docker permiten persistir datos más allá del ciclo de vida de un contenedor. Algunas cosas clave que debes saber:

  • Persistencia de bases de datos: Evita que los datos se borren al eliminar un contenedor.
  • Compartición de datos entre contenedores: Dos o más contenedores pueden acceder a un mismo volumen.
  • Optimización del rendimiento: Los volúmenes son más eficientes que los bind mounts.

Ejemplo de creación de un volumen y asignación a PostgreSQL:

docker run --name postgres -e POSTGRES_PASSWORD=secret -v pg_data:/var/lib/postgresql/data -d postgres

Para listar volúmenes:

docker volume ls

Para eliminar volúmenes no usados:

docker volume prune

🌐 Redes en Docker

Docker permite la comunicación entre contenedores a través de redes personalizadas.

Ejemplo de creación de una red y conexión de contenedores:

docker network create mi_red

docker run -d --name db --network=mi_red -e POSTGRES_PASSWORD=secret postgres

docker run -it --rm --network=mi_red alpine sh

Dentro del contenedor Alpine, puedes probar la conexión:

apk add --no-cache postgresql-client
psql -h db -U postgres

📉 Optimización de Imágenes en Docker

La optimización de imágenes en Docker es clave para reducir su tamaño, mejorar la seguridad y acelerar despliegues. Aplicamos múltiples estrategias para mejorar nuestras imágenes, pero alcanzamos un punto donde ya no había mejoras en tamaño. Esto indica que hemos eliminado lo innecesario y tenemos una imagen eficiente.

Hay varias estrategias esta lista es algunas de ellas:

  • Cambio a versiones alpine como node:18-alpine para reducir el tamaño base.
  • Uso de multi-stage builds para copiar solo lo esencial a la imagen final (ejemplo no node_modules).
  • Eliminación de dependencias innecesarias con npm install --only=production.
  • Minimización de capas en el Dockerfile.
  • Uso de .dockerignore para evitar archivos innecesarios en la imagen final.

Después de aplicar estas optimizaciones, construimos diferentes versiones y verificamos su tamaño con:

docker images | grep mi-app

Por ejemplo este Dockerfile optimizado:

# Etapa 1: Builder
FROM node:18 AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .

# Etapa 2: Producción
FROM node:18-alpine
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/index.js ./
EXPOSE 3000
CMD ["node", "index.js"]

Nos da estos resultados:

mi-app-original    latest    abc123456789   5 minutes ago   500MB
mi-app-opt         latest    def987654321   3 minutes ago   150MB

📡 Logs & Monitoring en Docker

Porque ir a ciegas no es divertido, aquí tienes algunos comandos útiles para ver qué está pasando antes de quedarte pelón como Toretto.

🔹 Ver logs de contenedores en tiempo real

docker logs -f nombre_del_contenedor

Esto es útil para monitorear la API en vivo sin necesidad de entrar al contenedor.

🔹 Monitoreo de recursos en vivo

docker stats

Muestra el consumo de CPU, memoria y red de los contenedores en ejecución.

🔹 Acceder a un contenedor en ejecución

docker exec -it nombre_del_contenedor sh

Si tiene bash, puedes usar:

docker exec -it nombre_del_contenedor bash

🔹 Detectar contenedores fallidos

Si un contenedor se apaga inesperadamente:

docker ps -a

Si el código de salida es 1, revisa los logs:

docker logs nombre_del_contenedor

🔹 Configurar logs en docker-compose.yml

services:
  app:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Esto evita que los logs ocupen demasiado espacio.

🔹 Ver logs de todos los servicios en docker-compose

docker-compose logs -f

Facilita la depuración cuando hay múltiples servicios.

🛠 Debugging en Docker

Mientras usemos Docker, no todo va a salir como esperamos. Para esos casos, hay que saber cómo depurar. Aquí tengo unos tips para cuando ese momento llegue, y llegará.

🔹 Ver contenedores en ejecución

docker ps -a

🔹 Revisar logs de un contenedor

docker logs nombre_del_contenedor

🔹 Acceder a un contenedor en ejecución

docker exec -it nombre_del_contenedor sh

🔹 Reconstruir imágenes desde cero

docker-compose build --no-cache

🔹 Ver códigos de salida

Si un contenedor sale inmediatamente después de ejecutarse, revisa su código de salida:

docker ps -a

Si el estado es Exited (0), significa que el proceso terminó correctamente, pero no tenía un proceso en segundo plano para mantenerlo vivo.

🔹 Limpiar Docker de imágenes y contenedores no usados

docker system prune -a -f

🔥 Limpiar el entorno de Docker

A medida que usas Docker, los contenedores, imágenes, volúmenes y redes pueden acumularse y ocupar espacio innecesario. Aquí te dejo algunos comandos esenciales para limpiar tu entorno de Docker.

🔹 Detener y eliminar contenedores

Para detener todos los contenedores en ejecución:

docker stop $(docker ps -q)

Para eliminar todos los contenedores detenidos:

docker rm $(docker ps -aq)

🔹 Eliminar imágenes innecesarias

Para eliminar imágenes que ya no están en uso:

docker image prune -a

Si necesitas eliminar una imagen específica:

docker rmi nombre_imagen

🔹 Limpiar volúmenes sin uso

Para eliminar volúmenes que no están en uso:

docker volume prune

Si quieres eliminar un volumen específico:

docker volume rm nombre_volumen

🔹 Eliminar redes no utilizadas

Si tienes redes creadas que ya no usas:

docker network prune

Para eliminar una red específica:

docker network rm nombre_red

🔹 Limpiar todo el sistema Docker

Si quieres hacer una limpieza completa de contenedores, imágenes, volúmenes y redes no utilizadas, usa:

docker system prune -a

Advertencia: Esto eliminará todo lo que no esté en uso por un contenedor en ejecución.

🔥 Comandos esenciales de Docker

Comando Descripción
docker build -t <image_name> . Construye una imagen a partir de un Dockerfile
docker run -p <host_port>:<container_port> <image_name> Ejecuta un contenedor basado en una imagen
docker ps Lista los contenedores en ejecución
docker ps -a Lista todos los contenedores, incluidos los detenidos
docker images Muestra las imágenes descargadas
docker logs <container> Muestra los logs de un contenedor
docker stop <container> Detiene un contenedor en ejecución
docker rm <container> Elimina un contenedor detenido
docker rm $(docker ps -aq) Elimina todos los contenedores detenidos
docker rmi <image> Elimina una imagen específica
docker image prune -a Elimina todas las imágenes sin uso
docker volume ls Lista los volúmenes existentes
docker volume prune Elimina los volúmenes sin uso
docker network ls Lista las redes de Docker
docker network prune Elimina las redes sin uso
docker system prune -a Limpia imágenes, contenedores, volúmenes y redes innecesarias
docker-compose up -d Levanta los servicios en segundo plano
docker-compose down Apaga y elimina los servicios definidos en docker-compose.yml
docker-compose logs <service> Muestra los logs de un servicio en Docker Compose
docker-compose build Construye las imágenes especificadas en el archivo docker-compose.yml
docker-compose ps Lista los servicios en ejecución en Docker Compose
docker-compose restart <service> Reinicia un servicio de Docker Compose

Con esta lista de comandos esenciales, puedes administrar y limpiar tu entorno Docker de manera eficiente.

Como podras haberte dado cuenta ya, Docker y Docker Compose facilitan la creación de entornos replicables y evitan problemas de dependencias, eliminando muchos problemas de configuracion, basta con levantar tus contenedores para estar listo para programar.