- Docker Compose permite definir y gestionar múltiples contenedores desde un único archivo YAML, facilitando el trabajo con aplicaciones basadas en microservicios.
- La sección services es obligatoria y se completa con redes, volúmenes, configs y secrets para controlar comunicación, persistencia y configuración.
- Con ejemplos como Flask+Redis o una app full stack con frontend, backend y base de datos se ve cómo Compose simplifica desarrollo y despliegue.
- Los comandos docker compose up, down, ps y logs forman el flujo de trabajo básico para levantar, depurar y detener pilas de contenedores.

Si ya has trasteado con contenedores y has visto que para una app «de verdad» necesitas más de un servicio corriendo a la vez (base de datos, API, frontend, caché…), tarde o temprano acabas chocando con Docker Compose. Esta herramienta te permite levantar todo ese tinglado con un solo archivo y un par de comandos, sin hacer malabares con terminales y scripts interminables.
En este tutorial vas a aprender qué es Docker Compose, cómo funciona su archivo compose.yaml y cómo orquestar aplicaciones multi-contenedor de forma limpia: desde ejemplos sencillos con Flask y Redis, hasta arquitecturas un poco más completas con frontend, backend y base de datos. Verás también redes, volúmenes, configs, secrets y los comandos clave para trabajar a gusto en desarrollo y en entornos más serios.
Qué es Docker Compose y por qué merece la pena usarlo
Docker Compose es una extensión de Docker que permite definir y gestionar varios contenedores como si fueran una sola aplicación. En lugar de ir arrancando cada servicio a mano con su «docker run» y sus parámetros, describes todo en un archivo YAML y lo levantas con un único comando.
La gracia de Compose es que muchas aplicaciones modernas están montadas en microservicios que viven en contenedores individuales: una base de datos, una API, un frontend, un sistema de colas, un caché tipo Redis, etc. Docker recomienda que cada contenedor ejecute un único servicio, así que si intentas meter todo dentro de una sola imagen acabas con un monstruo difícil de escalar y mantener.
Podrías correr dos o más servicios dentro de un mismo contenedor, pero eso rompe buena parte de las ventajas de Docker: si uno se cae arrastra a los demás, no puedes escalar solo la parte que recibe más carga y gestionar logs, recursos y fallos se vuelve un lío importante.
Con Docker Compose defines cada servicio por separado, marcando cómo se conectan entre sí, qué datos persisten, qué puertos exponen, qué variables de entorno usan… De esta forma si un contenedor falla, el resto puede seguir funcionando dependiendo de cómo lo configures, y escalar una pieza concreta es tan sencillo como modificar su configuración o el número de réplicas.
Además, Compose encaja de lujo en flujos de trabajo de CI/CD y despliegue a producción. Puedes usarlo directamente con herramientas como Portainer o Docker Swarm, y si trabajas con Kubernetes, proyectos como Kompose permiten traducir un compose.yaml a manifests de Kubernetes sin tener que reescribirlo todo a mano.
Requisitos previos para seguir el tutorial de Docker Compose
Para poder seguir cómodamente los ejemplos de este tutorial necesitas tener Docker y Docker Compose instalados. Hoy en día hay dos caminos principales:
- Docker Engine + Docker Compose instalados como binarios independientes.
- Docker Desktop, que incluye Docker Engine, Docker Compose y una interfaz gráfica.
Es importante que tengas una base mínima de comandos básicos de Docker (imágenes, contenedores, puertos, volúmenes) y que no te asuste usar la línea de comandos. Los ejemplos suelen asumirse en Linux (por ejemplo Ubuntu 22.04), pero la lógica se aplica igual en Windows y macOS con Docker Desktop.
Comprueba que todo está en orden ejecutando en tu terminal algo tan simple como «docker –version» y «docker compose version». Si ambos comandos responden sin error, estás listo para continuar con los ejemplos.
Estructura básica de un archivo compose.yaml
El corazón de Docker Compose es el archivo compose.yaml (o docker-compose.yml). Ahí describimos qué servicios queremos levantar y cómo deben relacionarse. Aunque antes se usaba el campo version para marcar la versión del formato, la documentación actual recomienda no definirlo para que se use siempre la versión más reciente del esquema.
Dentro del archivo Compose tendrás varias secciones posibles, aunque solo hay una obligatoria: services. A partir de ahí puedes añadir otras secciones según la complejidad de tu proyecto:
- services: definición de cada microservicio (obligatorio).
- networks: redes personalizadas para controlar la comunicación entre contenedores.
- volumes: volúmenes para persistir datos o compartirlos entre servicios.
- configs: configuración de servicios (por ejemplo, archivos de configuración de servidores web).
- secrets: gestión de información sensible (contraseñas, API keys…).
A lo largo de este tutorial verás cómo combinar todas estas secciones para un proyecto típico que incluye una aplicación, una base de datos y una API, y también un ejemplo de app web en Python con Flask y Redis.
Servicios en Docker Compose: el núcleo de la definición
La sección services es la pieza imprescindible de cualquier archivo Compose. En ella defines cada uno de los contenedores que formarán tu aplicación, dándoles el nombre que quieras (por ejemplo, web, database, api, redis, etc.).
Para cada servicio puedes establecer un buen número de parámetros, entre los que destacan algunos muy usados en proyectos reales:
El parámetro build indica dónde está el Dockerfile a partir del cual se construirá la imagen del servicio. Normalmente se señala un contexto (directorio) donde vive el Dockerfile de la aplicación que quieras empaquetar.
Si ya tienes una imagen creada o quieres tirar de una del registro, usas image para referenciarla. El nombre sigue el formato [<registry>/][<project>/]<image>[:<tag>|@<digest>], y si necesitas controlar cuándo se descarga o actualiza esa imagen puedes recurrir a pull_policy.
El campo ports sirve para mapear puertos entre el host y el contenedor. La sintaxis es del tipo [HOST:]CONTAINER[/PROTOCOL]. Por ejemplo, si una base de datos PostgreSQL escucha en el puerto 5432 dentro del contenedor y quieres exponerla en el 5555 del host, lo harías algo así como "5555:5432" en la lista de puertos.
La política de reinicio se controla con restart, que indica qué hacer cuando un contenedor termina por error o se detiene. Los valores típicos son no, always, on-failure y unless-stopped, permitiendo que los servicios críticos se mantengan levantados incluso si fallan puntualmente.
Si un servicio necesita que otro esté disponible antes de arrancar, puedes usar depends_on para definir dependencias entre contenedores. Un ejemplo clásico es una app que requiere que la base de datos esté ya en marcha para no fallar en la conexión inicial.
Para la configuración y credenciales tienes dos enfoques habituales: env_file y environment. Con env_file apuntas a uno o varios ficheros .env con las variables de entorno, mientras que en environment puedes listarlas directamente en el YAML. La práctica más recomendable es usar ficheros .env para evitar que contraseñas y datos sensibles acaben incrustados en el propio compose.yaml.
El parámetro volumes permite montar rutas del host o volúmenes en el contenedor, lo que sirve tanto para persistir datos como para compartir carpetas entre servicios. Aquí solo referenciarás los volúmenes que luego podrás definir en la sección superior volumes si necesitas que sean compartidos o gestionados de forma más explícita.
Con estos campos ya puedes montar servicios bastante completos. La especificación de Compose incluye muchas más opciones avanzadas (salud, límites de recursos, comandos de arranque, etc.), pero con estas ya cubres la mayoría de usos comunes.
Ejemplo 1: aplicación web en Python con Flask y Redis
Un caso típico para entender Docker Compose es crear una aplicación web sencilla en Python, usando Flask para servir páginas y Redis como almacén en memoria para un contador de visitas. La idea es que no necesitas instalar ni Python ni Redis en tu máquina: todo se ejecuta dentro de contenedores.
El flujo de trabajo sería algo así: primero creas un directorio para el proyecto y dentro añades un archivo app.py con el código de Flask. En ese código usas «redis» como hostname y el puerto 6379, que es el puerto por defecto del servicio Redis en su contenedor.
La función que maneja el contador de visitas intenta conectarse varias veces a Redis antes de rendirse, ya que es posible que el contenedor de Redis tarde unos segundos en estar disponible cuando levantas toda la pila.
Además de app.py, creas un archivo requirements.txt con las dependencias de Python (por ejemplo Flask y redis-py), y un Dockerfile que especifica cómo construir la imagen de tu aplicación web: imagen base de Python (3.7, 3.10 o la que toque), directorio de trabajo, variables de entorno para Flask, instalación de gcc y dependencias del sistema, copia del requirements.txt, instalación de paquetes y copia del código.
En el Dockerfile también marcas el puerto que expondrá el contenedor (por ejemplo 5000) y defines el comando predeterminado, normalmente flask run --debug o similar, para que se arranque automáticamente al crear el contenedor.
Con todo esto listo, el archivo compose.yaml define dos servicios: uno llamado, por ejemplo, web, que se construye a partir del Dockerfile del proyecto y expone por fuera el puerto 8000 (mapeando el 8000 del host al 5000 del contenedor), y otro llamado redis que tira de la imagen oficial de Redis en Docker Hub.
Para levantar la aplicación solo tienes que situarte en el directorio del proyecto y ejecutar «docker compose up». Compose se encarga de descargar la imagen de Redis, construir la imagen de tu aplicación web y arrancar ambos servicios en el orden adecuado.
Una vez en marcha, entras con tu navegador en http://localhost:8000 (o http://127.0.0.1:8000) y deberías ver un mensaje tipo «Hello World» y un contador de visitas que se incrementa cada vez que recargas la página. Si inspeccionas las imágenes locales con docker image ls, verás algo como redis y web creadas o descargadas.
Cuando quieras parar todo, puedes hacer CTRL+C en la terminal donde dejaste «docker compose up» o bien ejecutar docker compose down desde el directorio del proyecto. Esto detendrá y eliminará los contenedores creados por ese compose.
Mejorando el flujo de trabajo: Bind mounts y Compose Watch
Trabajar en desarrollo con Docker es más cómodo si no tienes que reconstruir la imagen cada vez que tocas el código. Ahí entran en juego los Bind Mounts y, en versiones más recientes, Docker Compose Watch.
Un Bind Mount consiste en montar una carpeta de tu máquina dentro del contenedor. En el compose.yaml añades en el servicio web una sección de volumes que mapee el directorio del proyecto al directorio de trabajo del contenedor, por ejemplo .:/code. De este modo, cualquier cambio que hagas en tu editor se refleja al instante dentro del contenedor.
Si además activas el modo depuración de Flask con la variable FLASK_DEBUG=1, el comando flask run recargará automáticamente la aplicación cuando detecte cambios en los archivos, sin necesidad de parar y volver a arrancar.
Docker Compose Watch da un paso más allá: puedes usar «docker compose watch» o «docker compose up –watch» para que Compose vigile los archivos del proyecto y sincronice los cambios con los contenedores de forma más inteligente. Cuando guardes un archivo, se copia dentro del contenedor y el servidor de desarrollo actualiza la aplicación sin reiniciar el contenedor completo.
Prueba, por ejemplo, a cambiar el mensaje de bienvenida en app.py de «Hello World!» a una frase como «Hola desde Docker». Guardas el archivo, actualizas el navegador y verás el nuevo mensaje al instante mientras el contador de visitas sigue funcionando sin perder el estado.
Y cuando termines de trabajar, como siempre, puedes tirar de docker compose down para apagar y limpiar los contenedores que estaban en marcha con ese stack.
Ejemplo 2: app full stack con frontend, backend y base de datos
Para ver Docker Compose en una arquitectura algo más realista, imagina una aplicación de lista de tareas (Todo List) con un frontend en Vue.js, una API en Node.js y una base de datos MongoDB. Cada parte vive en su propio directorio y tiene su propio Dockerfile.
En el repositorio podrías encontrar una carpeta frontend con la app Vue y otra backend con el servidor Node. El backend expone endpoints para crear, listar, actualizar y borrar tareas, y se conecta a MongoDB para almacenarlas. El frontend consume esos endpoints para mostrar y gestionar la lista de tareas en el navegador.
El archivo docker-compose.yml se sitúa en la raíz del proyecto y define tres servicios: frontend, backend y database. El servicio frontend se construye desde el Dockerfile en la carpeta correspondiente, suele exponer el puerto 80 interno y lo mapea al puerto 5173 del host (por ejemplo, para usar la misma URL que en desarrollo local).
El backend se construye desde el Dockerfile del directorio backend, expone el puerto 3000 (tanto dentro como fuera del contenedor, si quieres simplificar) y declara una dependencia sobre la base de datos para asegurarse de que MongoDB está disponible cuando arranca.
El servicio database usa directamente la imagen oficial de MongoDB y monta un volumen, digamos mongodb_data, en /data/db, que es donde Mongo guarda sus datos. Ese volumen se declara en la sección superior volumes del compose, de forma que los datos persisten aunque borres y vuelvas a crear los contenedores.
Por último, todos estos servicios se conectan a través de una red personalizada, por ejemplo my-network, definida en la sección networks. Esto permite que se resuelvan por nombre de servicio (el backend puede conectar con Mongo usando el hostname database) y que el tráfico quede encapsulado en esa red aislada.
Con la configuración lista, ejecutar docker compose up en la raíz del proyecto se encarga de construir o descargar las imágenes y lanzar los tres contenedores. Puedes comprobar que todo está en su sitio con docker compose ps, accediendo después a http://localhost:5173 para ver la app Vue en tu navegador y crear tus primeras tareas.
Redes en Docker Compose: conectando servicios entre sí
Las redes son la capa que permite que tus contenedores se «vean» y se hablen de forma controlada. Por defecto Docker ya crea redes para Compose, pero definirlas explícitamente te da más claridad y control sobre qué se puede comunicar con qué.
El funcionamiento es sencillo: cada servicio incluye un campo networks donde indicas a qué redes pertenece, y luego en la sección superior networks defines esas redes con su configuración. Lo más común (y recomendable en muchos casos) es usar el driver bridge.
Una red de tipo bridge crea un espacio privado de red para tus contenedores, con resolución automática de DNS basada en el nombre del servicio. Eso significa que, por ejemplo, si tu servicio de base de datos se llama database, cualquier otro servicio en la misma red puede conectarse usando simplemente database como hostname.
En un proyecto con frontend, backend y base de datos puedes decidir, por ejemplo, crear una red de frontend y otra de backend. El frontend se conectaría al backend, y el backend a la base de datos, pero el frontend y la base de datos no tendrían por qué compartir red, reduciendo la superficie de exposición interna.
En código, esto se traduce en algo tan directo como asignar en cada servicio la red correspondiente, y luego definir esas redes con driver bridge. A nivel de aplicación, lo más sencillo es usar el nombre del servicio como host cuando configuras conexiones: de app a database, por ejemplo, simplemente indicando que el host de la base de datos es «database».
Volúmenes en Docker Compose: persistencia de datos
Los volúmenes son la forma recomendada de persistir información generada por los contenedores, como bases de datos, ficheros de usuario, copias de seguridad, etc. También se usan para compartir datos entre servicios dentro del mismo stack.
En la sección services puedes montar volúmenes directamente con volumes, pero cuando quieres que ese volumen sea accesible por varios contenedores o lo quieres gestionar de forma más explícita, lo defines también en la sección superior volumes del compose.yaml.
Imagina que quieres montar un sistema de copias de seguridad para tu base de datos. Tendrías el servicio de la base de datos montando un volumen donde guarda sus datos y otro servicio dedicado a los backups que monta ese mismo volumen en modo lectura para hacer exportaciones o sincronizaciones sin tocar el contenedor principal.
Docker permite afinar la configuración de los volúmenes con más parámetros (tipo de driver, opciones específicas para drivers externos, etc.), pero en la mayoría de casos lo más práctico es dejar que Docker gestione los volúmenes automáticamente sin volverse loco con configuraciones raras.
Lo importante es que tengas claro qué carpetas de tus servicios necesitan ser persistentes, y que las declares como volúmenes en Compose para no perder datos cuando recreas contenedores o actualizas imágenes.
Configs: gestionando archivos de configuración
La sección configs está pensada para gestionar archivos de configuración de servicios dentro de tu stack, de forma similar a los volúmenes pero centrada específicamente en configuración.
Piensa en un servidor Apache o Nginx que corre en Docker. Es probable que necesites ajustar su archivo de configuración con cierta frecuencia. Hacer un build de la imagen cada vez que tocas estos archivos es poco eficiente y molesto, sobre todo en entornos donde se retocan parámetros con cierta frecuencia.
Con configs puedes indicar en el servicio que quieres aplicar una configuración concreta y luego describirla en la sección configs. Hay varias formas de definirlas, siendo las más comunes:
file: la configuración se genera a partir de un archivo local.external: si se marca comotrue, Compose asume que la configuración ya existe y solo la referencia.name: nombre interno del config en Docker, útil cuando combinas conexternal: true.
De este modo puedes actualizar el archivo de configuración en tu máquina y volver a levantar el stack sin tener que reconstruir la imagen base, manteniendo el código de la imagen separado de la configuración específica del entorno.
Secrets: credenciales y datos sensibles
La sección secrets resuelve un problema clásico: ¿dónde guardo las contraseñas, claves API y demás información delicada sin dejarlas tiradas por el código o por los YAML?
Al igual que con configs, los secrets se pueden definir de distintas formas. Lo habitual es:
file: el secret se genera a partir del contenido de un fichero (por ejemplo, un archivo de texto con una clave).environment: el secret se crea usando el valor de una variable de entorno de tu sistema.external: indica que el secret ya está creado y solo debe referenciarse, útil para no sobreescribir secretos que se gestionan desde fuera.name: nombre interno del secret, especialmente relevante cuando combinasexternal: truecon secretos creados por otra herramienta.
Con secrets puedes hacer que contenedores que necesitan acceso a estas credenciales las lean de forma controlada sin tener que dejarlas visibles en el repositorio de código ni en el propio compose.yaml, reforzando bastante la seguridad de tus despliegues.
Trabajar con múltiples archivos Compose e includes
En proyectos grandes no es raro que tu aplicación se divida en bastantes servicios, a veces gestionados por diferentes equipos. En estos casos resulta práctico separar la configuración en varios archivos Compose para modularizar mejor la arquitectura.
Un enfoque típico es tener un compose.yaml principal para la aplicación y otros archivos para partes de la infraestructura. Por ejemplo, puedes mover la definición de Redis u otros servicios de soporte a un archivo infra.yaml y mantener en el compose principal solo lo que concierne directamente a tu app.
Para hacer esto creas el archivo infra.yaml con su propia sección services donde dejas, por ejemplo, el servicio Redis completo. Luego, en tu compose.yaml principal, añades una sección include que apunta al archivo infra.yaml.
Cuando ejecutas docker compose up desde el directorio del proyecto, Compose combina ambos archivos y levanta todos los servicios como si estuvieran en un único YAML, pero tú sigues teniendo la lógica separada y más ordenada.
Esta técnica facilita que equipos distintos mantengan sus propios archivos Compose y que la aplicación global se ensamblen con includes, algo muy útil en arquitecturas con docenas de contenedores o entornos con mucha infraestructura compartida.
Comandos esenciales de Docker Compose
Aunque Compose tiene un buen catálogo de comandos, en el día a día la mayoría de personas trabajan con un puñado de ellos de forma recurrente. Conviene dominarlos porque son los que marcan tu flujo de trabajo.
El más importante es docker compose up. Este comando construye las imágenes necesarias (si no existen), crea los contenedores, configura redes y volúmenes y arranca todos los servicios definidos en tu archivo Compose. Es el comando que usas cuando quieres levantar tu stack.
Suele combinarse con la opción -d para ejecutarlo en modo «detached», es decir, en segundo plano. Así no llenas la terminal de logs y puedes seguir usando esa sesión para otros comandos. Por ejemplo: docker compose up -d.
Para detener y limpiar lo que has levantado, usas docker compose down, que para y elimina los contenedores, redes y, opcionalmente, las imágenes y volúmenes asociados. Dos flags muy comunes aquí son --rmi (para borrar imágenes) y -v (para eliminar volúmenes definidos en la sección volumes).
Si quieres ver qué contenedores forman parte del proyecto y en qué estado están, puedes ejecutar docker compose ps. Esto te lista cada servicio, su estado (up, exited, etc.) y los puertos expuestos, algo muy útil para comprobar que todo está como toca tras un up.
Cuando lanzas tu stack en modo detach, los logs no aparecen en la terminal. Para consultarlos, tiras de docker compose logs, bien globalmente o filtrando por servicio. El flag -f permite seguir los logs en tiempo real, muy útil para debuggear un servicio concreto sin necesidad de acceder dentro del contenedor.
Flujo de trabajo típico: definir compose.yaml, ejecutar un docker compose up -d, comprobar con docker compose ps, revisar logs con docker compose logs -f <servicio> si algo falla y, cuando acabas, usar docker compose down para dejar todo limpio.
Si alguna vez te pierdes, docker compose --help te muestra el listado de subcomandos y opciones disponibles para recordar qué hace cada cosa sin tener que ir a la documentación.
Visto todo lo anterior, herramienta clave para cualquiera que trabaje con contenedores más allá de cosas sueltas. Te permite desarrollar directamente sobre un entorno muy parecido (o idéntico) al de producción, controlar servicios, redes y datos desde un simple YAML y evitar montones de problemas de compatibilidad y despliegue que, tarde o temprano, acaban apareciendo cuando solo trabajas «en local» sin contenedores. Una vez que te acostumbras a escribir un buen compose.yaml para tus proyectos, cuesta bastante volver atrás.
Redactor apasionado del mundo de los bytes y la tecnología en general. Me encanta compartir mis conocimientos a través de la escritura, y eso es lo que haré en este blog, mostrarte todo lo más interesante sobre gadgets, software, hardware, tendencias tecnológicas, y más. Mi objetivo es ayudarte a navegar por el mundo digital de forma sencilla y entretenida.
