Servicios, Redes y Volúmenes en Compose

Lectura
25 min~8 min lectura
CONCEPTO CLAVE: Docker Compose permite definir y ejecutar aplicaciones multi-contenedor. Cada servicio en tu archivo compose representa un contenedor, y Compose maneja automáticamente la creación de redes y gestión de volúmenes necesarios para que estos servicios se comuniquen y persistican datos.

Introducción

En esta lección profundizaremos en los tres pilares fundamentales de Docker Compose: servicios, redes y volúmenes. Dominar estos conceptos te permitirá crear entornos de desarrollo locales robustos, reproducibles y idénticos entre todos los miembros de tu equipo.

Cuando trabajas con aplicaciones reales, rara vez tienes un solo contenedor. Típicamente necesitas una base de datos, un servidor web, un servicio de caché, quizás un message broker. Docker Compose resuelve este problema permitiéndote definir toda esta arquitectura en un único archivo docker-compose.yml.

Definición de Servicios

Los servicios son la pieza central de Docker Compose. Cada servicio representa un contenedor que se creará a partir de una imagen específica. Veamos la estructura básica:

version: '3.8'
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    environment:
      - NODE_ENV=development
    volumes:
      - ./app:/usr/share/nginx/html
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: developer
      POSTGRES_PASSWORD: devpassword
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
💡 Tip: Usa siempre versiones específicas de imágenes en producción. En desarrollo puedes usar :latest, pero especifica la versión exacta para evitar sorpresas cuando se actualicen las imágenes.

Configuración avanzada de servicios

Los servicios ofrecen numerosas opciones de configuración. Veamos las más relevantes para desarrollo local:

Dependencias entre servicios

Usa la opción depends_on para asegurar que un servicio inicie después de otro:

services:
  backend:
    build: ./backend
    depends_on:
      - db
      - redis
  db:
    image: postgres:15
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
📌 Nota: depends_on solo garantiza que el contenedor inicie en orden, no que el servicio esté completamente listo. Para aplicaciones que necesitan esperar a que una base de datos acepte conexiones, considera usar scripts de wait o herramientas como wait-for-it.sh o dockerize.

Control de restart

Configura el comportamiento de reinicio del contenedor:

services:
  api:
    image: node:18
    restart: unless-stopped
  worker:
    image: node:18
    restart: on-failure
  monitor:
    image: prometheus
    restart: always

Las opciones disponibles son: no (nunca reiniciar), always (siempre reiniciar), on-failure (reiniciar solo si sale con código de error), y unless-stopped (reiniciar siempre excepto si se detuvo manualmente).

Variables de entorno

Docker Compose soporta múltiples formas de definir variables de entorno:

services:
  api:
    image: node:18
    environment:
      # Forma explícita
      DB_HOST: db
      DB_PORT: 5432
      # Forma corta (valor desde el host)
      - NODE_ENV
      # Archivo .env
      env_file:
        - .env.production
        - ./config/secrets.env

Redes en Docker Compose

Por defecto, Docker Compose crea una red para todos los servicios definidos, permitiéndoles comunicarse entre sí usando los nombres de servicio como hostnames. Sin embargo, puedes customize esta configuración para casos más complejos.

CONCEPTO CLAVE: Cada servicio en Docker Compose está disponible para otros servicios por su nombre de servicio. Si tienes un servicio llamado database, los demás servicios pueden acceder a él usando database como hostname.

Redes personalizadas

Para arquitecturas más complejas, puedes definir múltiples redes:

version: '3.8'
services:
  frontend:
    image: nginx:alpine
    networks:
      - web-network
    ports:
      - "80:80"
  backend:
    image: node:18
    networks:
      - web-network
      - api-network
    depends_on:
      - db
  db:
    image: postgres:15
    networks:
      - api-network
    volumes:
      - db_data:/var/lib/postgresql/data

networks:
  web-network:
    driver: bridge
  api-network:
    driver: bridge
    internal: true  # Redisolada, sin acceso externo
💡 Tip: Usar internal: true en una red es útil para bases de datos o servicios internos que no necesitan exposición externa, añadiendo una capa extra de seguridad.

DNS y resolución de nombres

Docker Compose configura automáticamente un DNS interno. Cada servicio puede resolver:

  • Su propio nombre de servicio
  • Otros nombres de servicio en la misma red
  • alias personalizados que puedes definir
services:
  api:
    image: node:18
    networks:
      app-network:
        aliases:
          - api-service
          - rest-api

Volúmenes y Persistencia de Datos

Los volúmenes son esenciales para persistir datos más allá del ciclo de vida de un contenedor. Sin volúmenes, cualquier dato guardado dentro del contenedor se pierde cuando se elimina.

Tipos de volúmenes

Docker Compose soporta tres tipos de volúmenes:

1. Volúmenes anónimos (anonymous volumes)

services:
  app:
    image: node:18
    volumes:
      - /data  # Docker crea un volumen anónimo
⚠️ Advertencia: Los volúmenes anónimos no se persisten entre ejecuciones de docker-compose down. Son útiles para datos temporales o cachés que no necesitas mantener.

2. Volúmenes con nombre (named volumes)

services:
  db:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Los volúmenes con nombre persisten hasta que se eliminen explícitamente con docker volume rm. Son ideales para bases de datos y cualquier dato que necesites mantener.

3. Bind mounts (montajes de enlace)

services:
  frontend:
    image: node:18
    volumes:
      - ./src:/app/src:ro  # :ro = solo lectura
      - ./package.json:/app/package.json
  api:
    image: node:18
    volumes:
      - ~/.npm:/root/.npm  # Cache de npm entre ejecuciones
📌 Nota: Los bind mounts son perfectos para desarrollo local porque permiten que los cambios en tu código se reflejen inmediatamente en el contenedor sin rebuilds.

Configuración avanzada de volúmenes

volumes:
  db_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /host/path
  s3_backup:
    driver: local-persist
    driver_opts:
      persistonchange: true

Usos prácticos de volúmenes

Veamos un ejemplo completo de una aplicación con persistencia adecuada:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src        # Código fuente en vivo
      - node_modules:/app/node_modules  # Preserva node_modules
    environment:
      - NODE_ENV=development
      - DB_HOST=postgres
    depends_on:
      - postgres
      - redis
    command: npm run dev

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp_dev
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev123
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes

volumes:
  postgres_data:
  redis_data:
  node_modules:
💡 Tip: El volumen node_modules:/app/node_modules es un patrón muy útil. Vincula un volumen con nombre sobre el directorio local (que está vacío o tiene dependencias diferentes) para preservar los node_modules del contenedor y evitar problemas de compatibilidad entre sistemas operativos.
Ver más: Comandos útiles de gestión

Comandos esenciales para trabajar con volúmenes en desarrollo:

# Listar volúmenes
docker volume ls

# Inspeccionar un volumen
docker volume inspect mi_proyecto_postgres_data

# Eliminar volúmenes huérfanos
docker volume prune

# Eliminar un volumen específico
docker volume rm mi_proyecto_postgres_data

# Ver uso de disco
docker system df -v

Ejemplo Integrado: Stack MEAN Completo

Veamos un ejemplo completo que integra todos los conceptos:

version: '3.8'

services:
  mongodb:
    image: mongo:6
    restart: unless-stopped
    networks:
      - backend
    volumes:
      - mongodb_data:/data/db
    environment:
      MONGO_INITDB_DATABASE: myapp
    ports:
      - "27017:27017"

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    restart: unless-stopped
    networks:
      - backend
    volumes:
      - ./backend/src:/app/src
      - backend_modules:/app/node_modules
    environment:
      NODE_ENV: development
      MONGODB_URI: mongodb://mongodb:27017/myapp
      JWT_SECRET: dev-secret-change-in-prod
    ports:
      - "5000:5000"
    depends_on:
      - mongodb
    command: npm run dev

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    restart: unless-stopped
    networks:
      - frontend
      - backend
    volumes:
      - ./frontend/src:/app/src
      - frontend_modules:/app/node_modules
    environment:
      REACT_APP_API_URL: http://localhost:5000
    ports:
      - "3000:3000"
    depends_on:
      - backend
    command: npm start

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    networks:
      - frontend
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - backend
      - frontend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # La base de datos solo es accesible internamente

volumes:
  mongodb_data:
  backend_modules:
  frontend_modules:
📌 Arquitectura: Este compose define una red backend interna donde solo el backend puede comunicarse con MongoDB. El frontend accede al backend a través de Nginx (reverse proxy). Esta separación añade seguridad y claridad a la arquitectura.

Debugging y Troubleshooting

Cuando algo no funciona, estos comandos te ayudarán a diagnosticar problemas:

# Ver logs de todos los servicios
docker-compose logs -f

# Ver logs de un servicio específico
docker-compose logs -f backend

# Ver logs de múltiples servicios
docker-compose logs -f backend db redis

# Ejecutar un comando en un servicio existente
docker-compose exec backend npm run debug

# Abrir una shell en un contenedor
docker-compose exec backend sh

# Ver estado de todos los servicios
docker-compose ps

# Ver configuración resuelta
docker-compose config

Resumen de Comandos Esenciales

ComandoDescripción
docker-compose up -dIniciar todos los servicios en background
docker-compose downDetener y eliminar contenedores (mantiene volúmenes)
docker-compose down -vDetener y eliminar todo incluyendo volúmenes
docker-compose restartReiniciar todos los servicios
docker-compose buildRebuild imágenes de servicios
docker-compose up --buildBuild y ejecutar con un solo comando
docker-compose logs -f [service]Ver logs en tiempo real
docker-compose exec [service] shAbrir shell en servicio
🧠 Quiz: Servicios, Redes y Volúmenes en Compose

¿Cuál es la diferencia principal entre un volumen con nombre y un bind mount?

  • A) Los volúmenes con nombre se crean y gestionan por Docker, mientras que los bind mounts mapean un directorio específico del host
  • B) No hay diferencia, son exactamente iguales
  • C) Los bind mounts solo funcionan en Windows
  • D) Los volúmenes con nombre solo funcionan en producción
Respuesta correcta: A. Los volúmenes con nombre son gestionados por Docker y persistentes, mientras que los bind mounts mapean rutas específicas del sistema de archivos del host al contenedor.
🧠 Quiz

¿Para qué sirve la opción depends_on en un servicio?

  • A) Copiar archivos de un contenedor a otro
  • B) Asegurar que un servicio inicie después de otro
  • C) Compartir variables de entorno entre servicios
  • D) Crear una red privada entre servicios
Respuesta correcta: B. depends_on establece una dependencia de inicio entre servicios, garantizando que el servicio dependiente arranque después del servicio especificado.
🧠 Quiz

Si quieres que tu base de datos NO sea accesible desde el exterior y solo se comunique con tu backend, ¿qué configuración de red usarías?

  • A) No definir redes (usar default)
  • B) Crear una red con internal: true
  • C) Usar el driver host
  • D) Asignar la base de datos a dos redes públicas
Respuesta correcta: B. Una red con internal: true impide cualquier conexión externa, aislando los servicios dentro de esa red.
Dominar servicios, redes y volúmenes en Docker Compose es fundamental para crear entornos de desarrollo reproducibles. Practica con ejemplos reales y experimentarás cómo Docker Compose simplifica enormemente la gestión de aplicaciones complejas.
💡 Próximos pasos: En la siguiente lección aprenderemos sobre Desarrollo en Vivo con Volumes, profundizando en técnicas para hot-reload y sincronización de código entre tu entorno local y los contenedores.