SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-10-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 crear 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 hay valores en conflicto. Hay dos mecanismos para hacerlo desde la línea de comandos: un archivo de anulación que Compose carga automáticamente y la opción -f, que se especifica manualmente. El tercero está dentro del propio archivo: el elemento include. Funciona de forma diferente a los otros dos.

La combinación no consiste en sobrescribir sin más. Los mapas se combinan clave por clave, las secuencias se concatenan y un conjunto reducido de campos se reemplaza por completo. Esta diferencia es la causa de las sorpresas. La lista ports es la que más problemas causa.

Todo lo que sigue presupone Compose v2 o posterior, con el complemento docker compose en lugar del antiguo script docker-compose. Las versiones actuales se numeran como v5 y el complemento funciona de la misma manera. 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 en 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 algo válido y vacío.

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

!reset funciona con Compose v2.18.0 o posterior, varias versiones antes de que apareciera !override. Úselo cuando no pueda editar el archivo base, por ejemplo, cuando se trate de 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 al modelo. Es un elemento de nivel superior, no un flag.

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

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

La forma larga admite 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 combinan según las reglas normales antes de incorporar el resultado al modelo. project_directory establece la ruta base que se usa para resolver las rutas relativas del archivo incluido. env_file indica el archivo del que el archivo de Compose incluido obtiene sus valores predeterminados de interpolación. De forma predeterminada, es el .env del propio directorio de proyecto de ese archivo, y el entorno del proyecto local sigue teniendo prioridad. Así puede sobrescribir una de sus variables sin editar el fragmento. include requiere Compose v2.20.0 o posterior. Las mismas opciones sirven para añadir un complemento de un solo contenedor a un stack que ya ejecuta, por ejemplo Halcyon, que cambia el aspecto de una biblioteca de Jellyfin para convertirla en un videoclub de los años 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 su stack multimedia.

La documentación de Docker indica que un nombre de recurso declarado tanto en su archivo como en un archivo incluido se rechaza como conflicto. El loader de la versión actual no lo hace: a fecha de Compose v5.6.1, comprobado contra el código fuente del loader incluido en esa versión el 2026-10-27, combina ambas definiciones. El archivo incluido conserva el valor de su archivo cuando no hay conflicto, y su archivo tiene prioridad cuando existe uno. Por tanto, no se detiene con un error. No dé por hecho que un nombre duplicado fallará de forma explícita. Para cambiar algo declarado por un archivo incluido, ponga el cambio en compose.override.yaml. La sobrescritura se aplica al modelo ensamblado, de modo que el cambio queda explícito y no depende de cómo una versión concreta trate el conflicto. Esta práctica resulta especialmente útil con un stack cuyo archivo ascendente se reescribe en cada versión, como los servidores de fotos con varios contenedores comparados en PhotoPrism frente a Immich, donde una asociación con localhost o un volumen adicional debe estar en su sobrescritura, no en el archivo que reemplazará la siguiente actualización.

La versión corta: include compone aplicaciones independientes, mientras que -f añade capas de configuración a una 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 en sus directorios padre compose.yaml o docker-compose.yaml. Si hay un archivo de sustitución 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 genera la configuración en el orden en que proporciona los archivos. Cada archivo sustituye y amplía los archivos 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 sustituirlo?

Porque las entradas ports se identifican mediante el conjunto completo de ip, target, published y protocol. Una sustitución 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 mantenga ports fuera del archivo base para que no haya nada con lo que combinarlo.

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

-f combina varios archivos en una sola aplicación. Las rutas relativas de todos los archivos se resuelven respecto al directorio del primer archivo. include incorpora una aplicación Compose independiente. Cada archivo incluido conserva su propio directorio del proyecto, por lo que 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 otra ubicación. 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.18.0 o posterior. Escriba ports: !reset [] o MY_VAR: !reset null en el archivo de sustitución. El atributo volverá a su valor predeterminado o a null. El valor proporcionado a la etiqueta es obligatorio, pero se ignora. Si quiere sustituir un atributo en lugar de borrarlo, !override lo hace y requiere v2.24.4 o posterior.

¿Te resultó útil este artículo?