Docker Compose · DevOps · Container · Strukturierung
Docker Compose sauber strukturieren
Services, Profiles, Overrides und .env-Dateien

Wer alle Umgebungen in eine einzige docker-compose.yml presst, kämpft bald mit Konfigurations-Drift, hart kodierten Passwörtern und Deploys, die lokal anders verhalten als in der CI. Profiles, Overrides und sauber getrennte .env-Dateien lösen genau diese Probleme – ohne dass das Compose-Projekt unverständlich wächst.

12 Min. Lesezeit Profiles · Overrides · .env · extends · YAML anchors Docker Compose v2 · Docker Engine 25+

1. Das Problem: eine Datei für alle Umgebungen

Die häufigste Ursache für wartungsintensive Docker Compose-Projekte ist eine einzelne docker-compose.yml, die lokale Entwicklung, CI-Tests und Produktions-Deployments gleichzeitig bedienen soll. Das Ergebnis: Volumes für Hot-Reload stehen neben Health-Check-Einstellungen für Produktion, Debugger-Ports liegen neben Performance-Tuning-Flags, und irgendwo sind Passwörter hart kodiert, weil die Variable im einen Kontext immer verfügbar war. Docker Compose bietet alle notwendigen Mechanismen, um diese Konfigurationen sauber zu trennen – sie werden nur selten konsequent eingesetzt.

Das Kernproblem ist nicht die Komplexität von Docker Compose, sondern das Fehlen einer Struktur-Konvention im Team. Wer einmal Profiles, Overrides und separate .env-Dateien sinnvoll einführt, bekommt dafür eine Compose-Konfiguration, die in jeder Umgebung vorhersehbar verhält, keine Credentials im Repository enthält und neuen Entwicklern klar kommuniziert, welche Dienste für welchen Zweck gedacht sind. Der Aufwand ist einmalig – der Nutzen dauerhaft.

2. Basisstruktur: was in die Kern-Compose-Datei gehört

Die docker-compose.yml ist die Basisschicht: Sie definiert alle Services mit ihren Images, Netzwerken und Volumes in einer umgebungsneutralen Form. Keine Ports, die nur lokal sinnvoll sind. Kein stdin_open: true nur fürs Debugging. Keine Volume-Mounts, die nur auf dem Entwickler-Laptop existieren. Was in die Basis gehört: Service-Name, Image, Abhängigkeiten, Netzwerk-Membership und die absolut notwendigen Umgebungsvariablen, die in allen Umgebungen gelten. Alles andere gehört in spezifische Override-Dateien.

Die Basisstruktur eines Docker Compose-Projekts sollte immer eine docker-compose.yml (Basis), eine docker-compose.override.yml (automatisch für lokale Entwicklung geladen) und optionale docker-compose.ci.yml, docker-compose.staging.yml und docker-compose.prod.yml enthalten. Diese Trennung erlaubt es, in der CI explizit docker compose -f docker-compose.yml -f docker-compose.ci.yml up aufzurufen, ohne versehentlich Entwicklungs-Volumes zu mounten oder Debug-Ports zu öffnen.


# docker-compose.yml — Base configuration, environment-neutral
# All environments share this file; no dev-only or prod-only settings here

services:
  app:
    image: registry.mironsoft.de/shop/app:${APP_VERSION:-latest}
    networks: [backend, frontend]
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      APP_ENV: ${APP_ENV}
      DB_HOST: db
      REDIS_HOST: redis
    restart: unless-stopped

  db:
    image: mariadb:11.4
    networks: [backend]
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    networks: [backend]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

networks:
  backend:
  frontend:

volumes:
  db_data:

Ein häufiger Fehler bei der Basis-Konfiguration ist das Setzen von restart: always statt restart: unless-stopped. Der Unterschied: always startet den Container auch nach einem manuellen docker compose stop beim nächsten Docker-Daemon-Start neu. unless-stopped respektiert manuelle Stops. In Entwicklungsumgebungen ist unless-stopped fast immer die richtige Wahl – in Produktion dagegen sollte man prüfen, ob der Init-System des Hosts (systemd) den Daemon-Start kontrolliert.

3. .env-Dateien richtig trennen

Docker Compose lädt automatisch eine .env-Datei im Projektverzeichnis und interpoliert deren Werte in der Compose-Konfiguration. Das ist praktisch, führt aber schnell zu Durcheinander, wenn alle Umgebungen dieselbe Datei nutzen. Die saubere Lösung ist eine .env.example-Datei im Repository (committed, ohne echte Credentials), die als Vorlage für umgebungsspezifische .env-Dateien dient. Diese konkreten Dateien stehen in .gitignore und werden nie committed.

Für Teams mit mehreren Umgebungen empfiehlt sich eine explizite Struktur: env/local.env, env/ci.env, env/staging.env – jede mit den für ihre Umgebung passenden Werten. Beim Aufruf wird die passende Datei mit dem --env-file-Flag übergeben: docker compose --env-file env/ci.env -f docker-compose.yml -f docker-compose.ci.yml up -d. Damit ist jeder Aufruf vollständig deterministisch und nachvollziehbar – kein implizites Laden aus dem Arbeitsverzeichnis, keine Überraschungen durch lokal gesetzte Shell-Variablen, die Compose-Variablen überschreiben.


# env/local.env — Local development values (never committed with real secrets)
# Copy from .env.example and fill in your local values

APP_ENV=development
APP_VERSION=local
APP_DEBUG=true

DB_NAME=shop_dev
DB_USER=shop_user
DB_PASSWORD=dev_password_only_local
DB_ROOT_PASSWORD=root_dev_only

REDIS_PASSWORD=

# Enable Xdebug for local PHP debugging
PHP_XDEBUG_MODE=develop,debug
PHP_XDEBUG_CLIENT_HOST=host-docker-internal

---

# env/ci.env — CI-specific values (stored in CI secrets, not in repo)
APP_ENV=testing
APP_VERSION=${CI_COMMIT_SHORT_SHA}
APP_DEBUG=false

DB_NAME=shop_test
DB_USER=shop_test
DB_PASSWORD=${CI_DB_PASSWORD}    # injected by GitLab CI / GitHub Actions
DB_ROOT_PASSWORD=${CI_DB_ROOT}

PHP_XDEBUG_MODE=off

---

# .env.example — Template committed to the repo
APP_ENV=
APP_VERSION=
APP_DEBUG=
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_ROOT_PASSWORD=
REDIS_PASSWORD=
PHP_XDEBUG_MODE=off

4. Overrides: umgebungsspezifische Anpassungen

Docker Compose lädt docker-compose.override.yml automatisch zusätzlich zur Basis-Compose-Datei. Das macht diese Datei ideal für Entwicklungs-spezifische Erweiterungen: Volume-Mounts für Hot-Reload, offene Debug-Ports, vereinfachte Healthchecks und aktiviertes stdin_open. Da diese Datei nie explizit angegeben werden muss, können Entwickler einfach docker compose up aufrufen und erhalten sofort die richtige Entwicklungsumgebung. In CI und Produktion wird sie durch explizite -f-Flags gezielt nicht geladen.

Override-Dateien überschreiben einzelne Felder der Basis-Konfiguration, ersetzen sie aber nicht vollständig. Listen wie volumes, ports und environment werden zusammengeführt. Das bedeutet: eine Override-Datei kann neue Ports öffnen, ohne die in der Basis definierten zu verlieren. Skalare wie image und restart werden dagegen vollständig überschrieben. Dieses Merge-Verhalten ist bei der Gestaltung von Overrides zu berücksichtigen – besonders bei command und entrypoint, die als Gesamtheit ersetzt werden.


# docker-compose.override.yml — Loaded automatically for local development

services:
  app:
    # Mount source code for hot-reload — local only
    volumes:
      - ./src:/var/www/html:cached
      - ./var/cache:/var/www/html/var/cache
    # Expose Xdebug port to host
    ports:
      - "9003:9003"
    environment:
      PHP_XDEBUG_MODE: develop,debug
      PHP_XDEBUG_START_WITH_REQUEST: "yes"
    # Faster restart, less strict healthcheck for dev
    restart: "no"

  db:
    # Expose MariaDB to host for local DB tools (e.g. TablePlus, DBeaver)
    ports:
      - "3306:3306"
    # Shorter healthcheck interval for dev convenience
    healthcheck:
      interval: 5s
      retries: 10

  # Mail catcher — only needed locally
  mailhog:
    image: mailhog/mailhog:latest
    networks: [backend, frontend]
    ports:
      - "1025:1025"   # SMTP
      - "8025:8025"   # Web UI
    profiles: []      # no profile — always starts in dev override

  # Adminer — DB admin UI for local development
  adminer:
    image: adminer:4
    networks: [backend, frontend]
    ports:
      - "8080:8080"

5. Profiles: optionale Services gezielt aktivieren

Profiles sind das Feature in Docker Compose, das am häufigsten übersehen wird. Ein Profile ist ein benanntes Label, das einem oder mehreren Services zugeordnet wird. Services mit einem Profil starten nicht, wenn einfach docker compose up ausgeführt wird – sie müssen explizit mit --profile profilename aktiviert werden. Das erlaubt, selten benötigte Dienste wie Mailcatcher, Datenbankadmin-UIs, Monitoring-Stacks oder Import-Tools in derselben Compose-Datei zu definieren, ohne dass sie bei jedem Start hochfahren.

Ein typisches Beispiel für die sinnvolle Nutzung von Profiles in einem Docker Compose-Projekt: das Profil tools enthält Adminer und einen Redis-Commander, das Profil monitoring enthält Prometheus und Grafana, das Profil import enthält einen One-Shot-Container, der Testdaten importiert. Wer nur die Kernanwendung starten will, ruft docker compose up auf. Wer Datenbanktools braucht, nutzt docker compose --profile tools up. Mehrere Profile können kombiniert werden: --profile tools --profile monitoring. Das spart erheblich Ressourcen auf Entwickler-Laptops und in CI-Pipelines.

6. YAML-Anchors und Extends: Duplikate vermeiden

In größeren Docker Compose-Projekten wiederholen sich dieselben Konfigurationsblöcke – Logging-Einstellungen, gemeinsame Umgebungsvariablen, Restart-Policies und Healthcheck-Templates. YAML-Anchors (&anchorname) und Aliase (*anchorname) ermöglichen es, diese Blöcke einmal zu definieren und mehrfach zu referenzieren. Eine Änderung am Anchor ändert alle Aliase gleichzeitig. Das Feature ist in der YAML-Spezifikation definiert und wird von Docker Compose v2 vollständig unterstützt.

Das extends-Schlüsselwort in Docker Compose ermöglicht eine andere Form der Wiederverwendung: ein Service kann die Konfiguration eines anderen Services in derselben oder einer anderen Datei erben und einzelne Felder überschreiben. Das ist besonders nützlich, wenn mehrere Services auf demselben Image basieren, aber unterschiedliche Commands oder Umgebungsvariablen haben – etwa ein Web-Service und ein Worker-Service, die beide dieselbe Anwendung ausführen, aber verschiedene Einstiegspunkte nutzen.


# docker-compose.yml — YAML anchors eliminate configuration duplication

# Reusable logging configuration block
x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

# Reusable restart and resource limits
x-defaults: &service-defaults
  restart: unless-stopped
  logging: *default-logging

# Common environment for all PHP services
x-php-env: &php-env
  APP_ENV: ${APP_ENV}
  DB_HOST: db
  REDIS_HOST: redis
  REDIS_PORT: 6379

services:
  app:
    <<: *service-defaults        # merge defaults
    image: registry.mironsoft.de/shop/app:${APP_VERSION}
    environment:
      <<: *php-env               # merge PHP env block
      SERVER_ROLE: web

  worker:
    <<: *service-defaults
    image: registry.mironsoft.de/shop/app:${APP_VERSION}
    environment:
      <<: *php-env
      SERVER_ROLE: worker
    command: ["php", "bin/magento", "queue:consumers:start", "async.operations.all"]

  scheduler:
    <<: *service-defaults
    image: registry.mironsoft.de/shop/app:${APP_VERSION}
    environment:
      <<: *php-env
      SERVER_ROLE: scheduler
    command: ["php", "bin/magento", "cron:run"]

7. Secrets und sensible Werte sicher übergeben

Der häufigste Sicherheitsfehler in Docker Compose-Projekten ist das Hartcodieren von Credentials in der Compose-Datei selbst oder in commited .env-Dateien. Die sicherere Alternative ist Docker Secrets für Swarm-Deployments oder die Kombination aus externen Secret-Stores (HashiCorp Vault, AWS Secrets Manager) und Umgebungsvariablen, die zur Laufzeit injiziert werden. Für lokale Entwicklung ohne Swarm ist das .env-Muster akzeptabel, solange die Datei in .gitignore steht und niemals echte Produktions-Credentials enthält.

Bei Docker Compose v2 gibt es auch die secrets-Top-Level-Konfiguration, die Dateien aus dem Host-Filesystem als Read-only-Mounts in Container einbinden kann. Das ist praktischer als Swarm-Secrets, weil es keine Swarm-Initialisierung erfordert. Ein Secret-File auf dem Host (z.B. ./secrets/db_password.txt, in .gitignore) wird als /run/secrets/db_password in den Container gemountet. Die Anwendung liest den Wert aus der Datei statt aus einer Umgebungsvariable – das verhindert, dass Secrets in docker inspect oder Prozess-Logs auftauchen.

8. Strukturvarianten im Vergleich

Es gibt verschiedene Ansätze, ein Docker Compose-Projekt zu strukturieren. Die Wahl des richtigen Ansatzes hängt von der Teamgröße, der Anzahl der Umgebungen und den Deployment-Anforderungen ab.

Ansatz Vorteil Nachteil Geeignet für
Eine Datei Einfach zu verstehen Umgebungs-Drift, Credentials-Risiko Nur lokale Hobbyprojekte
Basis + Override Automatisches Merging für Dev Merge-Logik muss bekannt sein Teams mit 2–3 Umgebungen
Basis + Profiles Optionale Services klar gekennzeichnet Keine Umgebungs-Trennung Projekte mit optionalen Tools
Basis + Override + Profiles Vollständige Trennung, maximale Flexibilität Mehr Dateien, Einstiegshürde Professionelle Teams, mehrere Envs
YAML Anchors DRY, kein Duplikat-Config Schlechtere IDE-Unterstützung Projekte mit vielen ähnlichen Services

Die Empfehlung für professionelle Docker Compose-Projekte lautet: Basis-Datei plus override.yml für lokale Entwicklung, separate ci.yml für Tests und Profiles für optionale Services. Diese Kombination ist für alle Teamgrößen skalierbar und macht den Unterschied zwischen Umgebungen sofort lesbar – ohne dass jemand die Merge-Logik im Kopf durchrechnen muss.

9. Typische Workflow-Muster für Teams

Ein strukturiertes Docker Compose-Projekt erfordert auch klare Workflow-Konventionen. Das wichtigste Muster: Entwickler führen niemals docker compose up ohne explizite Flags in der CI aus – immer mit --env-file und expliziten -f-Flags. Das verhindert, dass die .override.yml in CI-Builds aktiv ist und Mounts oder Debug-Ports öffnet, die dort nicht gewünscht sind. Ein Makefile oder Shell-Skript, das die korrekten Flags für jede Umgebung kapselt, reduziert Fehler erheblich.

Ein zweites wichtiges Muster ist die explizite Versionierung von Docker Compose-Images. Statt image: mariadb:latest immer image: mariadb:11.4 verwenden. latest bedeutet in verschiedenen Umgebungen und zu verschiedenen Zeitpunkten unterschiedliche Images – ein subtiler und schwer debugbarer Drift. Dasselbe gilt für eigene Images: ${APP_VERSION} sollte immer einen konkreten Tag oder Commit-SHA enthalten, nie latest in nicht-lokalen Umgebungen. Das docker compose config-Kommando rendert die finale, zusammengeführte Konfiguration – ein nützliches Debugging-Tool, bevor man Services startet.

10. Zusammenfassung

Sauber strukturierte Docker Compose-Projekte nutzen die Trennungsmechanismen, die das Tool bietet: eine umgebungsneutrale Basis-Datei, automatisch geladene override.yml für lokale Entwicklung, explizite Override-Dateien für CI und Staging, Profiles für optionale Services und separate .env-Dateien für jeden Kontext. YAML-Anchors vermeiden Konfigurationsduplizierung bei ähnlichen Services. Secrets gehören niemals in committed Dateien – weder in die Compose-Datei selbst noch in .env-Dateien.

Der praktische Einstieg: ein bestehendes Docker Compose-Projekt in drei Schritten refaktorieren. Erstens, alle umgebungsspezifischen Werte in Variablen extrahieren und in separate .env-Dateien auslagern. Zweitens, Entwicklungs-spezifische Konfiguration (Ports, Volumes, Debug-Einstellungen) in docker-compose.override.yml verschieben. Drittens, selten genutzte Services mit Profiles kennzeichnen. Das Ergebnis ist ein Compose-Projekt, das in jeder Umgebung vorhersehbar verhält und neue Teammitglieder nicht überfordert.

Mironsoft

Docker Compose Strukturierung, DevOps-Beratung und Container-Infrastruktur

Docker Compose Projekt unübersichtlich geworden?

Wir analysieren bestehende Compose-Projekte, trennen Umgebungskonfigurationen sauber auf und führen Profiles, Overrides und sichere .env-Strukturen ein – damit euer Stack in jeder Umgebung vorhersehbar läuft.

Compose-Review

Analyse bestehender Compose-Dateien auf Umgebungs-Drift, Credential-Risiken und Merge-Fehler

Strukturierung

Basis, Overrides, Profiles und .env-Dateien einführen – mit Makefile-Wrapper für konsistente Team-Workflows

CI-Integration

GitLab CI / GitHub Actions mit korrekten Compose-Flags und Secret-Injection konfigurieren

Docker Compose sauber strukturieren — Das Wichtigste auf einen Blick

Basis + Overrides

Eine umgebungsneutrale Basis-Compose-Datei, override.yml für Dev, explizite ci.yml und prod.yml für andere Kontexte. Nie alles in eine Datei.

Profiles für optionale Services

Adminer, Mailcatcher, Monitoring – mit Profilen kennzeichnen. Starten nur, wenn explizit mit --profile name aktiviert.

.env-Dateien

.env.example committed, echte Werte in .gitignore. Für CI: --env-file env/ci.env explizit übergeben, kein implizites Laden.

YAML Anchors

Logging, Restart-Policy und gemeinsame Umgebungsvariablen einmal als Anchor definieren, in allen Services als Alias referenzieren.

11. FAQ: Docker Compose sauber strukturieren

1Wann wird override.yml automatisch geladen?
Automatisch, wenn sie im selben Verzeichnis liegt und kein explizites -f Flag gesetzt ist. In CI immer explizite -f Flags nutzen.
2Service nur in einem Profil starten?
profiles: [tools] im Service. Aktivieren mit docker compose --profile tools up.
3Credentials nicht ins Repo committen?
.env.example ohne Werte committen. Echte Dateien in .gitignore. CI-Secrets als Umgebungsvariablen injizieren.
4YAML Anchors in Docker Compose?
Vollständig unterstützt in v2. Mit &name definieren, *name referenzieren, <<: *name mergen. Änderungen am Anchor wirken auf alle Referenzen.
5Finale Compose-Konfiguration debuggen?
docker compose config zeigt die vollständig interpolierte Konfiguration nach allen Merges.
6Warum nicht latest als Image-Tag?
Führt zu Umgebungs-Drift. Konkrete Tags oder Commit-SHAs stellen sicher, dass alle Umgebungen dasselbe Image nutzen.
7Secrets sicher übergeben?
secrets-Konfiguration: Dateien als Read-only in /run/secrets/ mounten. Anwendung liest aus Datei statt Umgebungsvariable.
8CI ohne override.yml starten?
Explizite -f Flags: docker compose -f docker-compose.yml -f docker-compose.ci.yml up. Override.yml wird nur geladen, wenn sie in der Liste steht.
9always vs. unless-stopped?
unless-stopped respektiert manuelle docker compose stop. always startet auch nach manuellem Stop beim nächsten Daemon-Start neu.
10extends zwischen verschiedenen Compose-Dateien?
Ja: extends: file: base.yml / service: base-app. Zentralisiert gemeinsame Konfiguration in einer Base-Datei für mehrere Projekte.