Data9 min Lesezeit13.8.2026

Warum wir MultiDocker gebaut haben: drei Projekte, ein Laptop, kein Port-Streit

Shop, API und CMS liefen bei uns nie parallel, weil jedes Projekt dieselben Ports wollte. Aus dem täglichen Hoch- und Runterfahren von Compose-Stacks ist ein eigenes Werkzeug geworden. Die drei Entscheidungen dahinter und was sie im Alltag verändert haben.

Alexander Schikowsky
Alexander SchikowskyE-Commerce & KI-Experte

An einem Dienstagvormittag lagen drei Projekte parallel auf dem Tisch: ein Shop, dessen Checkout einen Fehler warf, eine API, die dazu die Preise liefert, und ein CMS, aus dem die Texte kommen. Alle drei laufen lokal in Docker, jedes mit eigener docker-compose.yml. Um den Fehler nachzustellen, mussten alle drei zusammen laufen. Nach zwanzig Minuten Portsortiererei war klar, dass der Checkout an diesem Vormittag das kleinere Problem sein würde.

Aus diesem Vormittag ist ein Werkzeug geworden, das wir seitdem täglich benutzen: MultiDocker. Es betreibt beliebig viele docker-compose-Projekte parallel auf einem Rechner und macht jedes davon unter einem eigenen HTTPS-Hostnamen erreichbar. Dieser Beitrag beschreibt, woran wir vorher hängengeblieben sind, welche Wege wir verworfen haben und welche drei Entscheidungen am Ende den Ausschlag gaben.

Drei Compose-Dateien wollen denselben Port

Jede Compose-Datei bringt ihre eigene Portzuordnung mit, und die orientiert sich an dem, was in Produktion gilt: 80 für den Webserver, 3306 für MySQL, 6379 für Redis. Solange nur ein Projekt läuft, passt das. Beim zweiten meldet Docker den Abbruch mit einer Zeile, die jeder kennt, der mehrere Stacks betreut:

Error response from daemon: driver failed programming external
connectivity on endpoint shop-web-1: Bind for 0.0.0.0:80 failed:
port is already allocated

Die naheliegende Antwort lautet, im zweiten Projekt andere Ports einzutragen. Genau da beginnt der Ärger. Die docker-compose.yml gehört dem Projekt und liegt im Repository. Wer dort 8081 statt 8080 einträgt, hat entweder eine lokale Änderung im Diff stehen oder muss sie vor jedem Commit herausnehmen. Wir haben beides ausprobiert, inklusive eines langlebigen lokalen Branches, der irgendwann bei jedem Rebase Konflikte produzierte.

Der zweite Effekt ist subtiler: Sobald jede Maschine ihre eigene Portverteilung hat, passt nichts mehr zusammen. Ein Kollege beschreibt einen Fehler unter localhost:8081, auf dem anderen Rechner liegt dort ein völlig anderes Projekt. Postman-Sammlungen, Browser-Lesezeichen und Notizen in Tickets verlieren ihre Gültigkeit, sobald jemand einen Stack neu aufsetzt.

Was localhost mit kaputten Logins zu tun hat

Portkonflikte kosten Zeit, sind aber sichtbar. Teurer waren die Fehler, die aus der gemeinsamen Adresse entstanden. Alles lag auf localhost, und der Browser behandelt localhost als einen einzigen Host. Cookies unterscheiden nicht nach Port. Der Shop auf 8080 und das CMS auf 8081 schrieben ihre Session-Cookies in denselben Namensraum und überschrieben sich gegenseitig. Wer sich im CMS anmeldete, flog im Shop heraus.

Dazu kam die Gegenrichtung: Für CORS und SameSite zählt der Port sehr wohl. Ein Request von localhost:3000 auf localhost:8080 ist ein Cross-Origin-Request. Ein Cookie mit SameSite=None verlangt zusätzlich das Secure-Flag, das ohne HTTPS nicht gesetzt werden kann. Wir hatten also lokal Fehler, die es in Produktion nicht gab, und daneben Fälle, die lokal funktionierten und im Staging brachen. Beides kostet Debugging-Zeit an Stellen, die mit dem eigentlichen Feature nichts zu tun haben.

Ein halber Tag, bevor die erste Zeile Code entsteht

Am deutlichsten wurde das Problem beim Onboarding. Die Anleitung für einen neuen Entwickler umfasste vierzehn Schritte: Repository klonen, .env aus dem Passwortmanager zusammenbauen, Ports gegen die Liste im Wiki abgleichen, Einträge in /etc/hosts ergänzen, Container starten, Datenbank importieren, Mailversand auf einen lokalen Catcher umbiegen. Wer alle drei Projekte brauchte, machte das drei Mal, mit jeweils anderen Portnummern.

Ein halber Tag war der Normalfall, ein ganzer keine Seltenheit. Und weil die Portliste im Wiki nur so gut ist wie ihre letzte Pflege, endete das regelmäßig in einer Frage im Teamchat, die einen zweiten Entwickler mit hineinzog.

Entscheidung 1: Override statt Änderung an der Compose-Datei

Die erste Festlegung war die wichtigste: Am Repository wird nichts angefasst. Docker Compose kann mehrere Dateien übereinanderlegen, und die Umgebungsvariable COMPOSE_FILE nimmt eine mit Doppelpunkt getrennte Liste entgegen. Die zweite Datei muss dafür nirgendwo im Projekt liegen. MultiDocker erzeugt sie pro Projekt im eigenen Konfigurationsverzeichnis und ruft Compose damit auf.

COMPOSE_FILE="$PROJECT/docker-compose.yml:$HOME/.config/multi-docker/overrides/shop.override.yml" \
  docker compose -p shop up -d

Der Inhalt des Overrides macht drei Dinge: Er entfernt die Host-Port-Mappings, hängt die Container zusätzlich an ein gemeinsames Docker-Netz und setzt die Routing-Labels für den Reverse Proxy. Das Entfernen war der Teil, der uns anfangs aufgehalten hat, denn Compose führt Listen normalerweise zusammen statt sie zu ersetzen. Seit Compose v2 gibt es dafür das Tag !reset:

services:
  web:
    ports: !reset []            # Host-Mappings verschwinden komplett
    networks: [default, edge]
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=multi-docker-edge"
      - "traefik.http.routers.shop.rule=Host(`shop.coding9.test`)"
      - "traefik.http.routers.shop.entrypoints=websecure"
      - "traefik.http.routers.shop.tls=true"
      - "traefik.http.services.shop.loadbalancer.server.port=80"
  db:
    ports: !reset []

networks:
  edge:
    name: multi-docker-edge
    external: true

Der praktische Nebeneffekt: Wer MultiDocker nicht installiert hat, startet dasselbe Projekt weiterhin mit einem schlichten docker compose up. Es gibt keinen Fork der Compose-Datei, keinen zusätzlichen Eintrag in der .gitignore und keine Datei, die im Review erklärt werden muss.

Entscheidung 2: ein Wildcard-Zertifikat und ein lokaler Resolver

Hostnamen statt Ports lösen die Cookie-Frage nur, wenn die Hostnamen auch auflösen und der Browser das Zertifikat akzeptiert. Beides wollten wir nicht pro Projekt einrichten müssen. Die Lösung besteht aus zwei Teilen, die beim ersten Setup einmal laufen.

Für die Namensauflösung läuft im Edge-Stack ein dnsmasq, der jede Anfrage unterhalb der Testdomain auf den lokalen Proxy zeigen lässt. macOS bekommt dafür eine Resolver-Datei unter /etc/resolver, die alle Anfragen für diese eine Domain an dnsmasq schickt. Alle übrigen Domains bleiben beim normalen DNS des Systems. Der Weg über /etc/hosts hätte für jedes neue Projekt eine weitere Zeile und einen sudo-Aufruf bedeutet.

Für das Zertifikat nutzt MultiDocker mkcert. Die lokale CA landet einmal in der System-Keychain, danach wird ein einziges Wildcard-Zertifikat für die gesamte Testdomain ausgestellt. Jedes neue Projekt ist dadurch ohne weiteres Zutun per HTTPS erreichbar. Secure-Cookies, SameSite=None und OAuth-Redirects verhalten sich lokal so, wie sie es später auf dem Server tun.

Entscheidung 3: ein Reverse Proxy für alle Projekte

Den Verkehr nimmt ein einzelner Traefik v3 entgegen, der als eigener Stack auf Port 80 und 443 hört. Er liest die Labels aus den laufenden Containern und leitet anhand des Hostnamens weiter. Kommt ein Projekt dazu, muss an der Proxy-Konfiguration nichts geändert werden, weil die Information über die Labels aus dem Override kommt.

In denselben Stack ist ein Mailcatcher gewandert. Vorher hatte jedes Projekt seinen eigenen, mit eigener Weboberfläche auf einem eigenen Port. Heute schicken alle Projekte ihre Mails an denselben Container im Edge-Netz, und lokale Mailcatcher in den Projekten werden über das Override abgeschaltet. Ein Postfach für alles, was lokal verschickt wird.

Von außen sieht der Alltag seitdem so aus:

mdocker setup                     # einmalig: Zertifikat, DNS, Edge-Stack

mdocker init ~/Projects/shop      # Projekt registrieren, Profil erkennen
mdocker init ~/Projects/api
mdocker init ~/Projects/cms

mdocker up shop api cms           # laufen parallel
mdocker ls                        # Status aller Projekte
mdocker doctor                    # Werkzeuge, Ports, DNS, Zertifikat pruefen

#   https://shop.coding9.test
#   https://api.coding9.test
#   https://cms.coding9.test
#   https://mail.coding9.test

Was sich seitdem im Alltag geändert hat

Vier Punkte fallen im Rückblick auf:

  • Der Kontextwechsel kostet keinen Neustart mehr. Wer zwischen Shop und API hin und her springt, wechselt den Browser-Tab. Vorher standen dazwischen ein down und ein up mit zwei bis drei Minuten Wartezeit.

  • Die Portliste im Wiki gibt es nicht mehr. Adressen ergeben sich aus dem Projektnamen und sind auf jedem Rechner im Team identisch. Ein Link in einem Ticket funktioniert bei allen.

  • Cookie- und CORS-Fehler treten dort auf, wo sie hingehören. Weil jedes Projekt einen eigenen Hostnamen mit gültigem Zertifikat hat, verhält sich der Browser lokal wie in Produktion.

  • Das Onboarding ist auf drei Befehle geschrumpft. Ein neuer Entwickler klont, ruft setup, init und up auf und hat am Vormittag eine laufende Umgebung. Die verbleibende Arbeit sind Zugangsdaten und Testdaten.

Zwei Dinge sind unterwegs dazugekommen, weil sie sich aus dem Aufbau ergaben. Erstens ein Datenbank-Werkzeug: Da die Container keine Host-Ports mehr belegen, brauchte es einen bequemen Weg zum Datenbank-Client. Der Befehl mdocker inspect listet je Service den Container, die internen Ports und einen fertigen Connection-String zum Kopieren; mdocker db legt Dumps und benannte Snapshots an und spielt sie zurück. Zweitens eine Oberfläche: Die macOS-App in SwiftUI setzt auf derselben CLI auf und zeigt Projektliste, Logs und Git-Stand, sodass auch Teammitglieder ohne Terminalroutine damit arbeiten.

Für wen sich der Aufbau lohnt

Der Nutzen wächst mit der Zahl der Projekte, die parallel gebraucht werden. Ab zwei Projekten rechnet sich das Setup schon in der ersten Woche, bei einer Agentur mit einem Dutzend betreuter Anwendungen fällt der Unterschied täglich auf. Voraussetzung sind macOS und Docker Desktop oder OrbStack; die weiteren Werkzeuge prüft mdocker doctor und meldet, was fehlt.

Wenig bringt der Aufbau Teams, die genau ein aktives Projekt betreuen: Dort entsteht kein Konflikt, den ein Proxy auflösen müsste. Wer lokal bereits mit Kubernetes, Tilt oder Devcontainern arbeitet, hat sein Routing an anderer Stelle geregelt und würde zwei Systeme parallel pflegen. Und wer im Team gemischt auf Linux und Windows unterwegs ist, muss DNS und Zertifikate dort von Hand einrichten, weil das automatische Setup auf macOS zugeschnitten ist.

Für uns war der Auslöser ein Vormittag mit drei Projekten und einem belegten Port 80. Was daraus geworden ist, steht im Detail auf der Produktseite zu MultiDocker. Wer selbst mehrere Stacks lokal betreut, kennt die Ausgangslage vermutlich gut genug, um zu wissen, ob sich der Umbau lohnt.

Ihre lokale Entwicklungsumgebung frisst mehr Zeit als das Projekt selbst? Wir schauen uns Ihr Compose-Setup an und sagen, welche Schritte sich lohnen - von der Portfrage über Zertifikate bis zum Onboarding neuer Entwickler.

Setup besprechen

Häufig gestellte Fragen

Warum reicht es nicht, in der docker-compose.yml einfach andere Ports einzutragen?

Weil die Datei dem Projekt gehört und im Repository liegt. Eine lokale Portverschiebung landet entweder im Commit oder muss vor jedem Commit wieder herausgenommen werden. Zusätzlich hat jede Maschine dann ihre eigene Portverteilung, sodass Bookmarks, Konfigurationen und Fehlerbeschreibungen im Team nicht mehr zusammenpassen.

Was macht eine Override-Datei anders als eine docker-compose.override.yml im Projekt?

Die Datei liegt außerhalb des Projektordners, im Konfigurationsverzeichnis des Werkzeugs. Aufgerufen wird sie über die Umgebungsvariable COMPOSE_FILE, die Original- und Override-Datei mit Doppelpunkt verknüpft. Im Repository ändert sich dadurch nichts, auch nicht in der .gitignore. Wer ohne das Werkzeug arbeitet, startet das Projekt weiter mit docker compose up.

Warum ein eigenes Zertifikat statt einfach http zu benutzen?

Secure-Cookies, SameSite=None, Service Worker und viele OAuth-Flows setzen HTTPS voraus. Ohne Zertifikat lassen sich diese Fälle lokal nicht nachstellen, und Fehler tauchen erst im Staging auf. Ein Wildcard-Zertifikat über mkcert für die gesamte Testdomain gilt für alle Projekte gleichzeitig und muss nur einmal angelegt werden.

Läuft MultiDocker auch unter Linux oder Windows?

Der Kern ist Bash plus Docker Compose und damit portabel. Das automatische Setup für Zertifikat und DNS-Resolver ist auf macOS zugeschnitten, weil es die Resolver-Dateien unter /etc/resolver anlegt und die mkcert-CA in die System-Keychain schreibt. Unter Linux funktioniert der Rest, wenn dnsmasq oder /etc/hosts von Hand gepflegt werden.

Für wen lohnt sich das nicht?

Für Teams mit genau einem aktiven Projekt bringt der Aufbau wenig, weil dort kein Portkonflikt entsteht. Ebenso für Setups, die lokal bereits über Kubernetes, Tilt oder Devcontainer laufen und ihr Routing dort geregelt haben. Der Aufwand rechnet sich ab etwa zwei Projekten, die regelmäßig gleichzeitig gebraucht werden.

Teilen Sie diesen Artikel: