SSD Nodes Learn 8GB de RAM — $66/an
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-02

Docker Compose : fusionner plusieurs fichiers correctement

Découvrez comment compose.override.yaml se charge seul, l’ordre réel de fusion, le piège des ports qui restent ouverts et l’usage de include pour séparer dev et prod.

Ce que Compose fait avec plusieurs fichiers

Docker Compose peut créer un seul projet à partir de plusieurs fichiers. Il les lit dans l’ordre où ils sont fournis et les fusionne dans un modèle unique. Le fichier suivant l’emporte donc sur toute valeur en conflit. Deux mécanismes permettent cela depuis la ligne de commande : un fichier d’override que Compose charge automatiquement et l’option -f que vous fournissez manuellement. Un troisième mécanisme se trouve dans le fichier lui-même, avec l’élément include. Il fonctionne différemment des deux autres.

La fusion ne consiste pas en un simple écrasement. Les mappings sont fusionnés clé par clé, les séquences sont ajoutées les unes aux autres et un petit nombre de champs sont entièrement remplacés. Cette différence est à l’origine des comportements inattendus. La liste ports est celle qui pose problème à presque tout le monde.

Tout ce qui suit suppose que vous utilisez Compose v2, avec le plugin docker compose et non l’ancien script docker-compose. Exécutez docker compose version pour vérifier. Si vous n’avez pas encore écrit de fichier Compose, commencez par le guide sur les bases de Docker Compose, puis revenez ici.

Le fichier de surcharge que Compose charge sans indication

Exécutez docker compose up sans l’option -f. Compose recherche alors, dans le répertoire de travail puis dans ses répertoires parents, compose.yaml ou docker-compose.yaml. Si un fichier de surcharge se trouve à côté du fichier de base, Compose le charge ensuite automatiquement.

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

Lorsque les deux fichiers sont présents, cela revient à les saisir manuellement.

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

Les noms reconnus par Compose sont compose.override.yaml, compose.override.yml, ainsi que les anciens docker-compose.override.yml et docker-compose.override.yaml. Tout autre nom, par exemple compose.dev.yaml, n’est chargé que si vous le spécifiez avec -f.

Dès que vous spécifiez un -f, le chargement automatique s’arrête. docker compose -f compose.yaml up lit exactement ce fichier et ignore le fichier de surcharge. C’est sur cette propriété que repose le modèle dev et prod présenté plus loin dans ce guide.

Cela peut aussi poser problème sur un serveur. Un fichier de surcharge laissé dans le répertoire de déploiement est chargé par chaque commande docker compose exécutée depuis ce répertoire, y compris celle lancée par votre tâche cron. La stack de production peut alors monter par liaison un répertoire source que personne ne voulait déployer. Exécutez docker compose config après chaque déploiement et vérifiez le résultat.

Ordre avec -f et résolution des chemins relatifs

Compose construit la configuration dans l’ordre où vous fournissez les fichiers. Les fichiers suivants remplacent et complètent les précédents. De gauche à droite, le dernier fichier est prioritaire.

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

Toutes les commandes de ce projet doivent utiliser la même liste de fichiers. Si vous exécutez up avec deux fichiers et logs avec un seul, vous manipulez un modèle fusionné différent. Vous risquez alors rapidement d’obtenir une erreur indiquant que le service n’existe pas dans Compose. Définissez plutôt la liste une seule fois avec la variable d’environnement COMPOSE_FILE.

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

Le séparateur est : sous Linux, et COMPOSE_PATH_SEPARATOR le modifie. COMPOSE_FILE peut aussi être défini dans le fichier .env du projet. Il fait alors partie du checkout et non de l’historique de votre shell. Toute valeur définie explicitement sur la ligne de commande est prioritaire sur la variable d’environnement.

Voici maintenant la règle qui provoque des problèmes avec les bind mounts. Lorsque vous utilisez plusieurs fichiers avec -f, tous les chemins relatifs de ces fichiers sont résolus par rapport au répertoire du premier fichier, et non par rapport au fichier qui les contient. Écrivez ./data:/var/lib/postgresql/data dans deploy/prod/compose.prod.yaml, et Compose cherchera quand même ./data à côté du fichier de base. Docker crée alors un répertoire vide à cet emplacement incorrect, puis le conteneur démarre sans son contenu. Cela ressemble à une perte de données, mais ce n’en est pas une. Utilisez --project-directory pour définir vous-même le chemin de base, ou utilisez include, qui résout chaque fichier par rapport à son propre répertoire.

Le nom du projet provient de ce même répertoire de base. Modifier le premier fichier peut donc renommer le projet. Un projet renommé utilise de nouveaux noms de conteneurs et de volumes, tandis que l’ancien volume reste présent sur le disque sous son ancien nom. Définissez plutôt explicitement un name: de niveau supérieur dans le fichier de base.

name: myapp

Quels champs sont fusionnés et lesquels sont remplacés

Compose fusionne les champs selon le type de leur valeur, et non selon le nom du champ.

  • Les champs à valeur unique sont remplacés. image, command, entrypoint et mem_limit prennent directement la valeur la plus récente. Vous ne pouvez pas ajouter un argument à un command, car la surcharge réécrit toute la ligne.
  • Les mappings sont fusionnés clé par clé. environment, labels, volumes et devices conservent toutes les clés des deux fichiers. Pour une clé présente dans les deux fichiers, la valeur du fichier le plus récent est utilisée. Pour environment et labels, la clé est le nom de la variable ou du label. Pour volumes et devices, la clé est le chemin du conteneur.
  • Les séquences sont ajoutées. dns, dns_search, expose, tmpfs et external_links sont concaténées. Une configuration de base contenant expose: ["3000"], fusionnée avec une surcharge contenant ["4000", "5000"], produit ["3000", "4000", "5000"].

Quatre séquences utilisent une clé d'identité. Les entrées qui ont la même clé sont donc fusionnées au lieu d'être ajoutées. volumes, secrets et configs utilisent target pour la correspondance. ports utilise la combinaison de ip, target, published et protocol.

Relisez deux fois la règle ports, car c'est le piège. Deux entrées de port représentent la même entrée uniquement lorsque ces quatre éléments correspondent. Si vous en modifiez un seul, Compose considère qu'il s'agit d'un second port indépendant et conserve les deux.

Pourquoi votre port reste publié après la surcharge

Un fichier de base qui publie un service sur toutes les interfaces :

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

Une surcharge qui le lie uniquement à localhost, car un reverse proxy sera placé devant :

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

Vérifiez le résultat avant de considérer que la modification a fonctionné.

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

Les deux entrées apparaissent dans la sortie. La partie ip diffère, 0.0.0.0 contre 127.0.0.1. Compose les considère donc comme deux ports différents lors de la fusion, et la liaison publique que vous avez essayé de supprimer est toujours présente dans le modèle. Cela est plus important avec Docker qu'ailleurs, car un port publié est écrit dans iptables avant vos règles de pare-feu. Le mécanisme est expliqué dans pourquoi les ports Docker publiés contournent ufw.

Il existe deux corrections. La correction explicite utilise la balise !override. Elle remplace l'attribut entier et ignore les règles de fusion :

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

!override nécessite Compose v2.24.4 ou une version ultérieure. La correction portable n'utilise aucune balise : gardez ports entièrement hors du fichier de base et déclarez-le uniquement dans les fichiers propres à chaque environnement. S'il n'y a rien à fusionner, rien ne peut être exposé par erreur. C'est le modèle utilisé dans l'exemple détaillé ci-dessous.

Supprimer une valeur définie par le fichier de base

!reset supprime un attribut et le rétablit à sa valeur par défaut ou à la valeur null. Il accepte une valeur, puis l’ignore. Indiquez donc une valeur valide et vide.

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

!reset nécessite Compose v2.24 ou une version ultérieure. Utilisez-le lorsque vous ne pouvez pas modifier le fichier de base, par exemple s’il s’agit d’un fragment fourni par un éditeur que vous incluez.

include, pour les stacks assemblées à partir de plusieurs éléments

include ajoute une autre application Compose à votre modèle. Il s’agit d’un élément de niveau supérieur, pas d’un flag.

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

Chaque chemin indiqué dans include est chargé comme son propre modèle d’application Compose, avec son propre répertoire de projet. Les chemins relatifs utilisés dans ce fichier sont donc résolus par rapport au répertoire de ce fichier. C’est la différence réelle avec -f et la raison pour laquelle include est l’outil adapté lorsque le fragment se trouve dans un autre dossier ou dans un autre dépôt.

La forme longue accepte des sous-options.

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

path accepte une liste. Ces fichiers sont fusionnés selon les règles normales avant que le résultat soit ajouté à votre modèle. project_directory définit le chemin de base utilisé pour résoudre les chemins relatifs du fichier inclus. env_file fournit au fichier inclus ses propres variables pour l’interpolation. Cela empêche un fragment partagé de lire implicitement le .env de votre projet. include nécessite Compose v2.20.0 ou une version ultérieure.

Les noms de ressources dupliqués entre votre fichier et un fichier inclus génèrent une erreur au lieu d’être fusionnés silencieusement. Ce comportement est intentionnel. Pour modifier un élément déclaré dans un fichier inclus, placez la modification dans compose.override.yaml. L’override est appliqué au modèle assemblé et peut donc modifier les ressources incluses sans entrer en conflit avec elles.

En résumé : include compose des applications distinctes, tandis que -f ajoute des couches de configuration à une seule application.

Séparer dev et prod sur un même VPS

Voici le schéma complet dans trois fichiers. Le fichier de base déclare ce qui est vrai partout et n’expose aucun port.

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 condition depends_on fait attendre l’application qu’une base de données réponde, plutôt qu’un simple container existant. Ce point est expliqué dans les healthchecks et les conditions depends_on. POSTGRES_PASSWORD est interpolé depuis le fichier .env du projet, qui ne doit jamais être placé dans git. Consultez les fichiers env et les secrets Compose pour connaître les variantes plus sûres.

Ensuite, compose.override.yaml, que Compose charge automatiquement. Il s’agit du fichier du développeur.

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"

Sur un laptop, un docker compose up sans autre option fusionne ces deux fichiers. command remplace la valeur par défaut de l’image, car cette valeur est unique. LOG_LEVEL remplace info, car environment effectue la fusion par clé. Le bind mount et les deux ports publiés sont de simples ajouts. Le port de la base de données est lié à localhost afin qu’un laptop connecté à un réseau partagé n’expose pas PostgreSQL aux autres utilisateurs.

Enfin, compose.prod.yaml. Son nom ne fait pas partie des noms recherchés par Compose. Il ne sera donc jamais chargé par erreur.

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

Sur le VPS, vous nommez les deux fichiers. Ce choix exclut exactement l’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 doit afficher les deux services comme étant en cours d’exécution, avec db indiquant (healthy). Comme vous avez utilisé -f, compose.override.yaml n’a pas été lu. La commande de développement, le bind mount du code source et le port public 3000 ne peuvent donc pas atteindre la production, même si le fichier se trouve dans le même répertoire. Le port 8000 est accessible uniquement sur localhost et peut être utilisé par un proxy. Consultez exécuter plusieurs applications derrière Traefik lorsque vous ajouterez le second service.

Définissez COMPOSE_FILE=compose.yaml:compose.prod.yaml dans le fichier .env du serveur. Vos autres commandes pourront alors redevenir de simples docker compose logs -f app.

Lisez le modèle fusionné avant le déploiement

docker compose config affiche le modèle entièrement fusionné et interpolé. Ce n’est pas un aperçu. Il s’agit exactement de l’entrée que Compose utilisera. Si la sortie ne correspond pas à vos attentes, c’est la sortie qui fait foi.

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 laisse ${VAR} non développé. Utilisez-le avant de coller la sortie ailleurs, car config affiche tous les secrets résolus en clair. --services liste uniquement les noms des services. C’est un moyen rapide de vérifier qu’un include a chargé ce que vous attendiez.

Modes d’échec et résultats observés

no configuration file provided: not found. Compose n’a trouvé aucun fichier à lire. Vous êtes en dehors du répertoire du projet, ou COMPOSE_FILE désigne un chemin qui n’existe pas. Compose recherche le fichier de base par défaut dans les répertoires parents, mais il ne recherche pas un fichier que vous avez vous-même nommé.

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. L’interpolation s’effectue à partir du fichier .env du projet et de l’environnement du shell. Le répertoire du projet est ici le répertoire du premier fichier -f. Un déploiement effectué depuis un répertoire différent de celui qui contient .env produit cet avertissement, puis une base de données qui refuse toutes les connexions.

Votre modification de l’override n’apparaît pas dans docker compose config. Vous avez soit indiqué -f, ce qui désactive le chargement automatique de l’override, soit Compose a trouvé compose.yaml dans un répertoire parent et votre fichier d’override ne se trouve pas à côté. L’exécution de docker compose config sans autre argument indique quel modèle Compose construit réellement.

Un bind mount est vide et Docker a créé un répertoire que vous n’aviez pas demandé. Le chemin relatif a été résolu par rapport au répertoire du premier fichier. Corrigez le chemin, indiquez --project-directory ou déplacez le fragment après include.

Les conteneurs réapparaissent avec de nouveaux noms et un volume semble vide. Le nom du projet a changé, car il dépend du répertoire du premier fichier. Ajoutez un name: de niveau supérieur au fichier de base pour empêcher le nom de changer. L’ancien volume existe toujours avec l’ancien préfixe, et docker volume ls l’affichera.

Un port que vous avez supprimé dans l’override reste ouvert. La fusion ports a ajouté l’élément au lieu de le remplacer. Vérifiez avec docker compose config, puis utilisez !override ou retirez ports du fichier de base.

FAQ

Compose charge-t-il automatiquement compose.override.yaml ?

Oui, lorsque vous exécutez docker compose sans l’option -f. Compose recherche compose.yaml ou docker-compose.yaml dans le répertoire de travail et ses répertoires parents. Si un fichier override se trouve à côté, il est chargé en second. Les noms reconnus sont compose.override.yaml, compose.override.yml, docker-compose.override.yml et docker-compose.override.yaml. Le fait de transmettre une option -f désactive ce comportement. docker compose -f compose.yaml up ne lit alors qu’un seul fichier.

Dans quel ordre plusieurs fichiers -f sont-ils fusionnés ?

De gauche à droite. Compose construit la configuration dans l’ordre où vous fournissez les fichiers. Chaque fichier remplace les éléments des fichiers précédents et y ajoute les siens. En cas de conflit, le dernier fichier indiqué sur la ligne de commande l’emporte. La même liste doit être utilisée pour chaque commande de ce projet. C’est le rôle de COMPOSE_FILE=compose.yaml:compose.prod.yaml.

Pourquoi mon port est-il toujours publié après l’avoir remplacé ?

Parce que les entrées ports sont identifiées par l’ensemble des éléments ip, target, published et protocol. Une redéfinition de 127.0.0.1:8080:80 sur une base 8080:80 diffère par la partie ip. Compose la traite donc comme un second port et conserve les deux. Exécutez docker compose config pour voir les deux entrées. Utilisez ports: !override avec Compose v2.24.4 ou une version ultérieure, ou ne définissez pas ports dans le fichier de base afin qu’il n’y ait aucun élément à fusionner.

Quelle est la différence entre include et -f ?

-f superpose plusieurs fichiers pour créer une seule application. Les chemins relatifs de tous les fichiers sont résolus par rapport au répertoire du premier fichier. include incorpore une application Compose distincte. Chaque chemin inclus conserve son propre répertoire de projet. Ses chemins relatifs sont donc résolus par rapport à cette application. Utilisez -f pour les couches d’environnement de votre propre stack, et include pour un fragment maintenu ailleurs. include nécessite Compose v2.20.0 ou une version ultérieure.

Comment supprimer une valeur définie par le fichier de base ?

Utilisez le tag !reset avec Compose v2.24 ou une version ultérieure. Écrivez ports: !reset [] ou MY_VAR: !reset null dans le fichier de remplacement. L’attribut reprend alors sa valeur par défaut ou devient null. La valeur fournie au tag est obligatoire, mais elle est ignorée. Pour remplacer un attribut plutôt que l’effacer, !override convient. Cette fonctionnalité nécessite v2.24.4 ou une version ultérieure.