SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-28

Docker Compose: varios archivos y reglas de combinación

Aprende cómo se carga compose.override.yaml, qué orden aplica al combinar archivos, por qué ports puede mantener un puerto abierto y cómo usar include.

Qué hace Compose con más de un archivo

Docker Compose puede construir un proyecto a partir de varios archivos. Los lee en el orden en que los recibe y los combina en un único modelo. Por tanto, el archivo posterior prevalece cuando un valor entra en conflicto. Hay dos mecanismos para hacerlo desde la línea de comandos: un archivo de sustitución que Compose carga automáticamente y la opción -f, que se indica manualmente. El tercero está dentro del propio archivo: el elemento include. Su funcionamiento es diferente al de los otros dos.

La combinación no consiste en una simple sustitución. Los mapas se combinan clave por clave, las secuencias se concatenan y un conjunto reducido de campos se reemplaza por completo. Estas diferencias son la causa de los resultados inesperados. La lista ports es la que más problemas causa.

Todo lo que sigue presupone el uso de Compose v2 y del complemento docker compose, no del antiguo script docker-compose. Ejecute docker compose version para comprobarlo. Si todavía no ha escrito un archivo de Compose, empiece por la guía básica de Docker Compose y vuelva después.

El archivo de override que Compose carga sin indicarlo

Ejecute docker compose up sin la opción -f y Compose buscará en el directorio de trabajo y, después, en sus directorios superiores, compose.yaml o docker-compose.yaml. Si hay un archivo de override junto al archivo base, Compose cargará también ese segundo archivo de forma automática.

ls compose.yaml compose.override.yaml
docker compose up -d

Si ambos archivos están presentes, el resultado es el mismo que escribirlos manualmente.

docker compose -f compose.yaml -f compose.override.yaml up -d

Los nombres que Compose reconoce son compose.override.yaml, compose.override.yml y los antiguos docker-compose.override.yml y docker-compose.override.yaml. Cualquier otro nombre, por ejemplo compose.dev.yaml, sólo se carga si se indica con -f.

En cuanto se pasa un -f, se detiene la carga automática. docker compose -f compose.yaml up lee exactamente ese archivo e ignora el override. Esta es la propiedad en la que se basa el patrón de desarrollo y producción que se explica más adelante en esta guía.

Esto funciona en ambos sentidos en un servidor. Un archivo de anulación que quede en el directorio de despliegue se carga con todos los comandos docker compose sin opciones que se ejecuten desde ese directorio, incluido el que ejecuta la tarea de cron. Así es como una pila de producción termina montando mediante bind un directorio de código fuente que nadie pretendía distribuir. Ejecute docker compose config después de cada despliegue y revise el resultado. Cuando el despliegue no requiere intervención, la comprobación sólo sirve si alguien recibe un aviso cuando algo falla. Para eso puede usar un canal de envío como un servidor ntfy autohospedado, al que una tarea de cron o una unidad OnFailure de systemd puede enviar notificaciones.

Orden de los archivos con -f y resolución de las rutas relativas

Compose crea la configuración en el orden en que proporciona los archivos. Los archivos posteriores sobrescriben y amplían los anteriores. De izquierda a derecha, prevalece el último.

docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d

Todos los comandos de ese proyecto necesitan la misma lista de archivos. Si ejecuta up con dos archivos y logs con uno, está interactuando con un modelo combinado diferente. Es una forma rápida de obtener un servicio que Compose indica que no existe. El riesgo aumenta en una pila cuyas actualizaciones se ejecutan mediante comandos independientes, como el paso de migración de la base de datos en una mesa de ayuda Chatwoot autoalojada, donde un docker compose run emitido con una lista de archivos incorrecta apunta silenciosamente a un modelo diferente del que ya utilizan los servicios. Establezca la lista una sola vez con la variable de entorno COMPOSE_FILE.

export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -d

El separador es : en Linux, y COMPOSE_PATH_SEPARATOR lo cambia. COMPOSE_FILE también puede estar en el archivo .env del proyecto. Así forma parte del checkout y no del historial de su shell. Cualquier valor establecido explícitamente en la línea de comandos prevalece sobre la variable de entorno.

Esta es la regla que provoca problemas en los bind mounts. Cuando usa varios archivos con -f, todas las rutas relativas de todos esos archivos se resuelven respecto al directorio del primer archivo, no respecto al archivo que las contiene. Escriba ./data:/var/lib/postgresql/data dentro de deploy/prod/compose.prod.yaml y Compose seguirá buscando ./data junto al archivo base. Docker crea entonces un directorio vacío en esa ruta incorrecta y el contenedor se inicia sin contenido. Esto parece una pérdida de datos, pero no lo es. Pase --project-directory para establecer usted mismo la ruta base, o use include, que resuelve cada archivo respecto a su propio directorio.

El nombre del proyecto procede de ese mismo directorio base. Por tanto, cambiar cuál es el primer archivo puede cambiar el nombre del proyecto. Un proyecto renombrado implica nuevos nombres de contenedores y de volúmenes. El volumen anterior sigue en el disco con el nombre antiguo. Fije el nombre con name: en el nivel superior del archivo base.

name: myapp

Qué campos se combinan y cuáles se reemplazan

Compose combina según el tipo del valor, no según el nombre del campo.

  • Los campos con un único valor se reemplazan. image, command, entrypoint y mem_limit toman directamente el valor posterior. No puede añadir un argumento a un command, porque la sustitución reescribe toda la línea.
  • Los mapas se combinan clave por clave. environment, labels, volumes y devices conservan todas las claves de ambos archivos; el archivo posterior prevalece cuando una clave aparece en los dos. En environment y labels, la clave es el nombre de la variable o de la etiqueta. En volumes y devices, la clave es la ruta del contenedor.
  • Las secuencias se concatenan. dns, dns_search, expose, tmpfs y external_links se concatenan. Una configuración base que contiene expose: ["3000"] combinada con una sustitución que contiene ["4000", "5000"] produce ["3000", "4000", "5000"].

Cuatro secuencias tienen una clave de identidad, por lo que las entradas que coinciden en esa clave se combinan en lugar de concatenarse. volumes, secrets y configs coinciden según target. ports coincide según la combinación de ip, target, published y protocol.

Lea dos veces la regla de ports, porque es el punto problemático. Dos entradas de puertos sólo son la misma entrada cuando coinciden las cuatro partes. Si cambia cualquiera de ellas, Compose considera que existe un segundo puerto independiente y conserva ambos.

Por qué el puerto sigue publicado después del override

Un archivo base que publica un servicio en todas las interfaces:

services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"

Un override escrito para enlazarlo sólo a localhost, porque habrá un reverse proxy delante:

services:
  web:
    ports:
      - "127.0.0.1:8080:80"

Compruebe el resultado antes de dar por hecho que ha funcionado.

docker compose -f compose.yaml -f compose.prod.yaml config

Ambas entradas aparecen en la salida. La parte ip es diferente: 0.0.0.0 frente a 127.0.0.1. Por tanto, para la combinación son dos puertos distintos y el enlace público que intentó eliminar sigue presente en el modelo. Esto es más importante en Docker que en otros entornos, porque un puerto publicado se escribe en iptables antes que las reglas del firewall. El mecanismo se explica en por qué los puertos publicados de Docker pasan por alto ufw.

Hay dos soluciones. La explícita usa la etiqueta !override, que reemplaza todo el atributo y omite las reglas de combinación:

services:
  web:
    ports: !override
      - "127.0.0.1:8080:80"

!override necesita Compose v2.24.4 o una versión posterior. La solución portable no necesita ninguna etiqueta: mantenga ports fuera del archivo base y declárelo sólo en los archivos específicos de cada entorno. Si no hay nada que combinar, no hay nada que se filtre. Este es el patrón usado en el ejemplo completo siguiente.

Eliminar un valor establecido por el archivo base

!reset elimina un atributo y lo devuelve a su valor predeterminado o a null. Recibe un valor y lo ignora, por lo que debe escribir un valor válido y vacío.

services:
  web:
    ports: !reset []
    environment:
      DEBUG: !reset null

!reset requiere Compose v2.24 o posterior. Úselo cuando no pueda editar el archivo base, por ejemplo, un fragmento de un proveedor que incorpora. Una pila publicada por el proyecto original es exactamente ese caso: el archivo Compose de un espacio de trabajo AFFiNE autohospedado declara cuatro contenedores que usted no escribió, y !reset permite borrar un atributo de uno de ellos sin bifurcar el archivo ni asumir la tarea de mantenerlo actualizado.

include, para ensamblar stacks a partir de componentes

include incorpora otra aplicación de Compose a tu modelo. Es un elemento de nivel superior, no un flag.

include:
  - path: ../commons/compose.yaml

Cada ruta de include se carga como su propio modelo de aplicación de Compose, con su propio directorio de proyecto. Por tanto, las rutas relativas de ese archivo se resuelven respecto a su propio directorio. Esa es la diferencia real frente a -f y la razón por la que include es la herramienta adecuada cuando el fragmento está en otra carpeta u otro repositorio. Esta es la estructura habitual de un stack de un proveedor que no has escrito: el archivo de Compose con varios servicios de una instalación SSO de Authentik autohospedada puede estar en su propio directorio y conservar sus rutas relativas, mientras tu archivo sigue centrado en tus propios servicios.

La forma larga acepta subopciones.

include:
  - path:
      - ../monitoring/compose.yaml
      - ../monitoring/compose.vps.yaml
    project_directory: ../monitoring
    env_file: ../monitoring/.env

path acepta una lista, y esos archivos se fusionan siguiendo las reglas normales antes de incorporar el resultado a tu modelo. project_directory establece la ruta base que se usa para resolver las rutas relativas del archivo incluido. env_file proporciona al archivo incluido sus propias variables para la interpolación, lo que impide que un fragmento compartido lea silenciosamente el .env de tu proyecto. include requiere Compose v2.20.0 o posterior. Las mismas opciones sirven para añadir un contenedor individual a un stack que ya ejecutas, por ejemplo Halcyon, que transforma una biblioteca de Jellyfin para simular un videoclub de los 90: su archivo conserva su propia etiqueta de imagen y su propio env_file, por lo que actualizarlo nunca implica modificar el archivo donde reside tu stack multimedia.

Los nombres de recursos duplicados entre tu archivo y un archivo incluido se notifican como un error en lugar de fusionarse silenciosamente, y es una decisión intencionada. Para cambiar algo declarado por un archivo incluido, coloca el cambio en compose.override.yaml: la sustitución se aplica al modelo ensamblado, por lo que puede modificar recursos incluidos sin colisionar con ellos. Esta práctica resulta especialmente útil con un stack cuyo archivo original se reescribe en cada versión, como los servidores de fotos con varios contenedores analizados en PhotoPrism frente a Immich, donde una vinculación a localhost o un volumen adicional deben estar en tu sustitución y no en el archivo que la siguiente actualización reemplazará.

En resumen: include compone aplicaciones separadas; -f superpone configuración sobre una sola aplicación.

Separación de desarrollo y producción en un solo VPS

Este es el patrón completo en tres archivos. El archivo base declara lo que es válido en todos los entornos y no publica ningún puerto.

name: myapp

services:
  app:
    image: ghcr.io/example/app:1.4.2
    environment:
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
      LOG_LEVEL: info
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  db_data:

La condición depends_on hace que la aplicación espere a una base de datos que responda, en lugar de esperar sólo a que exista un contenedor. Se explica en comprobaciones de estado y condiciones de depends_on. POSTGRES_PASSWORD se interpola desde el archivo .env del proyecto, que nunca debe estar en git. Consulte archivos env y secretos de Compose para conocer las variantes más seguras.

A continuación, compose.override.yaml, que Compose carga automáticamente. Este es el archivo del desarrollador.

services:
  app:
    build: .
    command: npm run dev
    environment:
      LOG_LEVEL: debug
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src

  db:
    ports:
      - "127.0.0.1:5432:5432"

En un portátil, un docker compose up sin opciones combina esos dos archivos. command sustituye el valor predeterminado de la imagen porque es un valor único. LOG_LEVEL sustituye info porque environment combina los valores por clave. El montaje bind y los dos puertos publicados son adiciones directas. El puerto de la base de datos está enlazado a localhost para que un portátil conectado a una red compartida no exponga PostgreSQL al resto de la red.

Por último, compose.prod.yaml. Su nombre no es uno de los que Compose busca, por lo que nunca se carga por accidente.

services:
  app:
    ports:
      - "127.0.0.1:8000:3000"
    deploy:
      resources:
        limits:
          memory: 512M

En el VPS debe indicar ambos archivos. Precisamente esa indicación excluye el archivo de sustituciones.

docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml ps

ps debería mostrar ambos servicios en ejecución y db debería mostrar (healthy). Como se pasó -f, no se leyó compose.override.yaml. Por tanto, el comando de desarrollo, el montaje bind del código fuente y el puerto público 3000 no pueden llegar a producción aunque el archivo esté en el mismo directorio. El puerto 8000 sólo está disponible en localhost y queda listo para un proxy. Consulte ejecutar varias aplicaciones detrás de Traefik cuando añada el segundo servicio.

Establezca COMPOSE_FILE=compose.yaml:compose.prod.yaml en el .env del servidor. A partir de ahí, el resto de los comandos volverá a ser un docker compose logs -f app simple.

Una pila con un solo servicio sigue la misma estructura, porque un rastreador de entrenamientos openGym autohospedado debe responder mediante TLS detrás de un proxy antes de registrar la primera passkey. Además, un archivo base sin ports evita que una publicación pública accidental se adelante al proxy.

Lee el modelo combinado antes de desplegar

docker compose config muestra el modelo completamente combinado y con todas las interpolaciones aplicadas. No es una vista previa. Es la entrada exacta sobre la que actuará Compose. Si la salida no coincide con lo esperado, la salida es la referencia correcta.

docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml config --no-interpolate
docker compose -f compose.yaml -f compose.prod.yaml config --services

--no-interpolate deja ${VAR} sin expandir. Úsalo antes de pegar la salida en otro lugar, porque config sin opciones muestra todos los secretos resueltos en texto sin cifrar. --services muestra sólo los nombres de los servicios. Es una forma rápida de confirmar que un include ha incorporado lo esperado.

Modos de fallo y lo que verá

no configuration file provided: not found. Compose no encontró nada que leer. Está fuera del directorio del proyecto o COMPOSE_FILE indica una ruta que no existe. Compose busca el archivo base predeterminado en los directorios superiores, pero no busca en ninguna ubicación un archivo que haya especificado explícitamente.

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. La interpolación se resuelve usando el archivo .env del proyecto y el entorno del shell. En este caso, el directorio del proyecto es el directorio del primer archivo -f. Si realiza el despliegue desde un directorio distinto del que contiene .env, verá esta advertencia y después tendrá una base de datos que rechaza todas las conexiones.

La edición del archivo de override no aparece en docker compose config. Puede que haya pasado -f, lo que desactiva la carga automática del override, o que Compose haya encontrado compose.yaml en un directorio superior y el archivo de override no esté junto a él. Ejecutar docker compose config sin otros argumentos indica qué modelo está construyendo realmente Compose.

Un bind mount está vacío y Docker creó un directorio que no había solicitado. La ruta relativa se resolvió respecto al directorio del primer archivo. Corrija la ruta, pase --project-directory o mueva el fragmento detrás de include.

Los contenedores vuelven con nombres nuevos y un volumen parece vacío. El nombre del proyecto cambió porque depende del directorio del primer archivo. Añada un name: de nivel superior al archivo base para que los nombres dejen de cambiar. El volumen antiguo sigue existiendo con el prefijo anterior; docker volume ls lo mostrará.

Un puerto que eliminó en el override sigue abierto. La combinación ports añadió el puerto en lugar de reemplazarlo. Confírmelo con docker compose config y, después, use !override o mueva ports fuera del archivo base.

FAQ

¿Compose carga automáticamente compose.override.yaml?

Sí, cuando ejecuta docker compose sin la opción -f. Compose busca en el directorio de trabajo y sus directorios padre compose.yaml o docker-compose.yaml. Si hay un archivo de override junto a él, Compose lo carga en segundo lugar. Los nombres reconocidos son compose.override.yaml, compose.override.yml, docker-compose.override.yml y docker-compose.override.yaml. Pasar cualquier -f desactiva este comportamiento, por lo que docker compose -f compose.yaml up lee un solo archivo.

¿En qué orden se combinan varios archivos -f?

De izquierda a derecha. Compose crea la configuración en el orden en que proporciona los archivos. Cada archivo sobrescribe y amplía los anteriores, por lo que el último archivo de la línea prevalece en cualquier conflicto. Debe usar la misma lista en todos los comandos de ese proyecto. Para eso sirve COMPOSE_FILE=compose.yaml:compose.prod.yaml.

¿Por qué el puerto sigue publicado después de sobrescribirlo?

Porque las entradas ports se identifican mediante el conjunto completo de ip, target, published y protocol. Una sobrescritura de 127.0.0.1:8080:80 sobre una base de 8080:80 difiere en la parte ip. Por eso Compose la trata como un segundo puerto y conserva ambos. Ejecute docker compose config para ver las dos entradas. Use ports: !override en Compose v2.24.4 o posterior, o quite ports del archivo base para que no haya nada que combinar.

¿Cuál es la diferencia entre include y -f?

-f combina varios archivos en una sola aplicación, y todas las rutas relativas de todos los archivos se resuelven respecto al directorio del primer archivo. include incorpora una aplicación Compose independiente, y cada ruta incluida conserva su propio directorio del proyecto. Por tanto, sus rutas relativas se resuelven respecto a ese directorio. Use -f para las capas de entorno de su propia pila y include para un fragmento mantenido en otro lugar. include requiere Compose v2.20.0 o posterior.

¿Cómo elimino un valor definido por el archivo base?

Use la etiqueta !reset con Compose v2.24 o posterior. Escriba ports: !reset [] o MY_VAR: !reset null en el archivo de sobrescritura para que el atributo vuelva a su valor predeterminado o pase a null. El valor que proporcione a la etiqueta es obligatorio, pero se ignora. Si quiere reemplazar un atributo en lugar de borrarlo, !override realiza esa operación y requiere v2.24.4 o posterior.