von Composer und npm
Die meisten GitLab-CI-Anleitungen zeigen Caching anhand von composer.lock oder package-lock.json und hoeren dort auf. Wer mehrere unabhaengige Caches pro Job braucht oder Invalidierung praeziser steuern will, muss tiefer in cache:key einsteigen.
Inhaltsverzeichnis
- 1. Warum der einfache Branch-Name als Cache-Key nicht reicht
- 2. cache:key:files: Hash-basierte Invalidierung ueber Lock-Dateien
- 3. cache:key:prefix: Denselben Hash fuer verschiedene Kontexte trennen
- 4. Mehrere unabhaengige Caches in einem einzigen Job
- 5. Cache-Policy: pull, push und pull-push gezielt einsetzen
- 6. Fallback-Keys fuer den Fall eines Cache-Misses
- 7. Cache und Artifacts nicht verwechseln
- 8. Cache-Verhalten debuggen und Fehlkonfigurationen finden
- 9. Eine praktikable Gesamtstrategie fuer wachsende Projekte
- 10. Zusammenfassung
- 11. FAQ
1. Warum der einfache Branch-Name als Cache-Key nicht reicht
Die einfachste Form eines Cache-Keys in GitLab CI ist ein fester String oder der Branch-Name ueber $CI_COMMIT_REF_SLUG. Das funktioniert fuer kleine Projekte ausreichend gut, hat aber einen strukturellen Nachteil: Der Cache wird pro Branch neu aufgebaut, selbst wenn sich die eigentlichen Abhaengigkeiten seit dem letzten Lauf gar nicht geaendert haben. Bei einem Feature-Branch, der zehnmal am Tag gepusht wird, aber die package-lock.json seit Tagen unveraendert ist, bedeutet das trotzdem im schlimmsten Fall wiederholtes Neuinstallieren, wenn der Cache aus irgendeinem Grund evictet wurde.
Das eigentliche Ziel eines guten Cache-Keys ist nicht, den Branch abzubilden, sondern den Zustand der Eingabedaten, die den Cache-Inhalt bestimmen. Wenn sich die Lock-Datei nicht aendert, sollte auch der Cache-Key gleich bleiben, unabhaengig davon, auf welchem Branch oder in welcher Pipeline er verwendet wird. Genau das leistet cache:key:files, indem es den Key aus dem Hash bestimmter Dateien ableitet statt aus Metadaten wie dem Branch-Namen.
2. cache:key:files: Hash-basierte Invalidierung ueber Lock-Dateien
Mit cache:key:files gibt man eine Liste von Dateien an, deren Inhalt GitLab zu einem Hash verrechnet und als Cache-Key verwendet. Aendert sich auch nur ein Byte in einer dieser Dateien, entsteht automatisch ein neuer Key und damit ein frischer, leerer Cache, waehrend der alte Cache-Eintrag unangetastet bleibt und spaeter regulaer ausgemustert wird. Bleiben die Dateien unveraendert, wird ueber Branches und Pipelines hinweg konsequent derselbe Cache wiederverwendet, was Neuinstallationen von Abhaengigkeiten drastisch reduziert.
Der Mechanismus laesst sich auf mehrere Dateien gleichzeitig anwenden, was besonders bei Monorepos mit mehreren Sprachen wichtig ist: Ein gemeinsamer Key, der sowohl composer.lock als auch package-lock.json einbezieht, aendert sich, sobald irgendeine der beiden Dateien sich aendert. Fuer echte Praezision sollte man aber meist mehrere getrennte Caches mit jeweils eigenem, spezifischem key:files-Eintrag definieren, damit eine Aenderung an der einen Abhaengigkeitsdatei nicht unnoetig den Cache der anderen Sprache invalidiert.
install_php:
stage: install
image: composer:2
cache:
key:
files:
- composer.lock
paths:
- vendor/
script:
- composer install --no-progress --prefer-dist
install_node:
stage: install
image: node:20-alpine
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
script:
- npm ci
3. cache:key:prefix: Denselben Hash fuer verschiedene Kontexte trennen
Ein reiner Hash aus key:files hat einen Nachteil: Zwei Jobs mit identischer Lock-Datei, aber unterschiedlichem Kontext, etwa ein Job fuer PHP 8.3 und einer fuer PHP 8.4 im selben Projekt, wuerden ohne weitere Unterscheidung denselben Cache-Key erhalten und sich gegenseitig ueberschreiben. Genau hier kommt prefix ins Spiel: Es wird dem berechneten Hash vorangestellt und erlaubt, denselben inhaltlichen Cache-Key fuer verschiedene Matrix-Dimensionen wie PHP-Version, Betriebssystem-Image oder Job-Namen sauber zu trennen.
In der Praxis kombiniert man prefix haeufig mit einer CI-Variable aus der Matrix-Konfiguration, etwa dem PHP_VERSION-Wert eines parallelisierten Jobs. Dadurch entsteht pro Kombination aus Lock-File-Zustand und PHP-Version ein eigener, klar abgegrenzter Cache-Eintrag, der weder mit anderen Versionen kollidiert noch unnoetig oft neu aufgebaut wird, wenn sich nur die Lock-Datei, aber keine der Matrix-Dimensionen aendert.
test_matrix:
stage: test
parallel:
matrix:
- PHP_VERSION: ["8.2", "8.3", "8.4"]
image: php:${PHP_VERSION}-cli
cache:
key:
files:
- composer.lock
prefix: "php-${PHP_VERSION}"
paths:
- vendor/
script:
- composer install --no-progress
- vendor/bin/phpunit
4. Mehrere unabhaengige Caches in einem einzigen Job
Seit GitLab 13.9 akzeptiert die cache-Direktive nicht nur ein einzelnes Objekt, sondern auch eine Liste mehrerer Cache-Definitionen innerhalb desselben Jobs. Das ist besonders wertvoll, wenn ein Job mehrere unabhaengige Artefaktarten produziert, die sich unterschiedlich oft aendern, etwa Composer-Abhaengigkeiten, die selten wechseln, und ein Build-Verzeichnis mit kompilierten Assets, das sich bei jeder Aenderung an den Quelldateien aendert. Ein einziger gemeinsamer Cache-Key wuerde in diesem Fall entweder zu haeufig oder zu selten invalidieren, je nachdem welche Datei man fuer den Hash waehlt.
Mit getrennten Cache-Eintraegen bekommt jede Artefaktart ihren eigenen, passend abgestimmten Key und ihre eigene Lebensdauer. Der Composer-Cache bleibt ueber viele Pipelines hinweg stabil, waehrend der Asset-Cache sich bei jeder relevanten Aenderung im Frontend-Code neu aufbaut, ohne dass die beiden sich gegenseitig beeinflussen. Diese Trennung reduziert unnoetiges Neuherunterladen von Abhaengigkeiten erheblich, ohne veraltete Build-Artefakte zu riskieren.
build_app:
stage: build
image: node:20-alpine
cache:
- key:
files:
- package-lock.json
paths:
- node_modules/
- key:
files:
- webpack.config.js
prefix: "assets"
paths:
- public/build/
script:
- npm ci
- npm run build
5. Cache-Policy: pull, push und pull-push gezielt einsetzen
Standardmaessig laedt ein Job den Cache herunter und laedt ihn am Ende wieder hoch, was fuer den ersten Job einer Pipeline sinnvoll ist, aber in nachgelagerten Jobs unnoetigen Overhead erzeugt, wenn diese den Cache nur lesen, aber nicht veraendern. Die Option policy: pull weist GitLab an, den Cache zwar herunterzuladen, aber am Ende nicht erneut hochzuladen, was besonders bei Test- oder Lint-Jobs Zeit spart, die node_modules zwar brauchen, aber keine neuen Pakete installieren.
Umgekehrt eignet sich policy: push fuer den ersten Job einer Kette, der den Cache aktiv aufbaut, aber selbst keinen vorherigen Zustand braucht, etwa einen dedizierten install-Job, der ausschliesslich Abhaengigkeiten installiert. Diese Aufteilung in einen einzigen push-Job gefolgt von mehreren pull-Jobs vermeidet, dass derselbe unveraenderte Cache-Inhalt von jedem einzelnen nachgelagerten Job redundant wieder hochgeladen wird, was bei grossen node_modules-Verzeichnissen spuerbar Zeit und Netzwerkbandbreite spart.
install:
stage: install
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: push
script:
- npm ci
lint:
stage: test
needs: ["install"]
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull
script:
- npm run lint
6. Fallback-Keys fuer den Fall eines Cache-Misses
Ein reiner Hash-Key hat einen scharfen Rand: Aendert sich die Lock-Datei auch nur geringfuegig, etwa durch eine einzelne aktualisierte Paketversion, gibt es fuer den neuen Key keinen bestehenden Cache-Eintrag, und der gesamte Abhaengigkeitsbaum wird von Grund auf neu installiert. Fuer Paketmanager, die inkrementelle Installation unterstuetzen, ist es oft effizienter, wenigstens einen aelteren, aehnlichen Cache als Startpunkt zu haben, statt komplett bei null anzufangen.
GitLab bietet dafuer keinen automatischen Fallback-Mechanismus wie andere CI-Systeme, aber ein aehnlicher Effekt laesst sich erreichen, indem man einen zusaetzlichen, groeber granularen Cache mit stabilerem Key als zweite cache-Definition im Job ergaenzt, dessen Inhalt der Paketmanager als Ausgangsbasis nutzen kann, bevor der praezise gehashte Cache greift. Bei npm etwa hilft ein zusaetzlicher Cache des globalen npm-Cache-Verzeichnisses, das sich deutlich seltener und langsamer aendert als die exakte node_modules-Struktur.
7. Cache und Artifacts nicht verwechseln
Ein haeufiger Denkfehler ist, Cache und Artifacts als austauschbare Werkzeuge zu behandeln. Ein Cache ist eine Optimierung ohne Garantie: GitLab darf ihn jederzeit evictent, insbesondere bei begrenztem Speicherplatz auf dem Runner, und eine Pipeline, die auf einen leeren Cache trifft, muss trotzdem korrekt funktionieren, nur eben langsamer. Artifacts dagegen sind ein garantiertes Ergebnis eines Jobs, das an nachfolgende Jobs weitergereicht wird und ohne das die Pipeline fehlschlagen wuerde, wenn es fehlt.
Wer Build-Ergebnisse ueber cache statt artifacts zwischen Jobs derselben Pipeline weiterreicht, riskiert intermittierende Fehlschlaege, die sich nur schwer reproduzieren lassen, weil sie vom Zufall der Cache-Verfuegbarkeit auf dem jeweiligen Runner abhaengen. Die klare Regel lautet: Alles, was ein nachfolgender Job der gleichen Pipeline zwingend braucht, gehoert in artifacts, alles, was nur eine Wiederverwendungs-Optimierung ueber mehrere Pipelines hinweg darstellt, gehoert in cache.
8. Cache-Verhalten debuggen und Fehlkonfigurationen finden
Wenn ein Cache scheinbar nicht greift, obwohl die Lock-Datei unveraendert ist, lohnt sich zuerst ein Blick in das Job-Log: GitLab protokolliert dort explizit, ob ein Cache heruntergeladen wurde, unter welchem Key gesucht wurde und ob ein Treffer oder ein Miss vorlag. Ein haeufiger Fehler ist ein abweichender Pfad in cache:paths zwischen zwei Jobs, die eigentlich denselben Cache teilen sollen, sodass GitLab formal denselben Key verwendet, aber unterschiedliche Verzeichnisse sichert und wiederherstellt.
Ein weiterer klassischer Stolperstein ist ein Runner-Wechsel: Wenn Jobs derselben Pipeline auf unterschiedlichen Runnern ohne verteiltes Cache-Backend laufen, etwa bei selbstgehosteten Runnern ohne S3-basiertes Distributed Cache, kann ein auf Runner A geschriebener Cache fuer einen Job auf Runner B schlicht nicht sichtbar sein. In solchen Umgebungen lohnt sich die Einrichtung eines zentralen, S3-kompatiblen Cache-Speichers in der Runner-Konfiguration, damit Caches runnerunabhaengig funktionieren.
9. Eine praktikable Gesamtstrategie fuer wachsende Projekte
Fuer ein wachsendes Projekt mit mehreren Sprachen und Matrix-Jobs bewaehrt sich eine Kombination aller vorgestellten Techniken: getrennte Caches pro Abhaengigkeitsart mit key:files auf die jeweilige Lock-Datei, ein prefix pro Matrix-Dimension wie Sprachversion, policy: push in einem dedizierten Install-Job gefolgt von policy: pull in allen nachgelagerten Jobs, und eine klare Trennung zwischen Cache fuer Wiederverwendung und Artifacts fuer garantierte Weitergabe innerhalb der Pipeline.
Die folgende Tabelle fasst die einzelnen Mechanismen und ihren jeweiligen Haupteinsatzzweck zusammen, damit sich beim Aufbau einer neuen Pipeline schnell die passende Kombination auswaehlen laesst, statt bei jedem neuen Job wieder von vorne über die richtige Cache-Konfiguration nachzudenken.
| Mechanismus | Zweck | Typisches Beispiel | Faellstrick |
|---|---|---|---|
| cache:key:files | Hash-basierte Invalidierung | composer.lock, package-lock.json | Mehrere Dateien im selben Key vermischen Aenderungen |
| cache:key:prefix | Kontexte trennen (Matrix) | PHP-Version, Node-Version | Prefix vergessen: Matrix-Jobs ueberschreiben sich |
| Mehrere Caches pro Job | Unterschiedliche Lebensdauer | Dependencies vs. Build-Assets | Ein Key fuer alles invalidiert zu oft oder zu selten |
| policy: pull/push | Overhead reduzieren | Install-Job push, Test-Jobs pull | Alle Jobs pull-push: unnoetiges Re-Upload |
Mironsoft
CI/CD-Pipelines, Zero-Downtime-Deployments und Release-Automatisierung
Deployments, die ohne Ausfallzeit und ohne Nervenkitzel laufen?
Wir prüfen bestehende GitLab-Pipelines auf fragile Deployment-Schritte und fehlende Absicherung und bauen daraus einen Release-Prozess mit Zero-Downtime-Deployments, automatisierten Checks und einem Rollback, dem ihr im Ernstfall vertrauen könnt.
Pipeline-Review
Bestehende .gitlab-ci.yml auf Fragilität, fehlende Stages und Sicherheitslücken prüfen.
Zero-Downtime-Deployment
Symlink-Releases, Health-Checks und Rollback-Strategien für Magento-Shops aufbauen.
CI/CD-Automatisierung
Tests, Security-Scans und Deployments zu einer zuverlässigen Pipeline verbinden.
10. Zusammenfassung
Cache-Key-Strategien: Das Wichtigste auf einen Blick
key:files
Cache-Key aus Datei-Hash statt Branch-Name, invalidiert nur bei echter Aenderung.
prefix
Trennt identische Hashes ueber Matrix-Dimensionen wie PHP- oder Node-Version.
Mehrere Caches
Unterschiedliche Artefaktarten bekommen eigene Keys und eigene Lebensdauer.
policy
push im Install-Job, pull in nachgelagerten Jobs spart Zeit und Bandbreite.