SSD Nodes Learn 8GB de RAM — $66/año
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-02

Docker Compose con varios archivos: orden y puertos

Aprende cómo carga compose.override.yaml, cómo se combinan los archivos, por qué ports puede mantener un puerto abierto y cómo separar dev y prod con 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, los valores del archivo posterior prevalecen cuando entran 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 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 una simple sobrescritura. Los mapas se combinan clave por clave, las secuencias se agregan y un conjunto reducido de campos se sustituye por completo. Esta diferencia provoca sorpresas. La lista ports es la que causa problemas a casi todo el mundo.

Todo lo siguiente presupone Compose v2 y el complemento docker compose, no el script antiguo docker-compose. Ejecuta docker compose version para comprobarlo. Si todavía no has escrito un archivo de Compose, empieza por la guía básica de Docker Compose y vuelve después.

El archivo de sobrescritura que Compose carga sin indicarlo

Ejecute docker compose up sin la opción -f. Compose busca en el directorio de trabajo y, después, en sus directorios padre, compose.yaml o docker-compose.yaml. Si hay un archivo de sobrescritura junto al archivo base, Compose carga también ese segundo archivo automáticamente.

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 reconoce Compose son compose.override.yaml, compose.override.yml y los nombres antiguos docker-compose.override.yml y docker-compose.override.yaml. Cualquier otro nombre, por ejemplo compose.dev.yaml, solo se carga si se indica con -f.

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

En un servidor, esto puede causar problemas. Un archivo de sobrescritura que queda en el directorio de despliegue se carga con cada comando docker compose ejecutado desde ese directorio, incluido el que ejecuta el trabajo de cron. Así, una pila de producción puede terminar montando mediante bind un directorio de código fuente que nadie pretendía incluir. Ejecute docker compose config después de cada despliegue y revise el resultado.

Orden con -f y dónde se resuelven las rutas relativas

Compose crea la configuración en el orden en que se proporcionan 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á trabajando con un modelo combinado diferente. Es una forma rápida de obtener un servicio que Compose indica que no existe. Defina 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 comandos del shell. Cualquier valor definido explícitamente en la línea de comandos tiene prioridad sobre la variable de entorno.

Ahora, la regla que causa problemas con los montajes bind. 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 nada dentro. Esto parece una pérdida de datos, pero no lo es. Pase --project-directory para definir manualmente 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 eso, cambiar qué archivo aparece primero puede cambiar el nombre del proyecto. Un proyecto renombrado implica nuevos nombres de contenedor y de volumen. 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 solo 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.
  • Las asignaciones se combinan clave por clave. environment, labels, volumes y devices conservan todas las claves de ambos archivos. El archivo posterior prevalece para cualquier clave presente en ambos. Para environment y labels, la clave es el nombre de la variable o de la etiqueta. Para 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 eso, las entradas que coinciden en esa clave se combinan en lugar de añadirse. volumes, secrets y configs coinciden mediante target. ports coincide mediante la combinación de ip, target, published y protocol.

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

Por qué el puerto sigue publicado después de la anulación

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

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

Una anulación que lo vincula solo a localhost, porque habrá un proxy inverso delante:

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

Comprueba el resultado antes de dar por hecho que funcionó.

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 lo que son dos puertos distintos para la combinación de archivos, y la vinculación pública que intentaste 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 es 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 requiere Compose v2.24.4 o posterior. La solución portable no necesita ninguna etiqueta: mantén ports fuera del archivo base y decláralo únicamente en los archivos específicos de cada entorno. Si no hay nada que combinar, no hay nada que exponer. Ese es el patrón utilizado en el ejemplo completo siguiente.

Eliminar un valor que establece el archivo base

!reset elimina un atributo y lo devuelve a su valor predeterminado o a null. Requiere un valor, pero lo ignora, así que escriba algo válido y vacío.

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

!reset requiere Compose v2.24 o una versión posterior. Úselo cuando no pueda editar el archivo base, por ejemplo, cuando se trate de un fragmento de un proveedor que incluya.

include, para pilas ensambladas a partir de componentes

include incorpora otra aplicación de Compose al modelo. Es un elemento de nivel superior, no una opción.

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

Cada ruta de include se carga como un modelo de aplicación de Compose independiente, con su propio directorio del proyecto. Por eso, las rutas relativas dentro de ese archivo se resuelven respecto a su propio directorio. Esta es la diferencia real con -f y el motivo por el que include es la herramienta adecuada cuando el fragmento está en otra carpeta o en otro repositorio.

La forma extendida 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 combinan según las reglas normales antes de que el resultado se incorpore al 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. Esto evita que un fragmento compartido lea silenciosamente el .env de su proyecto. include requiere Compose v2.20.0 o una versión posterior.

Los nombres de recursos duplicados entre su archivo y un archivo incluido se notifican como un error en lugar de combinarse silenciosamente. Esto es intencionado. Para cambiar algo declarado por un archivo incluido, coloque el cambio en compose.override.yaml: la sustitución se aplica al modelo ensamblado, por lo que puede modificar recursos incluidos sin entrar en conflicto con ellos.

En resumen: include compone aplicaciones independientes y -f añade capas de configuración a una sola aplicación.

Separar desarrollo y producción en un mismo 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, no solo a un contenedor que exista. 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. Consulta archivos de entorno y secretos de Compose para conocer 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 reemplaza el valor predeterminado de la imagen porque es un valor único. LOG_LEVEL reemplaza info porque environment combina por clave. El montaje bind y los dos puertos publicados son adiciones directas. El puerto de la base de datos se enlaza 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. Compose no busca un archivo con ese nombre, por lo que nunca se carga accidentalmente.

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

En el VPS debes indicar ambos archivos. Precisamente al indicarlos excluyes el archivo de override.

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 debe mostrar ambos servicios en ejecución, y db debe mostrar (healthy). Como pasaste -f, compose.override.yaml no se leyó. 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 solo está disponible en localhost y queda listo para un proxy. Consulta ejecutar varias aplicaciones detrás de Traefik cuando añadas el segundo servicio.

Configura COMPOSE_FILE=compose.yaml:compose.prod.yaml en el archivo .env del servidor. Después, el resto de tus comandos volverá a ser un docker compose logs -f app sin opciones.

Lea el modelo combinado antes de implementarlo

docker compose config imprime el modelo completamente combinado y con todas las variables interpoladas. 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 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. Úselo antes de pegar la salida en otro lugar, porque config sin opciones imprime todos los secretos resueltos en texto sin cifrar. --services muestra solo los nombres de los servicios. Es una forma rápida de confirmar que un include incorporó 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 en los directorios principales el archivo base predeterminado, pero no busca en ninguna ubicación un archivo que haya especificado manualmente.

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

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

Un bind mount está vacío y Docker creó un directorio que no solicitó. La ruta relativa se resolvió con 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 ahí con el prefijo anterior; docker volume ls lo mostrará.

Un puerto que eliminó en el archivo de sustituciones sigue abierto. La combinación ports lo añadió 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 compose.override.yaml automáticamente?

Sí, cuando ejecuta docker compose sin la opción -f. Compose busca compose.yaml o docker-compose.yaml en el directorio de trabajo y sus directorios padre. Si encuentra un archivo de override junto a él, carga ese archivo 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 archivos anteriores, por lo que el último archivo de la línea prevalece en cualquier conflicto. Debe usar la misma lista para 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 no incluya ports en el 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. Las rutas relativas de todos los archivos se resuelven respecto al directorio del primer archivo. include incorpora una aplicación de 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 otro lugar. include requiere Compose v2.20.0 o posterior.

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

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