Docker Compose: orquesta tus contenedores como un profesional

Aprende a orquestar múltiples contenedores de forma declarativa con Docker Compose: redes, volúmenes, variables de entorno y un caso práctico de WordPress + MySQL.

Docker Compose: orquesta tus contenedores como un profesional

Aprende a orquestar múltiples contenedores de forma declarativa con Docker Compose, configurar redes, volúmenes, variables de entorno y un caso práctico de WordPress + MySQL.

Introducción: De contenedores individuales a la orquestación

Cuando empezamos a explorar el mundo de Docker, la emoción de levantar nuestro primer contenedor con un comando sencillo como docker run -d -p 80:80 nginx es inigualable. Sentimos que tenemos el control absoluto sobre nuestro software y que los problemas de dependencia han quedado en el pasado. Sin embargo, a medida que nuestros proyectos crecen, la realidad nos golpea con fuerza. Una aplicación web moderna rara vez funciona de forma aislada; por lo general, requiere una base de datos (como MySQL o PostgreSQL), un sistema de almacenamiento en caché (como Redis), un backend, un frontend y tal vez un balanceador de carga.

Intentar gestionar todos estos elementos mediante comandos individuales de Docker se convierte rápidamente en una pesadilla. Tienes que recordar el orden exacto en el que debes iniciar cada contenedor, crear manualmente redes para que puedan comunicarse entre sí, configurar volúmenes para que tus datos no desaparezcan al reiniciar el sistema, y asegurarte de que cada comando tenga las variables de entorno correctas. Al final, terminas con un script de Bash gigantesco, difícil de mantener y propenso a errores.

Aquí es precisamente donde entra en juego Docker Compose. En este artículo aprenderás cómo pasar de la gestión manual e imperativa de contenedores a una orquestación declarativa, limpia y profesional. Al finalizar este tutorial, serás capaz de levantar infraestructuras completas con un único comando.

¿Qué es Docker Compose y por qué deberías usarlo?

Docker Compose es una herramienta desarrollada por Docker que permite definir y ejecutar aplicaciones multi-contenedor. En lugar de escribir comandos interminables en la terminal, utilizas un archivo en formato YAML (con extensión .yml o .yaml) para describir de forma declarativa todos los servicios, redes y volúmenes que componen tu aplicación.

¿Por qué deberías usarlo?

La orquestación con Docker Compose ofrece ventajas fundamentales sobre el uso de la CLI estándar de Docker:

  • Infraestructura como Código (IaC): Al definir tu arquitectura en un archivo de texto plano (`docker-compose.yml`), puedes guardarla en sistemas de control de versiones como Git. Esto facilita el seguimiento de cambios, la colaboración en equipo y la reproducibilidad del entorno.
  • Levantamiento con un solo comando: Con solo ejecutar `docker compose up`, la herramienta se encarga de descargar las imágenes necesarias, crear las redes, montar los volúmenes e iniciar todos los contenedores en el orden correcto.
  • Aislamiento de entornos: Docker Compose utiliza nombres de proyectos (usualmente basados en el nombre de la carpeta) para aislar los recursos. Esto significa que puedes ejecutar múltiples instancias de la misma aplicación en el mismo servidor sin que interfieran entre sí.
  • Facilidad de configuración de red: Crea automáticamente una red interna para tu proyecto, permitiendo que los contenedores se comuniquen utilizando sus nombres de servicio como nombres de dominio (DNS interno).

💡 Tip: Anteriormente, Docker Compose se ejecutaba como un comando independiente usando un guion (docker-compose). En las versiones modernas de Docker Desktop y Docker Engine, Compose se ha integrado directamente en la CLI de Docker, por lo que el estándar actual es usar docker compose (con espacio). ¡Asegúrate de actualizar tus comandos para seguir las mejores prácticas!

Anatomía de un archivo docker-compose.yml

El corazón de Docker Compose es el archivo docker-compose.yml. Este archivo utiliza el formato YAML, que se basa en la indentación con espacios para definir la estructura y la jerarquía de los datos. Veamos los componentes principales que estructuran un archivo básico:

version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    networks:
      - mi-red-personalizada

networks:
  mi-red-personalizada:
    driver: bridge

Analicemos detenidamente cada una de las secciones principales de este archivo de ejemplo:

  • version: Especifica la versión del formato de archivo de Docker Compose que estás utilizando. Aunque en las especificaciones más recientes de Docker esta directiva se ha vuelto opcional, sigue siendo una excelente práctica incluirla (como `3.8`) para garantizar la compatibilidad con herramientas de despliegue y servidores antiguos.
  • services: Es la sección más importante. Aquí defines cada uno de los contenedores que formarán parte de tu aplicación. En el ejemplo anterior, definimos un servicio llamado `web`. Cada servicio puede especificar su imagen de origen, los puertos que expondrá, los volúmenes que montará y las redes a las que se conectará.
  • networks: Permite definir redes personalizadas que tus servicios utilizarán para comunicarse entre sí de forma segura. Si no especificas esta sección, Docker Compose creará una red por defecto para tu aplicación.
  • volumes: Aquí se declaran los volúmenes nombrados para la persistencia de datos. Estos volúmenes son gestionados directamente por Docker y sobreviven al ciclo de vida de los contenedores individuales.

Redes en Compose: ¿Cómo se descubren los servicios?

Uno de los mayores dolores de cabeza al gestionar contenedores de forma manual es la conectividad de red y la resolución de nombres de dominio (DNS). Si tienes una aplicación web en el contenedor A y una base de datos en el contenedor B, ¿cómo sabe el contenedor A qué dirección IP tiene el contenedor B para conectarse?

Docker Compose soluciona esto de manera elegante y automática. Cuando ejecutas tu aplicación, Compose crea una red interna de tipo bridge por defecto para tu proyecto.

Lo revolucionario de este sistema es el descubrimiento de servicios basado en DNS. En lugar de rastrear direcciones IP dinámicas que cambian cada vez que un contenedor se reinicia, los servicios pueden comunicarse entre sí utilizando sus propios nombres declarados en el archivo YAML.

Por ejemplo, si tienes un servicio de base de datos definido bajo el nombre db y un servicio backend, el código de tu backend puede conectarse a la base de datos usando el host db en su cadena de conexión (por ejemplo, mongodb://db:27017 o mysql://db:3306). Docker se encarga de interceptar esta petición y resolverla a la IP interna correcta del contenedor.

💡 Tip: No expongas puertos al sistema host a menos que sea estrictamente necesario. Por ejemplo, si tu base de datos solo debe ser accedida por tu aplicación backend, no uses la propiedad ports en el servicio de la base de datos. De esta forma, la base de datos permanecerá invisible para el exterior y protegida de posibles ataques, comunicándose únicamente de forma interna dentro de la red privada de Compose.

Volúmenes: Persistencia de datos sin dolores de cabeza

Por naturaleza, los contenedores de Docker son efímeros. Esto significa que cualquier archivo que se cree o modifique dentro de un contenedor se perderá permanentemente cuando este se detenga o se elimine. Para aplicaciones sin estado (como un servidor web que solo sirve archivos estáticos), esto no es un problema. Sin embargo, para bases de datos o aplicaciones que permiten a los usuarios subir imágenes, la persistencia de datos es crucial.

Para resolver esto, Docker Compose nos permite definir e integrar volúmenes de forma muy sencilla. Existen dos tipos principales de almacenamiento que podemos utilizar en nuestros proyectos:

Volúmenes Nombrados (Named Volumes)

Son gestionados en su totalidad por Docker dentro de un espacio reservado del sistema de archivos del host (usualmente en /var/lib/docker/volumes/). Son ideales para bases de datos porque ofrecen un excelente rendimiento y aislamiento.

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

volumes:
  db_data:

Montajes de Unión (Bind Mounts)

Te permiten mapear un directorio específico de tu máquina host directamente dentro del contenedor. Son perfectos para el desarrollo local, ya que cualquier cambio que realices en tu código fuente se reflejará de inmediato en el contenedor sin necesidad de reconstruir la imagen.

services:
  web:
    image: node:18
    volumes:
      - ./src:/app/src

Variables de entorno: Protegiendo tus secretos con .env

Configurar contraseñas de bases de datos, claves de API o URLs de servicios de terceros directamente en el archivo docker-compose.yml es una de las prácticas más peligrosas en el desarrollo de software. Si subes este archivo a un repositorio de código público o privado, tus credenciales quedarán expuestas de inmediato.

Docker Compose ofrece un mecanismo nativo y robusto para gestionar estas configuraciones de forma segura utilizando archivos .env y variables de entorno.

El Archivo `.env`

Compose busca automáticamente un archivo llamado .env en el mismo directorio donde se ejecuta. En este archivo puedes almacenar tus pares clave-valor de configuración:

DB_USER=mi_usuario_seguro
DB_PASSWORD=una_contrasenia_super_secreta_123!
WORDPRESS_PORT=8080

Integración en el `docker-compose.yml`

Posteriormente, puedes referenciar estas variables de entorno en tu archivo Compose utilizando la sintaxis ${VARIABLE}:

version: '3.8'

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}

  wordpress:
    image: wordpress:latest
    ports:
      - "${WORDPRESS_PORT}:80"

De esta manera, puedes mantener tu archivo de configuración genérico y seguro para subirlo a Git, mientras que las credenciales reales se mantienen en tu archivo local .env (el cual debe ser agregado a tu archivo .gitignore).

💡 Tip: Puedes definir valores por defecto en tu archivo docker-compose.yml por si la variable no está definida en el .env. La sintaxis para esto es ${VARIABLE:-valor_por_defecto}. Esto es muy útil para asegurar que el despliegue funcione incluso si un desarrollador olvida configurar alguna variable opcional en el archivo de configuración.

Comandos esenciales para el día a día

Para trabajar eficientemente con Docker Compose, debes familiarizarte con su conjunto de comandos esenciales. Todos estos comandos deben ejecutarse desde el directorio donde se encuentra tu archivo docker-compose.yml.

A. Levantar la aplicación: `docker compose up`

Este comando lee el archivo de configuración, descarga las imágenes de Docker si no están presentes en tu sistema, y arranca los servicios.

  • **Modo interactivo (primer plano):** `docker compose up` (bloquea la terminal y muestra los logs de todos los contenedores en tiempo real).
  • **Modo desacoplado (segundo plano):** `docker compose up -d`. Es la forma estándar de arrancar servicios en producción o entornos de pruebas estables, ya que libera tu consola inmediatamente.

B. Detener la aplicación: `docker compose down`

Detiene y elimina los contenedores y las redes creadas por up. Tus datos persistidos en volúmenes nombrados no se verán afectados, a menos que agregues la bandera -v.

docker compose down

Si deseas eliminar también los volúmenes asociados para realizar una limpieza completa:

docker compose down -v

C. Visualizar logs: `docker compose logs`

Muestra las salidas de consola de todos tus contenedores combinadas en un solo flujo cronológico, lo cual es de gran utilidad para depurar errores.

# Ver todos los logs acumulados
docker compose logs

# Seguir los logs en tiempo real (modo stream)
docker compose logs -f

# Ver los logs de un servicio específico (por ejemplo, 'web')
docker compose logs -f web

D. Monitorear el estado: `docker compose ps`

Muestra una lista detallada de los contenedores asociados a tu proyecto de Compose, incluyendo su ID, estado actual, puertos expuestos y el comando interno que están ejecutando.

docker compose ps

E. Ejecutar comandos dentro de un servicio: `docker compose exec`

Te permite interactuar con un contenedor en ejecución. Es el equivalente a docker exec, pero referenciando el nombre del servicio en lugar de tener que buscar el ID del contenedor.

# Entrar a la terminal interactiva del servicio de base de datos
docker compose exec db bash

# Realizar un backup rápido de MySQL desde el host
docker compose exec db mysqldump -u root -p mi_base_de_datos > backup.sql

Ejemplo práctico paso a paso: WordPress y MySQL

Ahora que comprendemos la teoría, los componentes y los comandos esenciales, llevemos todo esto a la práctica. Vamos a crear un sitio web de WordPress completamente funcional conectado a una base de datos MySQL. Ambas piezas se comunicarán a través de una red interna y guardarán sus datos de forma persistente utilizando volúmenes de Docker.

Paso 1: Preparar el directorio de trabajo

Primero, crea una carpeta dedicada en tu servidor o máquina local para este proyecto y navega hacia ella:

mkdir -p /root/blog-articles/wordpress-demo
cd /root/blog-articles/wordpress-demo

Paso 2: Crear el archivo de variables de entorno `.env`

Crea un archivo llamado .env para almacenar nuestras credenciales seguras. Recuerda cambiar los valores por defecto por unos más robustos:

# Configuración de base de datos
DB_NAME=wordpress_db
DB_USER=wp_user
DB_PASSWORD=password_seguro_2026
DB_ROOT_PASSWORD=password_root_extremadamente_largo_y_seguro

# Configuración del puerto público
WP_PORT=8080

Paso 3: Crear el archivo `docker-compose.yml`

Ahora, crea el archivo docker-compose.yml en la misma carpeta. Este archivo estructurará nuestros dos servicios:

version: '3.8'

services:
  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - wp_network

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    restart: always
    ports:
      - "${WP_PORT}:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: ${DB_USER}
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
      WORDPRESS_DB_NAME: ${DB_NAME}
    volumes:
      - wp_data:/var/www/html
    networks:
      - wp_network

volumes:
  mysql_data:
  wp_data:

networks:
  wp_network:
    driver: bridge

¿Qué hace este archivo bajo el capó?

1. `depends_on`: Le indica a Docker Compose que el servicio wordpress depende de db. Por lo tanto, Compose se asegurará de iniciar el contenedor de MySQL antes de intentar arrancar WordPress.

2. `restart: always`: Configura una política de reinicio automático. Si el contenedor se detiene inesperadamente o si el servidor físico se reinicia, Docker volverá a encender el servicio automáticamente.

3. `volumes`: Vincula las rutas de datos internas de MySQL (/var/lib/mysql) y WordPress (/var/www/html) a volúmenes nombrados gestionados de forma externa. De esta forma, aunque destruyas los contenedores con docker compose down, tus posts, configuraciones y usuarios permanecerán a salvo.

Paso 4: Levantar el proyecto

Una vez creados ambos archivos, ejecuta el siguiente comando en tu terminal para iniciar la infraestructura en segundo plano:

docker compose up -d

Compose descargará las imágenes oficiales de WordPress y MySQL, configurará la red wp_network, creará los volúmenes, e iniciará ambos contenedores. Puedes verificar que todo esté en orden ejecutando:

docker compose ps

¡Andando! Abre tu navegador web y navega a http://localhost:8080 (o la dirección IP de tu servidor en el puerto 8080). Verás la famosa pantalla de instalación de WordPress, lista para que configures tu nuevo sitio web.

Conclusiones y Checklist Resumen

Docker Compose transforma radicalmente el desarrollo y despliegue de aplicaciones multi-contenedor. Al empaquetar toda la definición de tu infraestructura en un archivo docker-compose.yml, eliminas la inconsistencia de entornos ("en mi máquina funcionaba") y facilitas una colaboración ágil y libre de fricciones. Ya no necesitas memorizar comandos interminables ni preocuparte por configurar redes complejas de forma manual; Compose hace todo el trabajo pesado de manera declarativa.

Para ayudarte a asimilar estos conceptos y asegurar que tus despliegues siempre sigan las mejores prácticas de la industria, aquí tienes un checklist rápido que puedes revisar antes de enviar tu configuración a producción:

  • [ ] Estructura Declarativa: ¿Has definido toda tu infraestructura dentro de un archivo `docker-compose.yml` en lugar de usar comandos `docker run` aislados?
  • [ ] Variables de Entorno Protegidas: ¿Todas las contraseñas, claves secretas y configuraciones dinámicas están almacenadas en un archivo `.env` que ha sido debidamente agregado a tu `.gitignore`?
  • [ ] Persistencia Asegurada: ¿Has mapeado de forma explícita las rutas críticas de tus bases de datos y archivos de usuario a volúmenes nombrados (`volumes`) para evitar la pérdida accidental de datos?
  • [ ] Seguridad de Redes: ¿Has configurado redes personalizadas para que tus servicios internos (como bases de datos) se comuniquen de forma privada sin exponer puertos innecesarios al sistema host?
  • [ ] Políticas de Resiliencia: ¿Has añadido directivas como `restart: always` o `restart: unless-stopped` a tus servicios de producción para garantizar una alta disponibilidad?
  • [ ] Gestión de Ciclo de Vida: ¿Estás familiarizado con los comandos diarios como `docker compose up -d`, `docker compose down`, `docker compose logs -f` y `docker compose ps`?

Con estas bases, estás completamente equipado para estructurar, desplegar y mantener arquitecturas de contenedores de nivel profesional. ¡Es hora de orquestar!