Docker Compose: Mehrere Dateien richtig zusammenführen
Erfahren Sie, wie compose.override.yaml allein geladen wird, welche Reihenfolge beim Mergen gilt, warum ports einen Port offen hält und wie include Dev und Prod trennt.
Was Compose mit mehreren Dateien macht
Docker Compose kann ein Projekt aus mehreren Dateien erstellen. Die Dateien werden in der Reihenfolge eingelesen, in der sie übergeben werden, und zu einem einzelnen Modell zusammengeführt. Daher überschreibt eine spätere Datei jeden widersprüchlichen Wert. Über die Befehlszeile gibt es dafür zwei Mechanismen: eine Override-Datei, die Compose automatisch lädt, und das -f-Flag, das Sie manuell übergeben. Ein dritter Mechanismus befindet sich direkt in der Datei: das Element include. Es funktioniert anders als die beiden anderen.
Beim Zusammenführen werden Werte nicht einfach überschrieben. Mappings werden Schlüssel für Schlüssel zusammengeführt, Sequenzen angehängt, und einige wenige Felder werden vollständig ersetzt. Daraus entstehen die meisten Überraschungen. Die Liste ports sorgt dabei besonders häufig für Probleme.
Alle folgenden Beispiele setzen Compose v2 voraus, also das docker compose-Plugin und nicht das alte docker-compose-Skript. Führen Sie docker compose version aus, um dies zu prüfen. Wenn Sie noch keine Compose-Datei erstellt haben, beginnen Sie mit dem Grundlagenleitfaden zu Docker Compose und kehren Sie anschließend hierher zurück.
Die Override-Datei, die Compose ohne Angabe lädt
Führen Sie docker compose up ohne das Flag -f aus, durchsucht Compose das Arbeitsverzeichnis und anschließend die übergeordneten Verzeichnisse nach compose.yaml oder docker-compose.yaml. Liegt eine Override-Datei neben der Basisdatei, lädt Compose sie automatisch als zweite Datei.
ls compose.yaml compose.override.yaml
docker compose up -dSind beide Dateien vorhanden, entspricht das der manuellen Angabe beider Dateien.
docker compose -f compose.yaml -f compose.override.yaml up -dDie von Compose erkannten Namen sind compose.override.yaml, compose.override.yml sowie die älteren Namen docker-compose.override.yml und docker-compose.override.yaml. Jede andere Datei, zum Beispiel compose.dev.yaml, wird nur geladen, wenn Sie sie mit -f angeben.
Sobald Sie ein -f übergeben, wird das automatische Laden beendet. docker compose -f compose.yaml up liest genau diese eine Datei und ignoriert die Override-Datei. Auf dieser Eigenschaft basiert das später in diesem Leitfaden beschriebene Muster für Dev und Prod.
Das gilt auf einem Server in beide Richtungen. Eine im Deployment-Verzeichnis verbliebene Override-Datei wird von jedem dort ausgeführten einfachen docker compose-Befehl geladen, einschließlich des Befehls, den Ihr Cronjob ausführt. So bind-mountet ein Produktions-Stack am Ende ein Quellverzeichnis, das niemand bereitstellen wollte. Führen Sie nach jedem Deployment docker compose config aus und lesen Sie die Ausgabe. Bei einem unbeaufsichtigten Deployment hilft die Prüfung nur, wenn Sie über einen Fehler informiert werden. Dafür eignet sich ein Push-Kanal wie ein selbst gehosteter ntfy-Server, an den ein Cronjob oder eine systemd-OnFailure-Unit eine Nachricht senden kann.
Reihenfolge mit -f und Auflösung relativer Pfade
Compose erstellt die Konfiguration in der Reihenfolge, in der Sie die Dateien angeben. Nachfolgende Dateien überschreiben ihre Vorgänger und ergänzen sie. Von links nach rechts gilt: Die letzte Angabe gewinnt.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dJeder Befehl in diesem Projekt benötigt dieselbe Dateiliste. Wenn Sie up mit zwei Dateien und logs mit einer Datei ausführen, verwenden Sie ein anderes zusammengeführtes Modell. Dadurch kann schnell ein Dienst entstehen, von dem Compose behauptet, dass er nicht vorhanden ist. Das ist besonders kritisch bei einem Stack, dessen Aktualisierungen als einmalige Befehle ausgeführt werden, beispielsweise beim Datenbankmigrationsschritt in einem selbst gehosteten Chatwoot-Supportdesk. Ein docker compose run, das mit der falschen Dateiliste ausgeführt wird, zielt unbemerkt auf ein anderes Modell als dasjenige, das Ihre Dienste bereits verwenden. Legen Sie die Liste stattdessen einmal mit der Umgebungsvariablen COMPOSE_FILE fest.
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dDas Trennzeichen ist unter Linux :. COMPOSE_PATH_SEPARATOR ändert es. COMPOSE_FILE kann auch in der .env-Datei des Projekts stehen. Dadurch gehört die Einstellung zum Checkout und nicht zur Shell-Historie. Eine explizite Angabe in der Befehlszeile hat Vorrang vor der Umgebungsvariablen.
Nun zur Regel, die Bind-Mounts beeinträchtigt. Wenn Sie mehrere Dateien mit -f verwenden, werden alle relativen Pfade in allen diesen Dateien relativ zum Verzeichnis der ersten Datei aufgelöst. Sie werden nicht relativ zu der Datei aufgelöst, in der sie stehen. Schreiben Sie ./data:/var/lib/postgresql/data in deploy/prod/compose.prod.yaml, sucht Compose weiterhin nach ./data neben der Basisdatei. Docker erstellt dann an diesem falschen Pfad ein leeres Verzeichnis. Der Container startet darin ohne Inhalt. Das sieht wie ein Datenverlust aus, ist aber keiner. Übergeben Sie --project-directory, um den Basispfad selbst festzulegen. Alternativ können Sie include verwenden. Damit wird jede Datei relativ zu ihrem eigenen Verzeichnis aufgelöst.
Der Projektname stammt aus demselben Basisverzeichnis. Wenn Sie die Reihenfolge der Dateien ändern, kann sich daher der Projektname ändern. Bei einem umbenannten Projekt ändern sich die Container- und Volumenamen. Das alte Volume liegt weiterhin unter dem alten Namen auf dem Datenträger. Legen Sie den Namen stattdessen mit einem Top-Level-name: in der Basisdatei fest.
name: myappWelche Felder zusammengeführt und welche ersetzt werden
Compose führt Felder anhand des Werttyps zusammen, nicht anhand des Feldnamens.
- Felder mit einem einzelnen Wert werden ersetzt.
image,command,entrypointundmem_limitübernehmen direkt den späteren Wert. Sie können kein Argument an eincommandanhängen, weil die Überschreibung die gesamte Zeile ersetzt. - Mappings werden Schlüssel für Schlüssel zusammengeführt.
environment,labels,volumesunddevicesbehalten alle Schlüssel aus beiden Dateien. Bei einem Schlüssel, der in beiden Dateien vorhanden ist, gilt der Wert aus der späteren Datei. Beienvironmentundlabelsist der Variablen- oder Labelname der Schlüssel. Beivolumesunddevicesist der Containerpfad der Schlüssel. - Sequenzen werden angehängt.
dns,dns_search,expose,tmpfsundexternal_linkswerden verkettet. Eine Basisdatei mitexpose: ["3000"]ergibt zusammen mit einer Überschreibungsdatei mit["4000", "5000"]den Wert["3000", "4000", "5000"].
Vier Sequenzen verwenden einen Identitätsschlüssel. Einträge mit demselben Schlüssel werden daher zusammengeführt, statt angehängt zu werden. volumes, secrets und configs werden anhand von target abgeglichen. ports wird anhand der Kombination aus ip, target, published und protocol abgeglichen.
Lesen Sie diese Regel zu ports zweimal, weil sie die häufigste Fehlerquelle ist. Zwei Port-Einträge sind nur dann derselbe Eintrag, wenn alle vier Bestandteile übereinstimmen. Ändern Sie einen davon, erkennt Compose einen zweiten, unabhängigen Port und behält beide.
Warum Ihr Port nach dem Override weiterhin veröffentlicht ist
Eine Basisdatei, die einen Dienst auf allen Schnittstellen veröffentlicht:
services:
web:
image: nginx:1.27
ports:
- "8080:80"Ein Override, das den Dienst nur an localhost bindet, weil davor ein Reverse Proxy eingesetzt wird:
services:
web:
ports:
- "127.0.0.1:8080:80"Prüfen Sie das Ergebnis, bevor Sie davon ausgehen, dass die Änderung wirksam ist.
docker compose -f compose.yaml -f compose.prod.yaml configBeide Einträge sind in der Ausgabe enthalten. Der Teil ip unterscheidet sich: 0.0.0.0 gegenüber 127.0.0.1. Für das Zusammenführen handelt es sich daher um zwei verschiedene Ports, und die öffentliche Bindung, die Sie entfernen wollten, ist weiterhin im Modell enthalten. Das ist bei Docker besonders wichtig, weil ein veröffentlichter Port vor Ihren Firewall-Regeln in iptables eingetragen wird. Der Mechanismus wird in warum veröffentlichte Docker-Ports ufw umgehen beschrieben.
Es gibt zwei Korrekturen. Die explizite Variante ist das Tag !override. Es ersetzt das gesamte Attribut und überspringt die Regeln zum Zusammenführen:
services:
web:
ports: !override
- "127.0.0.1:8080:80"!override erfordert Compose v2.24.4 oder neuer. Die portable Variante benötigt kein Tag: Lassen Sie ports vollständig aus der Basisdatei heraus und deklarieren Sie es nur in den umgebungsspezifischen Dateien. Was nicht zusammengeführt wird, kann nicht versehentlich veröffentlicht werden. Dieses Muster wird im folgenden vollständigen Beispiel verwendet.
Ein Wert in der Basisdatei löschen
!reset entfernt ein Attribut und setzt es auf den Standardwert oder auf null zurück. Der Befehl nimmt einen Wert entgegen und ignoriert ihn. Geben Sie daher einen gültigen, leeren Wert an.
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset erfordert Compose v2.24 oder neuer. Verwenden Sie es, wenn Sie die Basisdatei nicht bearbeiten können, beispielsweise bei einem eingebundenen Fragment eines Anbieters. Ein veröffentlichtes Upstream-Stack ist genau so ein Fall: Die Compose-Datei hinter einem selbst gehosteten AFFiNE-Arbeitsbereich definiert vier Container, die Sie nicht selbst erstellt haben. Mit !reset können Sie ein Attribut eines dieser Container löschen, ohne die Datei zu forken und zusätzlich deren Pflege übernehmen zu müssen.
include für Stacks, die aus mehreren Teilen zusammengesetzt sind
include bindet eine weitere Compose-Anwendung in Ihr Modell ein. Es ist ein Element der obersten Ebene und kein Flag.
include:
- path: ../commons/compose.yamlJeder Pfad in include wird als eigenes Compose-Anwendungsmodell mit einem eigenen Projektverzeichnis geladen. Deshalb werden relative Pfade innerhalb dieser Datei relativ zu ihrem eigenen Verzeichnis aufgelöst. Das ist der wesentliche Unterschied zu -f und der Grund, warum include das richtige Werkzeug ist, wenn sich das Fragment in einem anderen Verzeichnis oder Repository befindet. Das ist der typische Aufbau eines Vendor-Stacks, den Sie nicht selbst geschrieben haben: Die Datei mit mehreren Compose-Diensten hinter einer selbst gehosteten Authentik-SSO-Installation kann in einem eigenen Verzeichnis liegen und ihre relativen Pfade unverändert beibehalten, während sich Ihre Datei weiterhin nur auf Ihre eigenen Dienste bezieht.
Die Langform unterstützt Unteroptionen.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath akzeptiert eine Liste. Diese Dateien werden nach den normalen Regeln zusammengeführt, bevor das Ergebnis in Ihr Modell übernommen wird. project_directory legt den Basispfad fest, der zum Auflösen relativer Pfade in der eingebundenen Datei verwendet wird. env_file gibt der eingebundenen Datei eigene Variablen für die Interpolation. Dadurch wird verhindert, dass ein gemeinsam verwendetes Fragment unbemerkt auf das .env Ihres Projekts zugreift. include erfordert Compose v2.20.0 oder neuer. Dieselben Optionen eignen sich für ein Add-on mit einem einzelnen Container in einem bereits betriebenen Stack, zum Beispiel Halcyon, das eine Jellyfin-Bibliothek wie eine Videothek aus den 90er-Jahren aussehen lässt: Seine Datei behält ihren eigenen Image-Tag und ihr eigenes env_file. Ein Upgrade erfordert dadurch keine Änderung an der Datei, in der Ihr Medien-Stack definiert ist.
Doppelte Ressourcennamen in Ihrer Datei und einer eingebundenen Datei werden als Fehler gemeldet und nicht stillschweigend zusammengeführt. Das ist beabsichtigt. Wenn Sie eine Deklaration aus einer eingebundenen Datei ändern möchten, nehmen Sie die Änderung in compose.override.yaml vor. Die Überschreibung wird auf das zusammengesetzte Modell angewendet und kann deshalb eingebundene Ressourcen ändern, ohne mit ihnen zu kollidieren. Das ist besonders bei einem Stack sinnvoll, dessen Upstream-Datei bei jeder Veröffentlichung neu geschrieben wird, etwa bei den in PhotoPrism im Vergleich zu Immich verglichenen Fotodiensten mit mehreren Containern. Eine Bindung an localhost oder ein zusätzliches Volume gehört dann in Ihre Überschreibung und nicht in die Datei, die beim nächsten Upgrade ersetzt wird.
Kurz gesagt: include setzt separate Anwendungen zusammen, -f legt Konfiguration auf eine einzelne Anwendung.
Trennung von Entwicklung und Produktion auf einer VPS
Hier ist das vollständige Muster in drei Dateien. Die Basisdatei legt fest, was überall gilt, und veröffentlicht überhaupt keine Ports.
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:Die Bedingung depends_on sorgt dafür, dass die Anwendung auf eine Datenbank wartet, die tatsächlich antwortet, und nicht nur auf einen vorhandenen Container. Das wird unter Healthchecks und depends_on-Bedingungen erklärt. POSTGRES_PASSWORD wird aus der .env-Datei des Projekts interpoliert. Diese Datei gehört niemals in Git. Unter Env-Dateien und Compose-Secrets finden Sie die sichereren Varianten.
Als Nächstes kommt compose.override.yaml, das Compose automatisch lädt. Dies ist die Datei für die Entwicklung.
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"Auf einem Laptop führt ein einfaches docker compose up diese beiden Dateien zusammen. command ersetzt den Standardwert des Images, weil dieser Wert nur einmal vorkommt. LOG_LEVEL ersetzt info, weil environment nach Schlüssel zusammengeführt wird. Das Bind-Mount und die beiden veröffentlichten Ports werden ausschließlich ergänzt. Der Datenbankport ist an localhost gebunden. Dadurch stellt ein Laptop in einem gemeinsam genutzten Netzwerk PostgreSQL nicht für andere Geräte bereit.
Zuletzt kommt compose.prod.yaml. Sein Name gehört nicht zu den Namen, nach denen Compose sucht. Daher wird die Datei nie versehentlich geladen.
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512MAuf der VPS geben Sie beide Dateien an. Genau dadurch wird die Override-Datei ausgeschlossen.
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 psps sollte beide Dienste als laufend anzeigen. db sollte dabei (healthy) ausgeben. Da Sie -f übergeben haben, wurde compose.override.yaml nicht gelesen. Der Entwicklungsbefehl, das Source-Bind-Mount und der öffentliche Port 3000 können daher nicht in die Produktion gelangen, obwohl die Datei im selben Verzeichnis liegt. Port 8000 ist nur an localhost gebunden und damit für einen Proxy vorbereitet. Wenn Sie den zweiten Dienst hinzufügen, lesen Sie dazu Mehrere Anwendungen hinter Traefik betreiben.
Setzen Sie COMPOSE_FILE=compose.yaml:compose.prod.yaml in der .env des Servers. Danach können Sie für die übrigen Befehle wieder einfach docker compose logs -f app verwenden.
Auch ein Stack mit nur einem Dienst folgt diesem Muster. Ein selbst gehosteter openGym-Workout-Tracker muss hinter einem Proxy über TLS erreichbar sein, bevor Sie den ersten Passkey registrieren. Eine Basisdatei ohne ports verhindert, dass eine versehentliche öffentliche Portbindung dem Proxy zuvorkommt.
Lesen Sie das zusammengeführte Modell, bevor Sie es bereitstellen
docker compose config gibt das vollständig zusammengeführte und interpolierte Modell aus. Es handelt sich nicht um eine Vorschau. Das Modell ist die exakte Eingabe, die Compose verarbeitet. Wenn die Ausgabe nicht Ihrer Erwartung entspricht, ist die Ausgabe maßgeblich.
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 lässt ${VAR} unverändert. Verwenden Sie den Befehl, bevor Sie die Ausgabe an anderer Stelle einfügen, denn config gibt jedes aufgelöste Secret im Klartext aus. --services listet nur die Dienstnamen auf. Damit können Sie schnell prüfen, ob ein include die erwarteten Inhalte eingebunden hat.
Fehlerfälle und ihre sichtbaren Auswirkungen
no configuration file provided: not found. Compose hat keine Datei zum Einlesen gefunden. Sie befinden sich außerhalb des Projektverzeichnisses, oder COMPOSE_FILE verweist auf einen nicht vorhandenen Pfad. Compose durchsucht übergeordnete Verzeichnisse nach der Standard-Basisdatei. Nach einer Datei, die Sie selbst angegeben haben, sucht Compose jedoch nicht an beliebigen Stellen.
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. Die Interpolation wird anhand der Projektdatei .env und der Shell-Umgebung aufgelöst. Das Projektverzeichnis ist hier das Verzeichnis der ersten -f-Datei. Wenn Sie die Bereitstellung aus einem anderen Verzeichnis als dem Verzeichnis mit .env starten, erhalten Sie diese Warnung. Anschließend verweigert die Datenbank jede Verbindung.
Ihre Änderung an der Override-Datei wird in docker compose config nicht angezeigt. Entweder haben Sie -f übergeben. Dadurch wird das automatische Laden von Override-Dateien deaktiviert. Oder Compose hat compose.yaml in einem übergeordneten Verzeichnis gefunden, und Ihre Override-Datei befindet sich nicht im selben Verzeichnis. Wenn Sie docker compose config ohne weitere Argumente ausführen, sehen Sie, welches Modell Compose tatsächlich erstellt.
Ein Bind-Mount ist leer, und Docker hat ein Verzeichnis erstellt, das Sie nicht angefordert haben. Der relative Pfad wurde relativ zum Verzeichnis der ersten Datei aufgelöst. Korrigieren Sie den Pfad, übergeben Sie --project-directory oder verschieben Sie das Fragment hinter include.
Die Container erhalten neue Namen, und ein Volume wirkt leer. Der Projektname hat sich geändert, weil der Projektname aus dem Verzeichnis der ersten Datei abgeleitet wird. Fügen Sie der Basisdatei auf oberster Ebene name: hinzu. Dadurch bleiben die Namen stabil. Das alte Volume befindet sich weiterhin unter dem alten Präfix. Mit docker volume ls wird es angezeigt.
Ein Port, den Sie im Override entfernt haben, ist weiterhin geöffnet. Beim Zusammenführen mit ports wurde der Eintrag angehängt, statt ihn zu ersetzen. Prüfen Sie dies mit docker compose config. Verwenden Sie anschließend entweder !override oder verschieben Sie ports aus der Basisdatei.
FAQ
Lädt Compose compose.override.yaml automatisch?
Ja, wenn Sie docker compose ohne das Flag -f ausführen. Compose durchsucht das Arbeitsverzeichnis und dessen übergeordnete Verzeichnisse nach compose.yaml oder docker-compose.yaml. Wenn sich daneben eine Override-Datei befindet, wird diese anschließend geladen. Die unterstützten Namen sind compose.override.yaml, compose.override.yml, docker-compose.override.yml und docker-compose.override.yaml. Die Übergabe eines beliebigen -f deaktiviert dieses Verhalten. docker compose -f compose.yaml up liest dann nur eine Datei ein.
In welcher Reihenfolge werden mehrere -f-Dateien zusammengeführt?
Von links nach rechts. Compose erstellt die Konfiguration in der Reihenfolge, in der Sie die Dateien angeben. Jede Datei überschreibt die vorherigen Dateien und ergänzt sie. Bei einem Konflikt gilt daher die letzte Datei in der Befehlszeile. Für jeden Befehl in diesem Projekt muss dieselbe Liste verwendet werden. Dafür ist COMPOSE_FILE=compose.yaml:compose.prod.yaml vorgesehen.
Warum ist mein Port weiterhin veröffentlicht, nachdem ich ihn überschrieben habe?
Weil ports-Einträge anhand der gesamten Kombination aus ip, target, published und protocol identifiziert werden. Ein Override von 127.0.0.1:8080:80 gegenüber einer Basis von 8080:80 unterscheidet sich beim Teil ip. Compose behandelt ihn daher als zweiten Port und behält beide Einträge bei. Führen Sie docker compose config aus, um die beiden Einträge anzuzeigen. Verwenden Sie ports: !override mit Compose v2.24.4 oder neuer. Alternativ können Sie ports aus der Basisdatei entfernen, sodass kein entsprechender Eintrag zusammengeführt werden muss.
Was ist der Unterschied zwischen include und -f?
-f legt mehrere Dateien über eine Anwendung. Relative Pfade in allen Dateien werden dabei anhand des Verzeichnisses der ersten Datei aufgelöst. include bindet eine separate Compose-Anwendung ein. Jede eingebundene Datei behält dabei ihr eigenes Projektverzeichnis, sodass ihre relativen Pfade anhand dieses Verzeichnisses aufgelöst werden. Verwenden Sie -f für Umgebungsdateien Ihres eigenen Stacks und include für ein Fragment, das an anderer Stelle gepflegt wird. include erfordert Compose v2.20.0 oder neuer.
Wie entferne ich einen Wert, den die Basisdatei setzt?
Verwenden Sie das Tag !reset mit Compose v2.24 oder neuer. Schreiben Sie ports: !reset [] oder MY_VAR: !reset null in die überschreibende Datei. Das Attribut wird dann auf seinen Standardwert oder auf null zurückgesetzt. Der Wert, den Sie dem Tag übergeben, ist erforderlich, wird aber ignoriert. Wenn Sie ein Attribut ersetzen statt löschen möchten, verwenden Sie !override. Dafür ist v2.24.4 oder neuer erforderlich.