SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-28

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 de réception et les fusionne dans un modèle unique. Les valeurs du dernier fichier remplacent celles des fichiers précédents en cas de 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 explicitement. 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 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 remplacés entièrement. 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, c’est-à-dire 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 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 le rétablit à sa valeur par défaut ou à null. Il prend une valeur et 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 lorsqu’il s’agit d’un fragment fourni par un éditeur que vous incluez. Une stack publiée en amont est exactement dans ce cas : le fichier Compose associé à 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 créer un fork du fichier ni devoir en suivre les modifications.

include, pour les stacks assemblées à partir de composants

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

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 au répertoire qui lui est propre. C’est la véritable différence avec -f, et la raison pour laquelle include convient lorsque le fragment se trouve dans un autre dossier ou un autre dépôt. C’est généralement le cas d’une stack fournie par un éditeur que vous n’avez pas écrite : le fichier Compose multi-service d’une installation auto-hébergée d’Authentik SSO peut rester dans son propre répertoire, avec ses chemins relatifs intacts, tandis que votre fichier reste consacré à vos propres services.

La syntaxe 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 habituelles avant que le résultat soit intégré à 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, ce qui l’empêche de lire discrètement le .env de votre projet. include nécessite Compose v2.20.0 ou une version ultérieure. Les mêmes options conviennent à un module complémentaire à conteneur unique ajouté à 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. Sa mise à niveau ne nécessite donc jamais de modifier le fichier utilisé par votre stack multimédia.

Les noms de ressources en double entre votre fichier et un fichier inclus sont signalés comme une erreur au lieu d’être fusionnés silencieusement. C’est volontaire. Pour modifier ce qu’un fichier inclus déclare, placez la modification dans compose.override.yaml : la surcharge est appliquée au modèle assemblé. Elle peut donc modifier les ressources incluses sans entrer en conflit avec elles. 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 ou Immich. Une liaison localhost ou un volume supplémentaire doit alors figurer dans votre surcharge, et non dans le fichier que la prochaine mise à niveau remplacera.

En résumé : include compose des applications distinctes, tandis que -f superpose une 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 répertoires parents. Si un fichier de remplacement 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. L’utilisation de -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 est donc prioritaire. Vous devez utiliser la même liste pour toutes les commandes du projet. C’est le rôle de COMPOSE_FILE=compose.yaml:compose.prod.yaml.

Pourquoi mon port est-il toujours publié après son remplacement ?

Parce que les entrées ports sont identifiées par l’ensemble des valeurs ip, target, published et protocol. Le remplacement de 127.0.0.1:8080:80 par rapport à une valeur de base 8080:80 diffère au niveau de ip. Compose le 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’aucune fusion ne soit nécessaire.

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 intègre une application Compose distincte. Chaque fichier inclus conserve son propre répertoire de projet, et ses chemins relatifs sont donc résolus par rapport à celui-ci. 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 dans 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 fonction nécessite v2.24.4 ou une version ultérieure.