Docker Compose: el docker-compose.yml explicado con una aplicación real

Archivo de Docker Compose con los servicios proxy, app, db y cache, y el esquema de redes, volumen y secreto que crea al ejecutarlo

Docker Compose es la herramienta que describe una aplicación de varios contenedores en un solo archivo de texto y la levanta con una orden. En lugar de encadenar cuatro docker run con sus redes, volúmenes y variables, se escribe el docker-compose.yml una vez y se ejecuta docker compose up -d. Esta guía explica ese archivo línea a línea con una aplicación real, y reúne los comandos, los secretos, las copias de seguridad y los errores que aparecen en producción.

Si nunca ha escrito uno, empiece por la anatomía del archivo. Si ya los usa, el caso completo y la tabla de errores le ahorrarán más de una tarde.

Dónde encaja esta guía

Es la segunda de tres guías construidas sobre la misma aplicación, el servicio interno inventario. En la primera se construyó su imagen y se midió el peso de las cuatro imágenes que usa: 238,1 MB comprimidos. Aquí se describen los cuatro servicios juntos y se calcula cuánta CPU y memoria reservan. En la tercera, ese mismo archivo se despliega en un clúster.

  1. Docker — qué es, instalación, primer despliegue y comandos esenciales.
  2. Docker Compose — está leyéndola: la aplicación completa en un archivo, con redes, volúmenes y secretos.
  3. Docker Swarm — el clúster de tres nodos, los comandos avanzados y las buenas prácticas.

Qué es Docker Compose y cuándo usarlo

Una aplicación real casi nunca es un solo contenedor. Suele haber un proxy delante, la aplicación en medio y detrás una base de datos y una caché. Cada pieza necesita su imagen, sus puertos, sus variables, su red y su almacenamiento. Escribir todo eso en órdenes sueltas funciona el primer día, pero el tercer mes nadie recuerda con qué opciones se creó cada contenedor.

Compose resuelve ese problema con un archivo declarativo: se describe cómo debe quedar la aplicación y la herramienta calcula qué crear, qué cambiar y qué dejar como está. Por eso el archivo sirve a la vez de instalador, de documentación y de registro de cambios si se guarda en control de versiones.

Encaja cuando la aplicación vive en un solo servidor: entornos de desarrollo, herramientas internas, aplicaciones de una pyme o servicios de sucursal. En cambio, si hace falta repartirla entre varios servidores para que sobreviva a la caída de uno, el paso siguiente es un orquestador, y ese es el tema de la tercera guía.

docker-compose o docker compose: la diferencia importa

La primera versión se ejecutaba como docker-compose, con guion, y era un programa aparte escrito en Python. Dejó de recibir actualizaciones en 2023. La actual es un complemento del cliente de Docker y se invoca como docker compose, con espacio. Se instala con el paquete docker-compose-plugin o viene incluida en Docker Desktop, y en septiembre de 2026 va por la versión 5.5.1.

El nombre del archivo también cambió. El preferido hoy es compose.yaml, aunque se siguen aceptando docker-compose.yml y docker-compose.yaml por compatibilidad; si conviven, gana compose.yaml. Además, la antigua línea version: "3.8" del principio ya no hace nada: según la especificación oficial es solo informativa y provoca un aviso de elemento obsoleto. En esta guía se usa compose.yaml, pero todo lo que sigue vale igual para un docker-compose.yml.

Anatomía de un archivo de Docker Compose

Un archivo de Compose es YAML, un formato en el que la sangría es la sintaxis: dos espacios de más o de menos cambian el significado. Por eso nunca se usan tabuladores. Tiene seis elementos de primer nivel, y en la práctica los tres primeros bastan para casi todo.

ElementoQué defineEjemplo en la serie
servicesLos contenedores: imagen, puertos, variables, salud, límitesproxy, app, db, cache
networksLas redes virtuales que los conectanfrontend y backend
volumesEl almacenamiento que sobrevive a los contenedoresdatos-db
secretsDatos sensibles que se entregan como archivo, no como variabledb_password
configsArchivos de configuración gestionados por la plataformaSe usa en la tercera guía
nameEl nombre del proyecto, que prefija redes y volúmenesinventario

El docker-compose.yml completo de la aplicación inventario

Este es el archivo de la serie. Describe los cuatro servicios, dos redes, un volumen y un secreto, y se validó con docker compose config antes de publicarlo. Debajo se explica bloque a bloque.

name: inventario

services:
  proxy:
    image: nginx:1.30-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      app:
        condition: service_healthy
    networks: [frontend]
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: "0.25", memory: 128M }

  app:
    build: .
    image: inventario:${APP_VERSION:-1.0}
    environment:
      DB_HOST: db
      DB_NAME: inventario
      DB_USER: inventario
      DB_PASSWORD_FILE: /run/secrets/db_password
      REDIS_URL: redis://cache:6379/0
    secrets: [db_password]
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/salud')"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 10s
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks: [frontend, backend]
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: "0.50", memory: 256M }

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: inventario
      POSTGRES_USER: inventario
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
    volumes:
      - datos-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U inventario -d inventario"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks: [backend]
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: "1.00", memory: 1024M }

  cache:
    image: redis:8-alpine
    command: ["redis-server", "--maxmemory", "96mb", "--maxmemory-policy", "allkeys-lru"]
    networks: [backend]
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: "0.25", memory: 128M }

networks:
  frontend:
  backend:
    internal: true

volumes:
  datos-db:

secrets:
  db_password:
    file: ./secretos/db_password.txt

Junto al archivo viven el Dockerfile de la primera guía, la configuración del proxy y la carpeta del secreto. La configuración de nginx es mínima: todo lo que llega al puerto 80 se reenvía a la aplicación por su nombre de servicio.

# nginx.conf
server {
    listen 80;
    location / {
        proxy_pass http://app:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

El proxy: el único servicio que se publica

Solo el proxy tiene ports, así que es la única puerta de entrada. La aplicación, la base de datos y la caché no publican nada: se hablan por las redes internas. Además, la configuración se monta con :ro, de solo lectura, para que ni un fallo del propio nginx pueda modificarla. Y depends_on con service_healthy hace que el proxy espere a que la aplicación responda antes de arrancar.

La aplicación: construir, configurar y vigilar

El servicio app combina build e image: Compose construye la imagen desde el Dockerfile de la carpeta y la etiqueta como inventario:1.0. La versión sale de la variable APP_VERSION, con 1.0 por defecto, así que actualizar es cambiar un número. Por otro lado, la conexión a la base de datos se configura con variables, pero la contraseña no: se entrega como archivo en /run/secrets/db_password.

El bloque healthcheck es lo que separa un contenedor arrancado de uno que funciona. Cada 15 segundos el motor pide la ruta /salud y, si falla tres veces seguidas, marca el contenedor como no saludable. La imagen slim no trae curl, por eso la prueba usa el propio Python. Es mejor así: añadir una herramienta solo para el chequeo engorda la imagen y amplía lo que un atacante encontraría dentro.

La base de datos: el volumen es lo que importa

PostgreSQL guarda sus datos en /var/lib/postgresql/data, y esa ruta se conecta al volumen datos-db. El contenedor de la base de datos se puede borrar y recrear cuantas veces haga falta, incluso para cambiar de versión menor, y los datos siguen ahí. La prueba de salud usa pg_isready, que viene en la imagen y responde en cuanto la base de datos acepta conexiones.

La imagen oficial lee la contraseña de POSTGRES_PASSWORD_FILE. Esa variante con sufijo _FILE es una convención que siguen muchas imágenes oficiales precisamente para trabajar con secretos. Conviene comprobar en la documentación de cada imagen si la admite antes de dar por hecho que funciona.

La caché: memoria acotada por partida doble

Redis recibe dos topes distintos. El límite del contenedor, 128 MiB, es la frontera que el núcleo no deja cruzar. El de --maxmemory, 96 MB, es el que Redis vigila por sí mismo: al llegar ahí descarta las claves que llevan más tiempo sin usarse, en lugar de crecer. La diferencia entre los dos deja margen al propio proceso, y así evita que el núcleo lo mate por falta de memoria en plena carga.

Dos redes: segmentar también dentro del servidor

El proxy y la aplicación comparten frontend; la aplicación, la base de datos y la caché comparten backend. Como backend es internal: true, sus contenedores no tienen salida a internet, y el proxy ni siquiera ve la base de datos. Es la misma idea que la segmentación en VLAN, aplicada dentro de un servidor: cada pieza solo alcanza lo que necesita. Dentro de cada red, los servicios se encuentran por su nombre gracias al DNS interno del motor.

Lo que reserva la aplicación: el presupuesto de recursos

El archivo no solo arranca servicios: también fija cuánto puede consumir cada uno. Sumando los límites se obtiene el tamaño mínimo del servidor que la aloja, un dato que casi nadie calcula antes de pedir una máquina.

ServicioImagenComprimidaCPUMemoriaRedes
proxynginx:1.30-alpine26,1 MB0,25128 MiBfrontend
appinventario:1.055,8 MB0,50256 MiBfrontend y backend
dbpostgres:17-alpine117,2 MB1,001.024 MiBbackend
cacheredis:8-alpine39,0 MB0,25128 MiBbackend
Total4 servicios238,1 MB2,001.536 MiB2 redes

La lectura práctica es que la aplicación cabe con holgura en una máquina virtual de 2 vCPU y 4 GiB. Los límites suman 1,5 GiB y dejan el resto para el sistema operativo, la caché de disco y los picos. Si el servidor se dimensiona justo en 1,5 GiB, el primer pico de la base de datos lo paga todo el servidor. En la tercera guía, este mismo presupuesto se multiplica por el número de réplicas.

Variables de entorno y el archivo .env

Compose lee automáticamente un archivo .env en la misma carpeta y sustituye sus valores en el YAML. Sirve para lo que cambia entre entornos sin tocar el archivo principal: versiones, puertos, nombres de host.

# .env
APP_VERSION=1.0

# Ver el archivo final, con las variables ya sustituidas
docker compose config

La sintaxis ${APP_VERSION:-1.0} significa «usa la variable y, si no existe o está vacía, 1.0». También existe ${VARIABLE:?mensaje}, que detiene el despliegue con un error si falta, y es la opción correcta para lo que nunca debe tener un valor por defecto. Por último, un orden que conviene conocer: una variable exportada en la consola gana al .env.

Secretos: por qué la contraseña no va en el YAML

Una contraseña escrita en environment queda visible para cualquiera que ejecute docker inspect, aparece en el control de versiones si alguien sube el archivo y se hereda en los procesos hijos. En cambio, un secreto se monta como archivo dentro del contenedor, en /run/secrets/, y solo lo reciben los servicios que lo declaran. En el archivo de la serie, la caché y el proxy no lo ven.

mkdir -p secretos
chmod 700 secretos
openssl rand -base64 24 > secretos/db_password.txt
chmod 644 secretos/db_password.txt
echo "secretos/" >> .gitignore

Los permisos parecen al revés, pero tienen su lógica. En un solo servidor, Compose monta el archivo tal cual, con su dueño y sus permisos. La aplicación corre con un usuario sin privilegios que no es el dueño, así que con un 600 no podría leerlo. Lo que protege la contraseña en el servidor es la carpeta con 700, que ningún otro usuario puede abrir. En un clúster el mecanismo es distinto: los secretos se cifran y se reparten solo a los nodos que los necesitan, como muestra la tercera guía.

Arranque en orden con Docker Compose: depends_on y healthcheck

Un depends_on a secas solo garantiza el orden de creación, no que el servicio anterior esté listo. Por eso una aplicación que arranca antes que la base de datos acepte conexiones falla en el primer intento. La forma completa, con condition: service_healthy, espera a que la prueba de salud del servicio anterior dé bien.

En el archivo de la serie la cadena queda así: primero la base de datos pasa pg_isready, después arranca la aplicación y responde en /salud, y por último arranca el proxy. Si la base de datos no llega a estar sana, docker compose up se detiene y lo dice, en vez de dejar un proxy sirviendo errores 502.

Comandos de Docker Compose para el día a día

Todos se ejecutan en la carpeta del archivo. Actúan sobre el proyecto entero o, si se añade el nombre, sobre un solo servicio.

ComandoQué hace
docker compose up -dCrea y arranca todo en segundo plano; si ya existe, aplica solo los cambios
docker compose psEstado de cada servicio, con su salud
docker compose logs -f appRegistro en vivo de un servicio
docker compose exec db psql -U inventarioConsola dentro de un servicio en marcha
docker compose configValida el archivo y muestra la versión final
docker compose pullDescarga las versiones nuevas de las imágenes
docker compose up -d --buildReconstruye la imagen propia y recrea lo que cambió
docker compose restart appReinicia un servicio sin recrearlo
docker compose stopDetiene sin borrar nada
docker compose downBorra contenedores y redes; los volúmenes quedan
docker compose down -vBorra también los volúmenes: los datos se pierden
docker compose topProcesos que corren en cada contenedor
docker compose imagesImágenes y versiones en uso
docker compose run --rm app python -VEjecuta una orden puntual en un contenedor desechable

La diferencia entre down y down -v es una letra y vale una base de datos entera. Por eso conviene tenerla presente cada vez que se limpia un entorno.

Actualizar una aplicación de Docker Compose sin sorpresas

El flujo seguro tiene cuatro pasos, y el primero es el que más se salta: copia de la base de datos antes de tocar nada.

# 1. Copia previa (ver el apartado de copias)
docker compose exec -T db pg_dump -U inventario -Fc inventario > previo-$(date +%F).dump

# 2. Nueva versión de la aplicación
sed -i 's/^APP_VERSION=.*/APP_VERSION=1.1/' .env
docker compose build app

# 3. Recrear solo lo que cambió
docker compose up -d

# 4. Comprobar
docker compose ps
docker compose logs --since 5m app

Compose recrea únicamente los servicios cuya configuración o imagen cambió; los demás siguen sin interrupción. Para volver atrás basta con restaurar el número anterior en .env y repetir up -d, porque la imagen 1.0 sigue en el equipo. En un solo servidor sí hay un corte de unos segundos mientras se recrea la aplicación. Evitarlo del todo exige réplicas, y eso ya es terreno del clúster.

Perfiles y archivos de sobrescritura en Docker Compose

Un mismo proyecto suele necesitar pequeñas diferencias entre el portátil y el servidor. Compose las resuelve de dos maneras sin duplicar el archivo.

La primera es el archivo compose.override.yaml, que se aplica solo encima del principal si existe en la carpeta. En desarrollo, por ejemplo, publica el puerto de la base de datos en la dirección local para conectar una herramienta gráfica, sin que ese puerto llegue nunca a producción.

# compose.override.yaml (solo en el equipo de desarrollo)
services:
  db:
    ports:
      - "127.0.0.1:5432:5432"

La segunda son los perfiles. Un servicio con profiles: [herramientas] no arranca con un up normal, solo cuando se pide expresamente con docker compose --profile herramientas up -d. Encaja con utilidades de uso ocasional, como un panel de administración de la base de datos o un contenedor de migraciones.

Copias de seguridad de los volúmenes de Docker

Un volumen no es una copia: es un directorio en el disco del mismo servidor. Si el disco falla, se va con todo lo demás, y un RAID tampoco lo evita, porque replica al instante también un borrado o un cifrado. Hay dos formas de sacar los datos, y conviene saber cuándo usar cada una.

# Base de datos: volcado lógico, coherente y en caliente
docker compose exec -T db pg_dump -U inventario -Fc inventario > inventario-$(date +%F).dump

# Restaurar ese volcado
docker compose exec -T db pg_restore -U inventario -d inventario --clean < inventario-2026-09-18.dump

# Cualquier volumen: copia en frío del contenido, con el servicio detenido
docker compose stop db
docker run --rm -v inventario_datos-db:/origen:ro -v "$PWD":/destino alpine \
  tar czf /destino/datos-db-$(date +%F).tar.gz -C /origen .
docker compose start db

Para una base de datos, el volcado lógico es el método correcto: se hace en caliente y siempre es coherente. Copiar los archivos de un PostgreSQL en marcha, en cambio, puede producir una copia que no arranca. El nombre del volumen lleva delante el del proyecto, por eso es inventario_datos-db y no datos-db. Y el archivo resultante tiene que salir del servidor: una copia que vive junto al original no protege de casi nada. La guía del respaldo inmutable explica por qué, además, conviene que no se pueda borrar.

Errores frecuentes en un docker-compose.yml

SíntomaCausaSolución
yaml: line 12: did not find expected keySangría mezclada o un tabuladorSolo espacios, de dos en dos; docker compose config señala la línea
the attribute version is obsoleteLínea version: heredadaBorrarla: no hace nada
La aplicación no conecta con dbEstán en redes distintas o se usó localhostMisma red y el nombre del servicio como host
Error 502 en el proxy tras un reinicioLa aplicación aún no respondíadepends_on con service_healthy y un healthcheck real
Los cambios del YAML no se aplicanSe ejecutó restart, que no recreadocker compose up -d, que recrea lo que cambió
La base de datos quedó vacíaSe ejecutó down -v o cambió el nombre del proyectoRestaurar el volcado; fijar name: en el archivo
Una variable llega vacíaFalta en .env y no tiene valor por defecto${VARIABLE:?falta} para que falle al desplegar

El sexto error merece explicación. Si no se fija name:, Compose toma el nombre de la carpeta como nombre del proyecto. Así, basta con renombrar la carpeta, o copiarla con otro nombre, para que Compose cree un volumen nuevo y vacío, y parezca que los datos desaparecieron. Siguen en el volumen antiguo, pero el susto está garantizado.

Preguntas frecuentes sobre Docker Compose

¿Sirve Compose para producción?

Sí, para aplicaciones que viven en un servidor y toleran unos segundos de corte al actualizar, que son la mayoría de las herramientas internas de una empresa. Lo que no ofrece es tolerancia a la caída del servidor. Si eso es un requisito, hace falta un clúster, y esa es la frontera exacta entre esta guía y la siguiente.

¿Qué hace restart: unless-stopped?

Reinicia el contenedor si se cae y también al encender el servidor, salvo que alguien lo haya detenido a mano. Ese matiz tiene una consecuencia práctica: un contenedor detenido con docker stop antes de un reinicio no vuelve a arrancar solo. Para apagar un servidor basta con apagarlo; el motor detiene los contenedores sin marcarlos como parados a mano.

¿Dónde se guarda el archivo?

En un repositorio de control de versiones, junto con el Dockerfile y la configuración, y sin la carpeta de secretos. Así cada cambio queda registrado con su autor y su fecha, y reconstruir el servicio en otro servidor es clonar y ejecutar up -d.

Qué llevarse sobre Docker Compose

Docker Compose convierte una aplicación de varios contenedores en un archivo que se lee, se versiona y se ejecuta con una orden. Lo que hace que ese archivo aguante en producción son cinco decisiones: versiones fijadas, pruebas de salud reales, secretos fuera del YAML, redes separadas y límites de recursos. Con ellos, la aplicación de la serie reserva 2 CPU y 1.536 MiB, y cabe en una máquina de 2 vCPU y 4 GiB.

Operar ese servidor, sus copias y sus actualizaciones es parte del servicio de infraestructura de TI de KHARONTE. Por su parte, la siguiente guía da el paso que Compose no puede dar: repartir estos mismos cuatro servicios entre tres servidores con un clúster que sobrevive a la caída de uno.

Compartir este artículo

Últimas entradas

Escríbanos ahora