Composer und Git im Zusammenspiel: Abhängigkeiten versionieren
git
HEAD
Git · Composer · Dependency Management · Magento 2
Composer und Git im Zusammenspiel: Abhängigkeiten versionieren
Warum composer.lock ins Repository gehört, vendor aber nicht

Wer vendor Verzeichnisse committet oder composer.lock ignoriert, riskiert inkonsistente Installationen zwischen Entwicklerrechner, CI Pipeline und Produktivserver. Dieser Artikel zeigt, warum composer.lock statt vendor versioniert werden sollte, wie private Magento Marketplace Pakete sicher über auth.json eingebunden werden, und wie sich composer.lock Merge Konflikte zwischen Branches sauber auflösen lassen, damit Deployments jederzeit reproduzierbar bleiben.

11 Min. Lesezeit composer.lock · vendor · auth.json Magento 2.4 · CI/CD · Deployment

1. Warum Composer und Git gemeinsam gedacht werden müssen

Composer verwaltet PHP-Abhängigkeiten, Git verwaltet den Verlauf des eigenen Quellcodes. Beide Werkzeuge arbeiten aus gutem Grund getrennt, doch in der Praxis entstehen die hartnäckigsten "es funktioniert nur bei mir"-Fehler genau an der Schnittstelle beider Systeme: wenn unklar ist, welche Composer-Datei tatsächlich Teil der gemeinsamen Git-Historie sein soll und welche ein rein lokales Build-Artefakt bleibt. composer.json beschreibt gewünschte Versionsbereiche, composer.lock hält den exakt aufgelösten Abhängigkeitsbaum fest, und vendor/ ist schließlich das materialisierte Ergebnis dieser Auflösung auf der Festplatte.

Die zentrale Regel, die diesen Artikel durchzieht, lässt sich vorab zusammenfassen: composer.lock wird committet, vendor/ nicht. Aus dieser einen Entscheidung folgen fast alle weiteren Praktiken, angefangen bei reproduzierbaren Installationen über CI-Pipelines bis hin zur sicheren Einbindung privater Magento-Marketplace-Pakete. Die folgenden Abschnitte bauen dieses Regelwerk Stück für Stück auf, inklusive der wenigen begründeten Ausnahmen und der typischen Stolperfallen beim Zusammenspiel von Composer und Git im Team.

2. composer.json und composer.lock: zwei Dateien, zwei Aufgaben

composer.json ist von Menschen verfasst und beschreibt Versionsbereiche als Absicht: "magento/module-catalog": "^2.4" bedeutet "irgendeine kompatible 2.4.x-Version", nicht eine exakte Version. composer.lock dagegen ist maschinengeneriert und speichert für jedes Paket, inklusive aller transitiven Abhängigkeiten, exakt eine aufgelöste Version samt Commit-Referenz und Prüfsumme. Diese Trennung ist bewusst: composer.json bleibt lesbar und gibt den erlaubten Spielraum vor, während composer.lock Determinismus garantiert.

Der Unterschied zeigt sich direkt im Verhalten der Befehle: composer install liest, sofern vorhanden, ausschließlich composer.lock und ignoriert die Versionsbereiche in composer.json für bereits gelockte Pakete komplett. composer update dagegen ignoriert das bestehende Lock-File gezielt für die angegebenen Pakete, löst die Abhängigkeiten gemäß composer.json neu auf und schreibt anschließend ein neues Lock-File. Gerade bei Magento-Installationen mit hunderten transitiven Abhängigkeiten ist das Lock-File der einzige Weg, denselben Baum zuverlässig auf jedem Rechner zu reproduzieren.


{
  "name": "mironsoft/magento-project",
  "type": "project",
  "require": {
    "php": "~8.4.0",
    "magento/product-community-edition": "2.4.8",
    "mironsoft/module-seosuite": "^1.2"
  },
  "repositories": [
    {
      "type": "composer",
      "url": "https://repo.magento.com/"
    }
  ],
  "minimum-stability": "stable",
  "prefer-stable": true,
  "config": {
    "optimize-autoloader": true,
    "sort-packages": true,
    "allow-plugins": {
      "magento/*": true,
      "cweagans/composer-patches": true
    }
  }
}

3. Warum vendor nicht ins Git-Repository gehört

vendor/ ist ein generiertes Build-Artefakt, das sich deterministisch aus composer.lock ableiten lässt, kein eigenständiger Quelltext. Wird es trotzdem committet, wächst das Repository bei einem typischen Magento-Projekt schnell um mehrere hundert Megabyte, weil jedes Modul samt sämtlicher Abhängigkeiten als Binärkopie in der Git-Historie landet und dort für immer verbleibt, selbst nach dem Löschen der Dateien in einem späteren Commit. Jede kleine Versionsänderung eines einzelnen Pakets erzeugt zudem einen riesigen, für Reviewer praktisch unlesbaren Diff über tausende Dateien hinweg.

Noch gravierender sind Merge-Konflikte innerhalb von vendor/: Zwei Branches, die unabhängig voneinander composer update ausgeführt haben, erzeugen widersprüchliche Dateiversionen, die inhaltlich keine sinnvolle Zeilen-für-Zeilen-Auflösung erlauben. Der korrekte Ansatz ist stattdessen, vendor/ konsequent in .gitignore aufzunehmen und in jeder Umgebung, ob lokal, in der CI-Pipeline oder beim Deployment, denselben Befehl composer install gegen dasselbe committete composer.lock auszuführen. Die alleinige Quelle der Wahrheit für Abhängigkeiten bleibt so klar definiert: composer.lock, nicht der materialisierte Ordner auf der Festplatte.

4. Ausnahmen: Wann vendor doch versioniert wird

Die Regel "vendor nicht committen" hat wenige, aber real begründete Ausnahmen. Die häufigste ist ein Air-Gapped-Deployment ohne Internetzugang zu Packagist oder repo.magento.com während des eigentlichen Deployment-Vorgangs, etwa in stark abgeschotteten Enterprise- oder Behördenumgebungen. Dort muss vendor/ entweder als vorab gebautes Deployment-Artefakt mitgeliefert werden, oder es existiert ein interner Composer-Proxy-Server. Eine zweite, deutlich seltenere Ausnahme sind Projekte, die direkt in vendor/ hineinpatchen, ohne einen sauberen Patch-Mechanismus zu nutzen - das ist allerdings ein Anti-Pattern, kein akzeptierter Grund.

Für beide Fälle existieren bessere Alternativen als das Committen des gesamten Ordners. Air-Gapped-Umgebungen profitieren von einer CI-Pipeline, die vendor/ einmal als Build-Artefakt (Zip oder Tarball) erzeugt und dieses Artefakt, nicht den Git-Verlauf, an das Zielsystem ausliefert, kombiniert mit einem privaten Composer-Mirror wie Satis oder Private Packagist. Lokale Patches an Drittanbieter-Paketen gehören mit cweagans/composer-patches versioniert: Dabei wird nur die eigentliche Diff-Datei im eigenen Repository gespeichert, während vendor/ weiterhin generiert bleibt und der Patch bei jeder Installation automatisch erneut angewendet wird.

5. Private Magento-Marketplace-Pakete in composer.json einbinden

Magento Commerce-Komponenten und kommerzielle Erweiterungen aus dem Magento Marketplace werden nicht über das öffentliche Packagist ausgeliefert, sondern über repo.magento.com, ein privates Composer-Repository, das mit einem Public/Private-Key-Paar aus dem Magento-Marketplace- beziehungsweise Adobe-Commerce-Konto authentifiziert wird. Dieses Repository wird im repositories-Block von composer.json deklariert, wie im Codebeispiel in Abschnitt 2 gezeigt - diese Deklaration enthält lediglich die URL, keinerlei Zugangsdaten, und darf deshalb bedenkenlos committet werden.

Die eigentlichen Zugangsdaten, der öffentliche und private Schlüssel des Marketplace-Kontos, gehören ausschließlich in auth.json, eine von Composer automatisch erkannte, separate Datei im Projektroot. Composer fragt bei fehlender Authentifizierung interaktiv nach diesen Daten und bietet an, sie dauerhaft zu speichern - genau dieser Speicherort muss aus dem Git-Repository ferngehalten werden, damit die Trennung zwischen öffentlich sichtbarer Konfiguration und geheimen Zugangsdaten sauber erhalten bleibt.


{
  "http-basic": {
    "repo.magento.com": {
      "username": "your-public-key-from-marketplace",
      "password": "your-private-key-from-marketplace"
    }
  },
  "github-oauth": {
    "github.com": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}

6. auth.json sicher aus Git heraushalten

auth.json enthält hochsensible Zugangsdaten: Magento-Marketplace-Schlüssel, GitHub-OAuth-Tokens für erhöhte API-Ratenlimits und gegebenenfalls Tokens für Private Packagist. Sie gehört bei jedem neuen Projekt sofort in .gitignore, noch bevor der erste composer install-Aufruf stattfindet. Bei bestehenden Repositories lohnt sich eine Prüfung mit git log --all --full-history -- auth.json, um sicherzustellen, dass die Datei nicht versehentlich in einem früheren Commit gelandet ist - falls doch, reicht ein Löschen im aktuellen Stand nicht aus, die Zugangsdaten müssen als kompromittiert gelten und rotiert werden.

In CI-Pipelines sollte auth.json möglichst gar nicht als Datei im Checkout existieren. Stattdessen liest Composer die Umgebungsvariable COMPOSER_AUTH, die den Inhalt von auth.json als JSON-String enthält und von der Pipeline aus dem Secrets-Store des CI-Systems gesetzt wird. Für lokale Entwicklerrechner bietet sich eine globale auth.json unter COMPOSER_HOME an, projektübergreifend gepflegt und ebenfalls niemals im Projekt-Repository, sondern nur auf der jeweiligen Maschine.


# .gitignore: Composer- und Magento-spezifische Ausschlüsse
/vendor/
/auth.json
/var/
/generated/
/pub/static/*
!/pub/static/.htaccess
/app/etc/env.php

# composer.lock bewusst NICHT hier eintragen: es soll committet werden

7. composer.lock-Merge-Konflikte zwischen Branches lösen

Ein Merge-Konflikt in composer.lock entsteht typischerweise, wenn zwei Branches unabhängig voneinander Pakete zu composer.json hinzugefügt oder aktualisiert haben. Das Lock-File enthält neben der Paketliste einen content-hash, der die letzte bekannte composer.json abbildet, sowie verschachtelte packages- und packages-dev-Arrays mit exakten Versionen. Diese Struktur ist für Git zeilenbasiert nicht sinnvoll zusammenführbar - ein manuell im Editor aufgelöster Konflikt führt fast immer zu einem inkonsistenten Zustand, bei dem der content-hash nicht mehr zur tatsächlichen composer.json passt.

Der korrekte Workflow läuft umgekehrt: composer.json wird ganz normal von Hand gemerged, da sie klein und lesbar ist. Für composer.lock wird eine der beiden Versionen komplett übernommen, etwa mit git checkout --theirs composer.lock, und anschließend composer update --lock ausgeführt. Dieser Befehl löst keine neuen Versionen auf, sondern regeneriert ausschließlich den content-hash und die Struktur des Lock-Files passend zur gemergten composer.json, ohne bereits gepinnte Versionen zu verändern. Bei tatsächlich neu hinzugekommenen Paketen aus beiden Branches folgt danach ein gezieltes composer update paket/a paket/b --with-all-dependencies.


# composer.lock merge conflict during a feature branch merge
$ git merge feature/add-payment-module
Auto-merging composer.json
CONFLICT (content): Merge conflict in composer.json
Auto-merging composer.lock
CONFLICT (content): Merge conflict in composer.lock

# 1. Resolve composer.json by hand, it is small and human-readable
$ nano composer.json
$ git add composer.json

# 2. Never hand-edit composer.lock JSON, take one side wholesale instead
$ git checkout --theirs composer.lock

# 3. Regenerate the lock file to match the merged composer.json
$ composer update --lock

# 4. If both branches added different packages, resolve dependencies too
$ composer update mironsoft/module-payment --with-all-dependencies

# 5. Stage the regenerated, consistent lock file and finish the merge
$ git add composer.lock
$ git commit

8. Semantic-Versioning-Constraints und Reproduzierbarkeit in CI

Versionsconstraints in composer.json folgen Semantic Versioning und steuern, wie viel Bewegungsraum composer update bei einem Paket hat. Das Caret ^2.4 erlaubt jedes 2.x-Update ab 2.4 aufwärts, solange die erste Ziffer ungleich Null gleich bleibt - der übliche, empfohlene Standard für gut gepflegte Pakete, die Semver ernst nehmen. Die Tilde ~2.4 ist enger gefasst und erlaubt nur Patch-Updates innerhalb 2.4.x. Wildcard-Constraints wie * oder komplett fehlende Versionsangaben sind ein Anti-Pattern: Sie machen jedes composer update zu einer unvorhersehbaren Blackbox, während zu enge exakte Pins Sicherheitsupdates unnötig blockieren.

Reproduzierbarkeit in der CI-Pipeline entsteht nicht durch die Constraints selbst, sondern dadurch, dass jeder Pipeline-Lauf exakt dasselbe committete composer.lock installiert, nie ein frisches composer update ausführt. Zusätzliche CI-Gates erhöhen die Sicherheit weiter: composer validate --strict prüft die Konsistenz von composer.json, composer.lock Aktualität lässt sich vorab mit einem einfachen Diff-Check gegen die letzte bekannte content-hash verifizieren, und composer audit meldet bekannte Sicherheitslücken in den exakt gelockten Versionen, bevor sie ins Deployment gelangen.

9. Composer in der Magento-Deployment-Pipeline

Die produktive Magento-Installation läuft typischerweise über composer install --no-dev --optimize-autoloader: --no-dev überspringt Test- und Entwicklungsabhängigkeiten wie PHPUnit oder PHPStan, die in Produktion weder benötigt werden noch dort ein Sicherheitsrisiko darstellen sollen, und --optimize-autoloader erzeugt eine Classmap-basierte Autoloader-Konfiguration anstelle der langsameren PSR-4-Verzeichnissuche zur Laufzeit. Entscheidend ist, dass dieser Befehl exakt gegen das composer.lock läuft, das bereits in der CI-Pipeline gegen dieselbe Codebasis getestet wurde - keine abweichende, nachträglich aufgelöste Version.

Für Magento-Projekte muss die Authentifizierung gegen repo.magento.com nicht nur lokal, sondern auch auf jedem CI-Runner und jedem Deployment-Ziel verfügbar sein, üblicherweise über COMPOSER_AUTH als Pipeline-Secret. Empfehlenswert ist zudem, vendor/ als CI-Build-Artefakt einmal zu erzeugen und exakt dieses Artefakt durch alle nachfolgenden Pipeline-Stufen (Staging, Produktion) weiterzureichen, statt in jeder Umgebung erneut aufzulösen. So werden Fehlklassen wie "auf Staging installiertes Patch-Level weicht von Produktion ab" strukturell ausgeschlossen, weil buchstäblich derselbe Ordner ausgeliefert wird.


# CI pipeline step: reproducible install using the committed lock file
$ composer validate --strict --no-check-publish
$ composer install --no-dev --optimize-autoloader --no-interaction --no-progress
$ composer audit --format=summary

# Production deployment, same lock file, same resolved dependency tree
$ COMPOSER_AUTH='{"http-basic":{"repo.magento.com":{"username":"$MAGENTO_PUBLIC_KEY","password":"$MAGENTO_PRIVATE_KEY"}}}' \
  composer install --no-dev --optimize-autoloader --no-interaction

$ bin/magento setup:di:compile
$ bin/magento setup:static-content:deploy de_DE -f

Die folgende Tabelle stellt gängige Composer/Git-Anti-Patterns dem empfohlenen Vorgehen gegenüber und fasst zusammen, worauf es in jedem der besprochenen Bereiche konkret ankommt.

Bereich Anti-Pattern Empfohlenes Pattern Warum
vendor/ im Repository vendor/ wird committet vendor/ in .gitignore, composer.lock committet Kleines Repo, keine unlösbaren Binär-Diffs
composer.lock Handling composer.lock ebenfalls ignoriert composer.lock immer committet Garantiert identischen Dependency-Baum überall
Private-Paket-Zugang auth.json mit Tokens committet auth.json in .gitignore, COMPOSER_AUTH in CI Verhindert geleakte Marketplace-Schlüssel
Lock-File-Konflikte Konflikt manuell im JSON editiert composer.json mergen, dann composer update --lock Vermeidet inkonsistenten content-hash
Versionsconstraints Wildcard "*" oder keine Angabe Caret-Constraints (^) plus committetes Lock Kontrollierte Updates, trotzdem reproduzierbar

Mironsoft

Git-Workflows, Composer-Setups und CI/CD-Pipelines für PHP- und Magento-Teams

Reproduzierbare Deployments für euren Magento-Shop?

Wir helfen Entwicklerteams, Composer- und Git-Workflows so aufzusetzen, dass jede Installation von lokal über CI bis Produktion exakt denselben Abhängigkeitsbaum ausliefert, inklusive sicherer Anbindung privater Magento-Marketplace-Pakete.

Composer-Audit

Bestehende composer.json/lock-Setups auf Reproduzierbarkeit und Sicherheit prüfen

CI/CD-Pipelines

composer install, Audit-Gates und Build-Artefakte sauber in eure Pipeline integrieren

Marketplace-Anbindung

repo.magento.com und private Pakete sicher ohne geleakte Zugangsdaten einbinden

10. Zusammenfassung

Das Zusammenspiel von Composer und Git lässt sich auf eine klare Grundregel reduzieren: composer.lock gehört ins Repository, vendor/ nicht. composer.json beschreibt gewünschte Versionsbereiche nach Semantic Versioning, composer.lock pinnt den exakt aufgelösten Abhängigkeitsbaum, und nur die konsequente Installation gegen dieses Lock-File in jeder Umgebung, lokal, in CI und im Deployment, garantiert reproduzierbare Ergebnisse. Ausnahmen wie Air-Gapped-Deployments bestätigen die Regel, lösen das Problem aber über Build-Artefakte und private Mirrors, nicht über committeten Code.

Private Magento-Marketplace-Pakete werden über repo.magento.com in composer.json deklariert, während die eigentlichen Zugangsdaten strikt in auth.json beziehungsweise COMPOSER_AUTH bleiben und niemals ins Repository gelangen. Merge-Konflikte im Lock-File löst man nie durch manuelles JSON-Editieren, sondern durch Mergen der lesbaren composer.json und anschließendes composer update --lock. Wer diese Prinzipien konsequent anwendet, eliminiert eine ganze Klasse von Deployment-Fehlern, bevor sie überhaupt entstehen kann.

Composer und Git: Die wichtigsten Regeln auf einen Blick

composer.lock committen

Garantiert denselben Abhängigkeitsbaum lokal, in CI und in Produktion. Nie ignorieren.

vendor/ ausschließen

Generiertes Artefakt, gehört in .gitignore. Ausnahmen nur für Air-Gapped-Deployments über Build-Artefakte.

auth.json niemals committen

Marketplace-Schlüssel und Tokens gehören in .gitignore, in CI über COMPOSER_AUTH bereitstellen.

Lock-Konflikte richtig lösen

composer.json mergen, composer.lock übernehmen, dann composer update --lock ausführen.

11. FAQ: Composer und Git im Zusammenspiel

1Soll ich vendor/ ins Git-Repository committen?
In der Regel nein. vendor/ ist ein generiertes Artefakt aus composer.lock. In .gitignore aufnehmen, composer.lock committen.
2Was ist der Unterschied zwischen composer.json und composer.lock?
composer.json definiert Versionsbereiche als Absicht. composer.lock pinnt den exakt aufgelösten Abhängigkeitsbaum für reproduzierbare Installationen.
3Wann ist es doch sinnvoll, vendor zu committen?
Vor allem bei Air-Gapped-Deployments. Besser sind aber meist CI-Build-Artefakte oder ein privater Composer-Mirror wie Satis.
4Wie binde ich private Magento-Marketplace-Pakete ein?
Über einen repositories-Eintrag für repo.magento.com in composer.json. Zugangsdaten separat in auth.json, nicht committen.
5Wie halte ich auth.json sicher aus Git heraus?
Sofort in .gitignore aufnehmen, Historie prüfen. In CI die Umgebungsvariable COMPOSER_AUTH statt einer Datei nutzen.
6Wie löse ich composer.lock-Merge-Konflikte?
composer.json von Hand mergen. composer.lock komplett übernehmen (z. B. checkout --theirs), dann composer update --lock ausführen.
7Was macht composer update --lock genau?
Regeneriert nur den content-hash und die Struktur des Lock-Files passend zur composer.json, ohne gepinnte Versionen zu ändern.
8Was bedeuten Caret (^) und Tilde (~)?
Caret erlaubt Updates bis vor der nächsten Major-Version. Tilde erlaubt nur Patch-Updates. Wildcards wie * sind ein Anti-Pattern.
9Wie stelle ich reproduzierbare Installationen in CI sicher?
Immer gegen das committete composer.lock installieren, nie neu auflösen. composer validate --strict und composer audit als CI-Gates ergänzen.
10Welcher Befehl gehört in die Magento-Deployment-Pipeline?
composer install --no-dev --optimize-autoloader gegen das getestete composer.lock, für schnelleren Autoloader ohne Test-Abhängigkeiten.