systematische Fehleranalyse, wenn eine GitLab Pipeline nur auf dem eigenen Rechner funktioniert
Kaum ein Satz frustriert ein Entwicklungsteam so zuverlaessig wie bei mir laeuft es doch, gefolgt vom Blick auf eine rote GitLab-Pipeline. Fast immer steckt die Ursache nicht im Code selbst, sondern in einem Unterschied zwischen lokaler Umgebung und CI-Runner, sei es eine andere PHP-Version, eine fehlende Extension, eine falsche Zeitzone oder ein Cache, der lokal etwas anderes enthaelt als in der Pipeline. Dieser Artikel zeigt eine systematische Vorgehensweise, um solche Diskrepanzen gezielt statt durch Raten zu finden.
Inhaltsverzeichnis
- 1. Das klassische Symptom und warum es Vertrauen kostet
- 2. Umgebungsunterschiede systematisch pruefen: PHP-Version und Extensions
- 3. Runner-Tags und Executor-Unterschiede als Fehlerquelle
- 4. Zeitzone, Locale und stille Environment-Variablen-Unterschiede
- 5. Dateisystem-Unterschiede: Gross-/Kleinschreibung, Zeilenenden und lokale Dateien
- 6. Job-Logs gezielt lesen mit CI_DEBUG_TRACE
- 7. Lokale Reproduktion mit gitlab-runner exec und identischem Docker-Image
- 8. Caching-Fallen: Der CI-Cache enthaelt etwas anderes als lokal
- 9. Systematische Checkliste fuer die naechste rote Pipeline
- 10. Zusammenfassung
- 11. FAQ
1. Das klassische Symptom und warum es Vertrauen kostet
Wenn ein Test, ein Build-Schritt oder ein Deploy-Job lokal anstandslos durchlaeuft, in GitLab CI aber fehlschlaegt, ist die erste Reaktion meist Verwirrung: Der Code hat sich schliesslich nicht veraendert, nur die Umgebung, in der er ausgefuehrt wird. Genau dieser Kontrast ist der wichtigste Hinweis, denn er verschiebt die Fehlersuche sofort weg vom Code und hin zur Umgebung, in der Code lokal und in der Pipeline jeweils laeuft.
Wiederholt sich dieses Muster oefter, sinkt das Vertrauen ins CI-System insgesamt, Entwickler beginnen, rote Pipelines vorschnell als Umgebungsproblem abzutun und uebersehen dabei irgendwann echte Fehler. Eine systematische, wiederholbare Vorgehensweise bei der Fehlersuche verhindert genau dieses Muster, weil sie die Ursache statistisch zuverlaessig findet, statt auf Zufallstreffer beim Ausprobieren zu hoffen.
2. Umgebungsunterschiede systematisch pruefen: PHP-Version und Extensions
Der haeufigste Einzelgrund fuer ein lokal-gruen-CI-rot-Muster ist eine abweichende PHP-Version oder eine PHP-Extension, die lokal installiert ist, im CI-Image aber fehlt. Ein lokal per Homebrew oder System-Paketmanager installiertes PHP 8.4 mit allen ueblichen Extensions unterscheidet sich haeufig erheblich von einem schlanken CI-Image, das bewusst minimal gehalten ist und etwa ext-intl oder ext-bcmath gar nicht mitbringt.
Der zuverlaessigste erste Schritt ist deshalb, php -v und php -m sowohl lokal als auch als erster Schritt im CI-Job auszugeben und die beiden Ausgaben direkt zu vergleichen, statt zu raten. Weicht die Extension-Liste ab, laesst sich das entweder durch Anpassen des CI-Images oder durch einen expliziten docker-php-ext-install-Schritt vor dem eigentlichen Job beheben, je nachdem, ob eigenes Images gepflegt wird oder ein Standard-Image zum Einsatz kommt.
debug_environment:
stage: .pre
script:
- echo "PHP version:"
- php -v
- echo "Loaded extensions:"
- php -m
- echo "Timezone:"
- php -r 'echo date_default_timezone_get() . PHP_EOL;'
- echo "Locale:"
- locale
3. Runner-Tags und Executor-Unterschiede als Fehlerquelle
GitLab-Runner koennen ueber Tags gezielt bestimmten Jobs zugewiesen werden, was in gewachsenen Projekten oft dazu fuehrt, dass verschiedene Jobs auf unterschiedlichen Runnern mit unterschiedlichen Basis-Images oder sogar unterschiedlichen Executor-Typen laufen, etwa Docker-Executor auf dem einen Runner und Shell-Executor auf einem anderen. Ein Job, der zufaellig auf einen Runner mit veraltetem Cache-Layer oder einer aelteren Docker-Image-Version geroutet wird, kann fehlschlagen, waehrend derselbe Job auf einem anderen Runner problemlos durchlaeuft.
Ein gezielter Blick auf das Feld Runner in der Job-Detailansicht in GitLab zeigt sofort, welcher konkrete Runner den fehlgeschlagenen Job ausgefuehrt hat, inklusive dessen Tags und Executor-Typ. Bei Verdacht auf Runner-spezifische Unterschiede hilft ein expliziter Vergleich zweier Runs auf unterschiedlichen Runnern, am einfachsten durch ein erzwungenes erneutes Ausfuehren des Jobs mit einem geaenderten Tag in der Job-Definition.
4. Zeitzone, Locale und stille Environment-Variablen-Unterschiede
Container-Images setzen haeufig standardmaessig die Zeitzone UTC, waehrend ein lokaler Entwicklungsrechner auf die Zeitzone Europe/Berlin konfiguriert ist. Tests, die Datumsvergleiche durchfuehren oder Zeitstempel gegen einen erwarteten Wert pruefen, laufen dadurch lokal unauffaellig durch, scheitern aber in der Pipeline an einer Differenz von ein oder zwei Stunden, die im Testcode nie explizit beruecksichtigt wurde.
Aehnlich verhaelt es sich mit der Locale: Ein deutschsprachiges lokales System formatiert Dezimalzahlen mit Komma statt Punkt, was bei Tests, die auf number_format oder aehnliche Funktionen ohne explizite Locale-Angabe vertrauen, zu unterschiedlichen Ergebnissen fuehren kann. Die robuste Loesung ist, Zeitzone und Locale in Tests niemals implizit von der Systemumgebung abhaengen zu lassen, sondern sie explizit im Testcode oder in der PHPUnit-Konfiguration zu setzen.
# Zeitzone und Locale explizit im CI-Job setzen,
# statt sich auf das Container-Image zu verlassen
export TZ="Europe/Berlin"
export LC_ALL="de_DE.UTF-8"
php -r 'echo date_default_timezone_get() . PHP_EOL;'
5. Dateisystem-Unterschiede: Gross-/Kleinschreibung, Zeilenenden und lokale Dateien
Ein macOS-Dateisystem ist standardmaessig case-insensitiv, waehrend der Linux-Container, in dem der CI-Job laeuft, strikt zwischen Gross- und Kleinschreibung unterscheidet. Ein require 'Vendor/Module/Model/MyClass.php', das lokal trotz falscher Gross-Kleinschreibung im Pfad anstandslos funktioniert, weil macOS beide Schreibweisen als identisch behandelt, schlaegt im CI-Container mit einer eindeutigen Datei-nicht-gefunden-Fehlermeldung fehl, obwohl der Code selbst unveraendert ist.
Auch Zeilenenden koennen zum Problem werden, wenn Git lokal CRLF in LF umwandelt oder umgekehrt, abhaengig von der core.autocrlf-Konfiguration, wodurch ein Shell-Skript mit Windows-Zeilenenden im CI-Container mit einer kryptischen bad interpreter-Fehlermeldung abbricht. Ebenso trueglich sind lokale, ueber .gitignore ausgeschlossene Konfigurationsdateien, die lokal vorhanden sind, im frisch geklonten CI-Workspace aber komplett fehlen, weshalb ein Vergleich der tatsaechlich im Repository vorhandenen Dateien ueber git ls-files vor der Fehlersuche in anderen Bereichen hilfreich ist.
# Pruefen, ob eine Datei tatsaechlich im Repository verfolgt wird,
# statt nur lokal ungetrackt vorhanden zu sein
git ls-files | grep -i "config/local"
# Zeilenenden einer Datei pruefen
file scripts/deploy.sh
6. Job-Logs gezielt lesen mit CI_DEBUG_TRACE
Der Standard-Output eines GitLab-Jobs zeigt normalerweise nur den script:-Abschnitt und dessen Ausgabe, nicht aber, welche Umgebungsvariablen tatsaechlich gesetzt sind oder in welcher genauen Reihenfolge Shell-Befehle ausgefuehrt werden. Die Projekt- oder Pipeline-Variable CI_DEBUG_TRACE auf true gesetzt aktiviert einen deutlich detaillierteren Trace-Modus, der jeden einzelnen ausgefuehrten Shell-Befehl inklusive aufgeloester Variablenwerte protokolliert.
Weil CI_DEBUG_TRACE potenziell sensible Werte wie Secrets im Klartext ins Log schreiben kann, sollte es ausschliesslich temporaer fuer die Fehlersuche und niemals dauerhaft in einer produktiven Pipeline aktiviert bleiben. Ergaenzend hilft es, bei Bedarf gezielt Artefakte wie Log-Dateien oder generierte Konfigurationsdateien ueber artifacts: when: on_failure zu sichern, um sie nach einem gescheiterten Lauf lokal genauer zu untersuchen.
variables:
CI_DEBUG_TRACE: "true" # nur temporaer zur Fehlersuche aktivieren
test_job:
stage: test
script:
- vendor/bin/phpunit
artifacts:
when: on_failure
paths:
- var/log/
- var/report/
expire_in: 3 days
7. Lokale Reproduktion mit gitlab-runner exec und identischem Docker-Image
Statt jede Hypothese durch einen erneuten Commit und einen weiteren Pipeline-Lauf zu testen, was Minuten kostet und Runner-Kapazitaet bindet, laesst sich das exakte CI-Image lokal mit docker run -it starten und der fragliche Schritt manuell und interaktiv ausfuehren. So wird sofort sichtbar, ob das Problem tatsaechlich am Image liegt oder erst durch die Pipeline-Konfiguration entsteht.
Fuer Shell-Executor-basierte Runner bietet der GitLab Runner zusaetzlich den Befehl gitlab-runner exec docker , mit dem sich ein einzelner Job weitgehend so ausfuehren laesst, wie er es auch im echten Pipeline-Lauf wuerde, inklusive derselben before_script- und script-Abschnitte, allerdings ohne die vollen GitLab-CI-Variablen aus dem Projektkontext, die separat gesetzt werden muessen.
# Exaktes CI-Image lokal starten und interaktiv untersuchen
docker run -it --rm registry.gitlab.com/mironsoft/php:8.4-ci bash
# Innerhalb des Containers:
php -v
composer install --no-dev
vendor/bin/phpunit --filter TestThatFailsInCi
8. Caching-Fallen: Der CI-Cache enthaelt etwas anderes als lokal
GitLab-CI-Caches fuer Composer- oder npm-Abhaengigkeiten werden ueber einen konfigurierten Cache-Key zwischen Pipeline-Laeufen wiederverwendet, koennen dabei aber veraltete oder inkompatible Versionen enthalten, wenn sich die composer.lock geaendert hat, der Cache-Key selbst aber nicht mit aktualisiert wurde. Lokal wird meist ein frischer composer install ausgefuehrt, wodurch dieselbe Inkonsistenz dort nie auftritt.
Ein bewaehrter erster Diagnoseschritt ist, den Cache fuer einen einzelnen Testlauf komplett zu deaktivieren, etwa durch einen manuell angestossenen Pipeline-Lauf mit geleertem Cache ueber die GitLab-Oberflaeche, und zu pruefen, ob der Fehler dann verschwindet. Verschwindet er, liegt die Ursache mit hoher Wahrscheinlichkeit im Cache-Key-Schema, das idealerweise den Hash der composer.lock mit einbezieht, statt eines statischen, nie wechselnden Schluessels.
9. Systematische Checkliste fuer die naechste rote Pipeline
Statt einzelne Hypothesen wahllos durchzuprobieren, lohnt sich eine feste Reihenfolge: zuerst PHP-Version und Extensions vergleichen, dann Zeitzone und Locale pruefen, dann den ausfuehrenden Runner und dessen Tags identifizieren, danach den Cache-Zustand hinterfragen, und erst danach, falls alles andere unauffaellig bleibt, tatsaechlich Codeaenderungen als Ursache in Betracht ziehen.
Diese Reihenfolge ist bewusst nach Haeufigkeit sortiert: Umgebungsunterschiede sind statistisch die weitaus haeufigste Ursache fuer ein lokal-gruen-CI-rot-Muster, waehrend ein tatsaechlicher, umgebungsunabhaengiger Bug im Code, der lokal zufaellig nicht auftritt, deutlich seltener vorkommt. Die folgende Tabelle fasst die wichtigsten Diagnosewerkzeuge und ihre jeweiligen Anwendungsfaelle zusammen.
| Ursache | Diagnosewerkzeug | Typisches Symptom | Fix |
|---|---|---|---|
| Abweichende PHP-Version/Extension | php -v, php -m im Job | Fatal error: undefined function | CI-Image anpassen oder Extension nachinstallieren |
| Zeitzonen-Unterschied | date_default_timezone_get() | Datumsvergleich weicht um Stunden ab | TZ explizit im Job setzen |
| Falscher Runner/Executor | Runner-Feld im Job-Detail | Nur auf bestimmten Runnern rot | Tags gezielt einschraenken |
| Veralteter Cache | Pipeline mit geleertem Cache | Fehler nach Dependency-Update | Cache-Key an Lock-Datei-Hash koppeln |
| Fehlende Umgebungsvariable | CI_DEBUG_TRACE | Unerwartet leere Konfiguration | Variable in Projekteinstellungen ergaenzen |
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
Lokal vs. CI Fehleranalyse: Das Wichtigste auf einen Blick
Wichtigster Reflex
Bei lokal-gruen-CI-rot zuerst die Umgebung vergleichen, nicht den Code, denn Umgebungsunterschiede sind die haeufigste Ursache.
Erster Diagnoseschritt
php -v und php -m als eigener Job-Schritt ausgeben und direkt mit der lokalen Ausgabe vergleichen.
Detailliertes Logging
CI_DEBUG_TRACE=true nur temporaer aktivieren, es kann Secrets im Klartext ins Log schreiben.
Lokale Reproduktion
Das exakte CI-Image per docker run -it interaktiv starten statt jede Hypothese per neuem Commit zu testen.