Guía completa para desplegar aplicaciones con Docker Compose

Última actualización: 13/01/2026
Autor: Isaac
  • Docker Compose permite definir y orquestar aplicaciones multicontenedor mediante un archivo YAML, simplificando despliegues complejos.
  • Servicios, redes y volúmenes se describen de forma declarativa, facilitando la persistencia de datos y la comunicación interna segura.
  • Comandos como up, down, logs, exec y build cubren el ciclo completo de vida de los proyectos gestionados con Compose.
  • Un solo docker-compose.yml hace reproducible el despliegue tanto en local como en servidores cloud o VPS gestionados vía consola o Portainer.

Desplegar aplicaciones con Docker Compose

Trabajar con contenedores se ha convertido en el pan de cada día para casi cualquier equipo de desarrollo, y cuando una aplicación deja de ser un simple servicio y empieza a sumar base de datos, caché, frontend y algún microservicio más, manejar todo eso a mano es un auténtico lío. Aquí es donde entra en juego Docker Compose, una herramienta pensada precisamente para que no tengas que ir arrancando contenedor por contenedor, configurando redes a mano ni recordando cada comando kilométrico.

El objetivo de este artículo es que termines sabiendo desplegar aplicaciones completas con Docker Compose, tanto en local como en servidores, entendiendo bien qué hace cada parte del fichero docker-compose.yml, cómo funcionan servicios, volúmenes y redes, qué comandos necesitas en tu día a día y cómo encaja todo esto en un contexto real con una app Node.js y una base de datos. Si vienes del desarrollo puro (frontend, móvil, backend) y te suenan a chino cosas como orquestación u operaciones, no te preocupes: vamos a ir paso a paso pero sin ahorrar detalles.

Requisitos básicos del entorno

Antes de ponernos a desplegar nada necesitas un sistema base mínimamente preparado. Un entorno típico y muy usado para practicar y para pequeños proyectos en producción podría ser algo así:

  • Ubuntu 20.04 como sistema operativo de servidor o máquina local.
  • Docker 20.10.x instalado y funcionando (daemon activo).
  • Docker Compose 1.29.x o superior para gestionar proyectos multicontenedor.
  • Node.js 18.x y NPM 8.x si vas a construir imágenes a partir de una aplicación Node.

No es obligatorio usar exactamente estas versiones, pero sí tener algo similar y relativamente reciente. En servidores cloud tipo VPS (Google Cloud, AWS, Arsys, etc.) lo habitual es levantar una VM Linux, instalar Docker y luego añadir Docker Compose sobre esa base.

Qué es Docker y qué resuelve Docker Compose

Conceptos de Docker y Compose

Docker es una plataforma para empaquetar y ejecutar aplicaciones en contenedores aislados. Cada contenedor incluye sólo lo necesario para ejecutar tu proceso (binarios, librerías, runtime…), compartiendo el kernel con el host pero sin mezclarse con el resto de servicios. Te evita el clásico “en mi máquina funciona” y permite mover la app entre distintas máquinas sin drama.

Docker Compose, por su parte, es la herramienta que se encarga de orquestar varios contenedores como si fueran una sola aplicación. Si sólo tuvieras un servicio sencillo podrías apañarte con docker run, pero en cuanto necesitas:

  • Un frontend (Angular, React, Vue…)
  • Un backend (Node.js, Java, Python…)
  • Una base de datos (MySQL, PostgreSQL, MongoDB…)
  • Quizá un sistema de caché tipo Redis

Gestionar todo esto contenedor a contenedor se vuelve inviable. Compose te permite definir toda la “pila” en un único archivo YAML, declarando qué servicios existen, cómo se conectan, qué puertos exponen, qué volúmenes usan y qué variables de entorno necesitan.

Ese archivo se llama normalmente docker-compose.yml, aunque puede llamarse de otra manera si lo indicas con -f al lanzar los comandos. Funciona como una receta: cualquier persona que tenga Docker y Docker Compose puede reproducir la misma infraestructura de contenedores en su máquina o en un servidor.

Instalación de Docker Compose en los distintos sistemas

Instalar Docker Compose

El único requisito previo para instalar Docker Compose es tener Docker funcionando. A partir de ahí, el proceso cambia un poco según el sistema operativo:

Instalación en Linux (ejemplo Ubuntu)

En distribuciones como Ubuntu puedes instalar Docker Compose descargando el binario oficial y dándole permisos de ejecución. Un patrón típico es:

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" \
  -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose

Una vez hecho esto, ejecuta docker-compose –version para comprobar que el comando está disponible y responde con la versión correcta.

Instalación en macOS

En Mac la vía más sencilla suele ser instalar Docker Desktop, que ya lleva Docker Engine y Docker Compose integrados. Alternativamente, muchos desarrolladores usan Homebrew:

brew install docker-compose

Con Docker Desktop normalmente ya no tendrás que preocuparte por composiciones, porque el propio paquete se encarga de mantenerlo al día junto con el motor Docker.

Instalación en Windows

En Windows lo más práctico es usar Docker Desktop para Windows. Descargas el instalador desde la web oficial de Docker, lo ejecutas y sigues el asistente. Esto te deja tanto Docker como Compose listos en PowerShell o en WSL2.

Aunque se puede trabajar con Docker en Windows nativo, para entornos de desarrollo algo más serios suele recomendarse usar WSL2 con una distro Linux o directamente una VM Linux remota, donde Docker y Compose funcionan de forma más cercana a producción.

Estructura básica de un docker-compose.yml

Estructura de archivo docker-compose

Dentro del archivo docker-compose.yml podemos definir varios bloques principales: versión del formato, servicios, volúmenes, redes y, en algunos casos, configuraciones extra. Lo esencial para desplegar una app típica son los servicios y, cuando quieres persistencia, los volúmenes y las redes.

  Orbitiny Desktop para Linux: escritorio modular, portátil e innovador

Servicios: el corazón de la aplicación

Cada servicio del fichero Compose suele corresponder a un contenedor (o grupo de contenedores) que ofrece una pieza de la aplicación. Por ejemplo, un servicio web, un servicio de base de datos, un servicio de caché, etc. La definición de cada servicio admite un buen número de propiedades, entre las que destacan:

  • image: imagen Docker que se usará para el contenedor (por ejemplo, nginx:latest, postgres:15, node:18…).
  • container_name: nombre explícito del contenedor (opcional; si no se define, Compose genera uno).
  • build: ruta al directorio donde se encuentra el Dockerfile para construir la imagen si no existe o queremos una personalizada.
  • command: comando que se ejecutará al arrancar el contenedor (sobreescribe el CMD de la imagen).
  • ports: mapeo de puertos en formato host:contenedor, por ejemplo «80:80» o «3030:3030».
  • volumes: montajes de volúmenes (named volumes, bind mounts, etc.).
  • environment: variables de entorno que se inyectan al contenedor.
  • depends_on: indica dependencias entre servicios, para que unos se arranquen antes que otros.
  • networks: redes Docker a las que se conecta el servicio.
  • restart: política de reinicio (no, always, on-failure, unless-stopped).

Lo interesante es que muchas de estas propiedades son opcionales. Podrías definir un servicio sólo con image y Compose ya sería capaz de levantar el contenedor con valores por defecto. Luego, según necesidades, vas afinando puertos, redes, variables, etc.

Volúmenes: persistencia y compartición de datos

Los volúmenes en Docker son el mecanismo estándar para persistir datos y compartirlos entre contenedores o con el host. En Compose, se declaran generalmente en la sección «volumes» a nivel raíz, y luego se referencian desde los servicios.

Un volumen puede tener varias propiedades relevantes:

  • driver: tipo de driver de volumen (por defecto, local).
  • driver_opts: opciones específicas del driver, donde puedes indicar:
    • type: tipo de volumen («volume», «bind», «nfs», etc.).
    • device: ruta en el host que quieres montar, en el caso de un bind mount.
    • o: opciones de montaje (rw, ro, etc.).
  • external: si el volumen es gestionado externamente (no lo crea Compose).
  • labels: etiquetas arbitrarias.
  • name: nombre del volumen, si quieres personalizarlo.
  • scope: alcance del volumen (generalmente local).

Aunque puedas configurar muchísimos detalles, en muchos casos basta con declarar el nombre del volumen y usarlo en el servicio. Por ejemplo, para una base de datos MySQL o PostgreSQL, es típico tener un volumen de datos y, a veces, un bind mount para scripts de inicialización.

Redes: comunicación entre contenedores

Cuando una aplicación tiene varios módulos, es habitual que quieras aislar su tráfico en una red interna para que frontend y backend o backend y base de datos se vean sólo entre ellos y no estén expuestos sin control al host.

Docker implementa redes con un modelo inspirado en tres capas:

  • Endpoint: la interfaz virtual que conecta el contenedor con la red.
  • Sandbox: el espacio de red aislado del contenedor (su pila TCP/IP propia).
  • Network: la red que interconecta los distintos sandboxes a través de los endpoints.

Entre los drivers de red más habituales en Docker tenemos:

Driver Ámbito Descripción
bridge Local Es la red por defecto en muchos hosts Docker, crea un bridge virtual en el host y conecta contenedores entre sí.
host Local Desactiva el aislamiento de red: el contenedor comparte la red del host directamente.
overlay Global Permite conectar contenedores que se ejecutan en distintos hosts Docker dentro de un swarm.
macvlan Global Asigna una MAC al contenedor como si fuera un dispositivo físico en la red.
none Local Sin conectividad de red gestionada por Docker, para casos muy específicos.

En Compose, las redes se definen en la sección «networks», donde eliges driver y opciones. Si no defines nada, normalmente se usa una red bridge por defecto. Los servicios se conectan a esas redes simplemente listando el nombre en su definición.

Ejemplo simple: nginx y PostgreSQL con Docker Compose

Ejemplo de despliegue con Docker Compose

Para aterrizar todos estos conceptos, imagina que quieres montar una web sencilla con Nginx y una base de datos PostgreSQL. Un ejemplo minimalista de docker-compose.yml podría incluir dos servicios, una red interna y un par de volúmenes:

version: '3.8'

services:
  web:
    image: nginx:latest
    container_name: mi_nginx
    ports:
      - "80:80"
    depends_on:
      - db
    networks:
      - app_net

  db:
    image: postgres:latest
    container_name: mi_postgres
    environment:
      POSTGRES_PASSWORD: ejemplo_password
    volumes:
      - datos_db:/var/lib/postgresql/data
    networks:
      - app_net

volumes:
  datos_db:

networks:
  app_net:
    driver: bridge

Aquí vemos dos cosas muy importantes: por un lado, que Nginx expone el puerto 80 al host mientras la base de datos sólo es accesible dentro de la red app_net, y por otro, que los datos de PostgreSQL se persisten en un volumen llamado datos_db. Además, web depende de db, de modo que Compose intentará arrancar primero la base de datos.

Dependencias entre servicios con depends_on

En una aplicación real suele haber relaciones de “este servicio no tiene sentido sin aquel”. Por ejemplo, una API REST que necesita que la base de datos esté arrancada para inicializar conexiones, o un frontend que arranca sólo cuando el backend responde.

En Docker Compose puedes expresar estas relaciones con la clave depends_on, listando los servicios de los que depende el actual:

services:
  api:
    image: mi_usuario/mi_api:latest
    depends_on:
      - db

  db:
    image: postgres:15

Con esta configuración, cuando ejecutes docker-compose up sin especificar servicios, Compose levantará primero db y, a continuación, api. Ten en cuenta, eso sí, que depends_on controla el orden de arranque, pero no garantiza al 100% que el servicio dependiente ya esté «listo» (por ejemplo, que la base de datos acepte conexiones). Para casos delicados, se suelen usar scripts de espera o healthchecks.

  Ejemplos prácticos de comandos FFmpeg para convertir formatos en Linux

Comandos esenciales de Docker Compose

Una vez que tienes el fichero docker-compose.yml en su sitio, el día a día con Compose gira alrededor de unos cuantos comandos imprescindibles. Asumiremos siempre que estás en el directorio donde está el archivo, o que usas la opción -f para indicarlo.

Levantar la aplicación: docker-compose up

El comando principal para desplegar los contenedores es docker-compose up. Si lo ejecutas sin más parámetros, intentará construir las imágenes necesarias (si tienen sección build) y arrancar todos los servicios definidos:

docker-compose up

Si quieres que los contenedores se ejecuten en segundo plano, como es habitual en servidores, añade -d:

docker-compose up -d

Detener contenedores: docker-compose down

Para parar y borrar los contenedores del proyecto (aunque no necesariamente imágenes ni volúmenes) usas:

docker-compose down

Puedes combinar este comando con opciones adicionales si quieres borrar también volúmenes, redes personalizadas, etc., pero en la mayoría de casos con down sin más es suficiente para detener el proyecto de forma ordenada.

Ver el estado de los servicios: docker-compose ps

Si necesitas comprobar qué servicios están activos, qué puertos tienen mapeados y su estado, el comando a usar es:

docker-compose ps

Esto te mostrará una tabla con los contenedores gestionados por ese Compose, incluyendo columnas de nombre, imagen, puertos y estado actual, muy útil para verificar que todo está como esperabas.

Consultar logs: docker-compose logs

Para ver lo que está ocurriendo dentro de tus servicios, dispones de docker-compose logs. Puedes ver los logs de todos los servicios o de uno en concreto:

docker-compose logs

# Sólo los logs del servicio "api"
docker-compose logs api

Si añades la opción -f, harás un seguimiento en tiempo real (similar a tail -f):

docker-compose logs -f api

Entrar en un contenedor: docker-compose exec

Cuando necesitas “meterte” dentro de un contenedor para depurar o ejecutar comandos, recurres a docker-compose exec. Por ejemplo, para abrir una shell en un servicio llamado api:

docker-compose exec api sh

En contenedores basados en distribuciones con bash puedes usar bash en lugar de sh, lo que sea más cómodo dependiendo de la imagen base.

Construir o reconstruir imágenes: docker-compose build y docker-compose pull

Si has modificado algún Dockerfile o parte del contexto de build, necesitarás reconstruir las imágenes asociadas:

docker-compose build
# O bien para un servicio concreto
docker-compose build api

Cuando las imágenes vienen de un registro remoto (Docker Hub, registro privado…) y simplemente quieres descargar la última versión declarada en el YAML, usas:

docker-compose pull

Recuerda que siempre puedes recurrir a los comandos docker “normales” (docker ps, docker images, docker volume ls, docker network ls, etc.), pero para mantener la coherencia del proyecto es mejor que todo lo que afecte a los servicios definidos en docker-compose.yml se maneje vía Compose.

Ejemplo completo: app Node.js + MySQL con Docker y Docker Compose

Vamos ahora con un ejemplo algo más realista: una API REST en Node.js que usa MySQL para almacenar información (por ejemplo, datos de coches). El flujo típico sería:

  1. Desarrollar la API usando variables de entorno para la configuración.
  2. Crear un Dockerfile para la API.
  3. Construir y, si quieres, subir la imagen a Docker Hub.
  4. Definir un docker-compose.yml con la API y la base de datos.
  5. Levantar todo con docker-compose up y probar la app.

1. API en Node.js preparada para variables de entorno

Supón que tienes un proyecto Node con Express que expone algunos endpoints para consultar, crear y listar coches. La clave aquí no es el código en sí, sino que la configuración de conexión a la base de datos venga de variables de entorno como DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, etc.

Esto es fundamental para convivir bien con Docker: no quieres quemar credenciales ni URLs en el código, sino parametrizarlo en el momento del despliegue, ya sea en local o en la nube.

2. Dockerfile para la aplicación web

Dentro del directorio de la app creas un Dockerfile que se encargue de construir la imagen. Un ejemplo básico podría ser:

FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install --only=production

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]

Este Dockerfile parte de una imagen oficial de Node, instala dependencias, copia el código, expone el puerto de la app (3000 en este caso) y define el comando de arranque. A partir de aquí puedes construir la imagen localmente:

docker build -t mi_usuario/mi-api-coches:latest .

Comprueba que la imagen existe con docker images y, si todo va bien, ya podrías incluso ejecutar esa API en un contenedor suelto usando docker run. Pero lo interesante vendrá cuando la orquestemos con la base de datos usando Compose.

3. Subir la imagen a un registro (opcional pero muy útil)

Si quieres poder desplegar tu app en otra máquina (una VM en la nube, por ejemplo), es muy cómodo subir la imagen a Docker Hub u otro registro. Para eso:

docker login
# te pedirá usuario y contraseña

docker push mi_usuario/mi-api-coches:latest

Si quieres versionar explícitamente, puedes etiquetar varias versiones:

docker tag mi_usuario/mi-api-coches:latest mi_usuario/mi-api-coches:v1

docker push mi_usuario/mi-api-coches:v1

En el servidor donde vayas a desplegar, bastará con hacer docker pull de la imagen y usarla en el docker-compose.yml, sin necesidad de reconstruir allí.

  Cómo enviar correos desde Bash y PowerShell paso a paso

4. Definir el docker-compose.yml con API y MySQL

El siguiente paso es crear un archivo docker-compose.yml que junte API y base de datos, añadiendo además un volumen para los datos de MySQL y, si lo necesitas, un script de inicialización:

version: '3.8'

services:
  web:
    image: mi_usuario/mi-api-coches:latest
    container_name: api_coches
    ports:
      - "3000:3000"
    environment:
      DB_HOST: db
      DB_USER: coches_user
      DB_PASSWORD: coches_pass
      DB_NAME: coches_db
    depends_on:
      - db
    networks:
      - coches_net

  db:
    image: mysql:8
    container_name: mysql_coches
    environment:
      MYSQL_ROOT_PASSWORD: root_pass
      MYSQL_DATABASE: coches_db
      MYSQL_USER: coches_user
      MYSQL_PASSWORD: coches_pass
    volumes:
      - datos_mysql:/var/lib/mysql
      - ./initdb:/docker-entrypoint-initdb.d
    ports:
      - "3306:3306"
    networks:
      - coches_net

volumes:
  datos_mysql:

networks:
  coches_net:
    driver: bridge

Hay varios detalles interesantes en esta configuración:

  • La API apunta a la base de datos usando DB_HOST=db, que coincide con el nombre del servicio MySQL. Docker Compose ofrece un DNS interno, de modo que no necesitas preocuparte de IPs, basta con usar el nombre del servicio.
  • La carpeta ./initdb se monta en /docker-entrypoint-initdb.d en el contenedor MySQL, lo que permite incluir scripts .sql o .sh que se ejecutan automáticamente al primer arranque (por ejemplo, para crear tablas, insertar datos de ejemplo, etc.).
  • Los datos de MySQL se guardan en el volumen datos_mysql, de modo que si tiras los contenedores con docker-compose down, la información de la base sigue intacta.
  • Ambos servicios comparten la red coches_net, que actúa como red interna aislada. Hacia el exterior expones sólo los puertos que quieras (3000 para la API, 3306 si necesitas acceder a la BBDD desde fuera, cosa que a veces ni hace falta).

5. Desplegar y probar la aplicación

Con el docker-compose.yml listo, desplegarlo es tan simple como lanzar desde el directorio del archivo:

docker-compose up -d

La primera vez tardará un poco más porque tiene que descargar las imágenes de MySQL (y de la API si no está local), crear el volumen y ejecutar los scripts de inicialización. Luego, para comprobar el estado:

docker-compose ps

Si todo está “Up” puedes empezar a hacer peticiones a la API desde curl o Postman. Por ejemplo:

# Listar todos los coches
curl http://localhost:3000/coches

# Obtener un coche concreto
curl http://localhost:3000/coches/1

# Crear un coche vía POST
curl -X POST http://localhost:3000/coches \
  -H "Content-Type: application/json" \
  -d '{"marca": "Seat", "modelo": "León"}'

Cuando quieras tumbar el despliegue, bastará un docker-compose down. Si no borras el volumen, los datos se conservarán para el siguiente arranque.

Desplegar Docker Compose en servidores cloud y Portainer

Todo lo que hemos visto vale igual para desplegar en tu propio portátil que en un servidor cloud. La diferencia es, básicamente, dónde ejecutas los comandos docker-compose up y cómo abres los puertos al exterior.

Un enfoque muy sencillo para proyectos personales o side projects es crear una VM pequeña (por ejemplo, una e2-micro gratuita en Google Cloud), instalar Docker y Docker Compose, clonar tu repositorio con el código y el docker-compose.yml, y levantar la app ahí.

Lo único que debes tener en cuenta es la política de firewall del proveedor: si tu app escucha en el puerto 3000, tienes que abrir ese puerto en la configuración de red del proveedor (o usar un proxy inverso en el 80/443 si ya te quieres poner fino con HTTPS). Una vez abierto el puerto, podrás entrar con http://IP_DEL_SERVIDOR:3000 desde cualquier navegador.

Si gestionar contenedores por consola se te hace pesado, puedes apoyarte en Portainer, una herramienta que se ejecuta también en un contenedor y te ofrece una interfaz web para administrar Docker (y Docker Compose). Para levantar Portainer en un servidor bastaría con algo como:

docker volume create portainer_data

docker run -d \
  -p 8000:8000 -p 9000:9000 \
  --name=portainer \
  --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer

Luego accederías a Portainer en http://IP_DEL_SERVIDOR:9000, crearías un usuario administrador y, a partir de ahí, podrías ver contenedores, stacks de Compose, redes, volúmenes, etc., todo desde el navegador.

Trabajar con Docker Compose te permite encapsular la arquitectura completa de una aplicación en un único archivo, facilitando que cualquier desarrollador, desde su portátil o desde una VM remota, pueda levantar la misma pila con un solo comando. Sumando buenas prácticas como el uso de variables de entorno, redes internas, volúmenes persistentes y, si hace falta, herramientas como Portainer, tienes una base sólida para desplegar desde pequeños proyectos personales hasta entornos bastante serios sin morir en el intento.