SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor

Comment fusionner plusieurs fichiers Docker Compose

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

Ce que Compose fait avec plusieurs fichiers

Docker Compose peut construire un projet à partir de plusieurs fichiers. Il les lit dans l’ordre dans lequel il les reçoit et les fusionne en un modèle unique. Les valeurs du dernier fichier remplacent donc celles qui sont 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 indiquez manuellement. Le troisième mécanisme se trouve dans le fichier lui-même. Il s’agit de l’élément include et son fonctionnement diffère des deux autres.

La fusion ne consiste pas en un simple remplacement. 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. C’est cette différence qui provoque les surprises. La liste ports est celle qui pose problème à presque tout le monde.

Tout ce qui suit suppose que vous utilisez Compose v2 ou une version ultérieure, avec le plugin docker compose plutôt que l’ancien script docker-compose. Les versions actuelles sont numérotées v5 et le plugin fonctionne de la même manière. Exécutez docker compose version pour vérifier. Si vous n’avez pas encore écrit de fichier Compose, commencez par le guide des bases de Docker Compose, puis revenez ici.

Le fichier d’override chargé automatiquement par Compose

Lancez docker compose up sans l’option -f : Compose recherche dans le répertoire de travail, puis dans ses répertoires parents, un fichier nommé compose.yaml ou docker-compose.yaml. Si un fichier d’override 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, le résultat est identique à une saisie manuelle des deux fichiers.

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 passez un -f, le chargement automatique s’arrête. docker compose -f compose.yaml up lit exactement ce fichier et ignore le fichier d’override. C’est sur ce comportement que repose le modèle dev et prod présenté plus loin dans ce guide.

Sur un serveur, cela peut fonctionner dans les deux sens. Un fichier d’override laissé dans le répertoire de déploiement est chargé par chaque commande docker compose exécutée sans chemin depuis ce répertoire, y compris celle lancée par votre tâche cron. C’est ainsi qu’une stack de production finit par monter en bind un répertoire source que personne ne voulait livrer. Exécutez docker compose config après chaque déploiement et consultez le résultat. Lorsque le déploiement est automatique, cette vérification n’est utile que si quelqu’un vous informe d’un problème. C’est le rôle d’un canal de notification push comme un serveur ntfy auto-hébergé, auquel une tâche cron ou une unité systemd OnFailure peut envoyer une notification.

Ordre des fichiers 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. Exécutez up avec deux fichiers et logs avec un seul : vous ciblez alors un autre modèle fusionné. C’est un moyen rapide d’obtenir un service que Compose indique comme inexistant. Le risque est plus important avec une stack dont les mises à niveau s’exécutent sous forme de commandes ponctuelles, comme l’étape de migration de base de données d’un outil de support Chatwoot auto-hébergé, où un docker compose run lancé avec une mauvaise liste de fichiers cible discrètement un autre modèle que celui déjà utilisé par vos services. 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 également être défini dans le fichier .env du projet. Il fait alors partie du dépôt plutôt que 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 casse les montages bind. 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 : Compose cherchera tout de même ./data à côté du fichier de base. Docker crée alors un répertoire vide à cet emplacement incorrect et le conteneur démarre sans rien à l’intérieur. 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 est également déterminé à partir de ce répertoire de base. Modifier le premier fichier peut donc renommer le projet. Un projet renommé entraîne de nouveaux noms de conteneurs et de volumes. 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 valeurs selon leur type, 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 l’override 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. Le fichier le plus récent est prioritaire lorsqu’une clé existe dans les deux. 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 concaténées. dns, dns_search, expose, tmpfs et external_links sont concaténées. Une configuration de base contenant expose: ["3000"], fusionnée avec un override contenant ["4000", "5000"], produit ["3000", "4000", "5000"].

Quatre séquences utilisent une clé d’identification. Les entrées qui correspondent sur cette clé sont donc fusionnées au lieu d’être ajoutées. volumes, secrets et configs utilisent target pour établir la correspondance. ports utilise la combinaison de ip, target, published et protocol.

Relisez deux fois cette règle ports, car c’est le piège. Deux entrées de port correspondent à la même entrée uniquement lorsque ces quatre éléments sont identiques. 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, parce qu’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 reste présente dans le modèle. C’est particulièrement important avec Docker, car un port publié est ajouté à iptables avant vos règles de pare-feu. Le mécanisme est expliqué dans pourquoi les ports Docker publiés contournent ufw.

Deux solutions sont possibles. La solution explicite utilise l’attribut !override, qui remplace entièrement l’attribut 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 solution portable ne nécessite aucun attribut : retirez complètement ports 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 dans le fichier de base

!reset supprime un attribut et lui applique sa valeur par défaut ou la valeur null. Il prend une valeur et l’ignore. Indiquez donc une valeur valide et vide.

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

!reset fonctionne avec Compose v2.18.0 ou une version ultérieure, plusieurs versions avant l’arrivée de !override. Utilisez-le lorsque vous ne pouvez pas modifier le fichier de base, par exemple un fragment fourni par un éditeur que vous incluez. Une stack publiée en amont correspond exactement à ce cas : le fichier Compose de un espace de travail AFFiNE auto-hébergé déclare quatre conteneurs que vous n’avez pas écrits, et !reset vous permet d’effacer un attribut sur l’un d’eux sans forker le fichier ni devoir en suivre les évolutions.

include, pour assembler des stacks à partir de fragments

include intègre 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 de ce fichier sont donc résolus par rapport à son propre répertoire. C’est la véritable différence avec -f, et la raison pour laquelle include est l’outil adapté lorsque le fragment se trouve dans un autre dossier ou un autre dépôt. C’est la structure habituelle d’une stack fournie par un éditeur et que vous n’avez pas écrite : le fichier Compose multi-service de l’installation auto-hébergée d’Authentik SSO peut se trouver dans son propre répertoire, avec ses propres chemins relatifs, tandis que votre fichier reste consacré à vos propres services.

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, et ces fichiers sont fusionnés selon les règles habituelles 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 indique le fichier dont le fichier Compose inclus utilise les valeurs par défaut pour l’interpolation. Cette option prend déjà par défaut le .env situé dans le répertoire de projet du fichier inclus. L’environnement de votre projet local reste prioritaire, ce qui vous permet de remplacer l’une de ses variables sans modifier le fragment. include nécessite Compose v2.20.0 ou une version ultérieure. Les mêmes options conviennent à un add-on mono-conteneur pour une stack que vous exécutez déjà, par exemple Halcyon, qui transforme une bibliothèque Jellyfin en vidéoclub des années 90 : son fichier conserve son propre tag d’image et son propre env_file, si bien que sa mise à niveau ne nécessite jamais de modifier le fichier de votre stack multimédia.

La documentation de Docker indique qu’un nom de ressource déclaré à la fois dans votre fichier et dans un fichier inclus est rejeté comme conflit. Le loader de la version actuelle ne se comporte pas ainsi : dans Compose v5.6.1, vérifié dans le code source du loader fourni le 2026-10-27, les deux définitions sont fusionnées. Votre fichier est prioritaire en cas de valeur contradictoire, au lieu de provoquer l’arrêt avec une erreur. Ne comptez donc pas sur un nom dupliqué pour déclencher une erreur explicite. Pour modifier un élément déclaré par un fichier inclus, placez la modification dans compose.override.yaml : l’override est appliqué au modèle assemblé. La modification est ainsi explicite et ne dépend pas de la manière dont une version donnée traite le conflit. Cette méthode est particulièrement utile avec une stack dont le fichier amont est réécrit à chaque release, comme les serveurs photo multi-conteneurs comparés dans PhotoPrism et Immich : une liaison localhost ou un volume supplémentaire doit figurer dans votre override, et non dans le fichier qui sera remplacé lors de la prochaine mise à niveau.

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

Séparer le développement et la production sur un même VPS

Voici le modèle complet dans trois fichiers. Le fichier de base déclare ce qui est commun partout et ne publie 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 une base de données qui répond, et non un simple conteneur 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 versionné 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 option fusionne ces deux fichiers. command remplace la valeur par défaut de l’image, car il s’agit d’une valeur unique. LOG_LEVEL remplace info, car environment fusionne les éléments selon leur 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 du réseau.

Enfin, compose.prod.yaml. Son nom ne correspond à aucun nom recherché par Compose, donc il ne sera jamais chargé par accident.

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

Sur le VPS, vous nommez les deux fichiers. C’est précisément ce qui exclut 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 actifs, avec db indiquant (healthy). Comme vous avez transmis -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 lié à localhost uniquement et peut être utilisé par un proxy. Consultez exécuter plusieurs applications derrière Traefik lorsque vous ajouterez le deuxième 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.

Une stack avec un seul service suit la même structure, car un outil auto-hébergé de suivi des entraînements openGym doit répondre en TLS derrière un proxy avant que vous n’enregistriez la première passkey. Un fichier de base qui ne contient aucun ports empêche un bind public accidentel de passer devant le proxy.

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

docker compose config affiche le modèle entièrement fusionné et interpolé. Il ne s’agit pas d’un aperçu. C’est l’entrée exacte 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 copier la sortie ailleurs, car config affiche tous les secrets résolus en clair. --services affiche uniquement les noms des services. C’est un moyen rapide de vérifier qu’un include a bien chargé ce que vous attendiez.

Modes d’échec et symptômes observés

no configuration file provided: not found. Compose n’a trouvé aucun fichier à lire. Vous n’êtes pas dans le 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 ailleurs un fichier que vous avez nommé vous-même.

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. L’interpolation s’appuie sur le fichier .env du projet et sur l’environnement du shell. Ici, le répertoire du projet est celui du premier fichier -f. Un déploiement depuis un répertoire différent de celui qui contient .env affiche cet avertissement, puis la base de données 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’avez 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 derrière include.

Les conteneurs reviennent 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 stabiliser les noms. L’ancien volume existe toujours sous l’ancien préfixe, et docker volume ls l’affichera.

Un port supprimé dans l’override reste ouvert. La fusion ports a ajouté l’entrée au lieu de la remplacer. Vérifiez avec docker compose config, puis utilisez !override ou déplacez ports hors 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 parents. Si un fichier d’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 fournir 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 et complète ceux qui le précèdent. En cas de conflit, le dernier fichier indiqué l’emporte. La même liste doit être utilisée pour chaque commande du 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 valeurs ip, target, published et protocol. Un override de 127.0.0.1:8080:80 sur une base 8080:80 diffère par la partie ip. Compose le considère donc comme un second port et conserve les deux. Exécutez docker compose config pour afficher les deux entrées. Utilisez ports: !override avec Compose v2.24.4 ou une version ultérieure, ou supprimez ports du fichier de base afin qu’il n’y ait rien à fusionner.

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

-f superpose plusieurs fichiers pour une même application. Les chemins relatifs de tous les fichiers sont résolus par rapport au répertoire du premier fichier. include inclut une application Compose distincte. Chaque fichier inclus conserve son propre répertoire de projet, et ses chemins relatifs sont donc résolus par rapport à ce répertoire. Utilisez -f pour les couches d’environnement de votre propre stack, et include pour un fragment géré 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.18.0 ou une version ultérieure. Écrivez ports: !reset [] ou MY_VAR: !reset null dans le fichier d’override. 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 au lieu de l’effacer, !override convient. Cette fonction nécessite v2.24.4 ou une version ultérieure.

Cet article vous a-t-il été utile ?