GitLab CI Fehleranalyse: Lokal gruen, in der Pipeline rot
AI generated
CI/CD
.yml
GitLab · CI/CD · Debugging
Lokal gruen, in CI rot
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.

17 Min. Lesezeit Debugging · Troubleshooting GitLab CI/CD · Runner

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 bash 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.

11. FAQ: Lokal vs. CI Fehleranalyse: Das Wichtigste auf einen Blick

1Warum ist eine abweichende PHP-Version die haeufigste Fehlerursache?
Weil PHP zwischen Minor-Versionen Verhalten bei Typumwandlungen, Deprecation-Warnungen und manchen Funktionssignaturen aendert. Ein lokal installiertes PHP weicht dabei haeufig unbemerkt von der im CI-Image gepflegten Version ab.
2Wie aktiviere ich CI_DEBUG_TRACE fuer einen einzelnen Job?
Als Variable entweder global in der .gitlab-ci.yml, in den Projekteinstellungen unter CI/CD Variablen, oder beim manuellen Ausloesen eines einzelnen Pipeline-Laufs ueber die GitLab-Oberflaeche mit dem Wert true.
3Ist CI_DEBUG_TRACE sicher fuer produktive Pipelines?
Nein, es protokolliert unter Umstaenden aufgeloeste Variablenwerte einschliesslich Secrets im Klartext. Es sollte deshalb nur temporaer fuer die Fehlersuche aktiviert und danach sofort wieder deaktiviert werden.
4Wie finde ich heraus, welcher konkrete Runner einen Job ausgefuehrt hat?
In der Job-Detailansicht in GitLab wird der ausfuehrende Runner mit seiner ID und seinen Tags direkt angezeigt. Das hilft, Runner-spezifische Unterschiede wie ein abweichendes Basis-Image zu identifizieren.
5Wie reproduziere ich einen CI-Job lokal, ohne staendig zu committen?
Am zuverlaessigsten mit docker run -it und exakt demselben Image, das auch in der Pipeline verwendet wird. Fuer Shell-Executor-Runner bietet sich zusaetzlich gitlab-runner exec an.
6Warum schlaegt ein Test nur in der Pipeline mit einem Datumsfehler fehl?
Meist wegen einer abweichenden Zeitzone im Container, oft UTC statt der lokal konfigurierten Zeitzone. Tests, die Zeitstempel implizit vergleichen, sollten die Zeitzone stattdessen explizit setzen.
7Kann ein veralteter Composer-Cache tatsaechlich einen CI-Fehler verursachen?
Ja, wenn der Cache-Key nicht an den Hash der composer.lock gekoppelt ist, kann ein veralteter Cache-Inhalt trotz aktueller Lock-Datei wiederverwendet werden. Ein Pipeline-Lauf mit geleertem Cache zeigt schnell, ob das die Ursache ist.
8Woran erkenne ich, ob eine fehlende PHP-Extension die Ursache ist?
Meist an einer Fehlermeldung wie Call to undefined function oder Class not found, die auf eine bestimmte Extension wie intl oder bcmath hinweist. Ein Abgleich der Ausgabe von php -m mit den lokal geladenen Extensions bestaetigt den Verdacht.
9Sollte ich Zeitzone und Locale immer explizit im Job setzen?
Fuer Tests mit Datums- oder Zahlenformatierung ja, weil dadurch die Abhaengigkeit von impliziten Image-Standardwerten entfaellt. Das macht Tests unabhaengig davon reproduzierbar, welches Basis-Image gerade verwendet wird.
10Wie sichere ich Logs eines fehlgeschlagenen Jobs fuer die spaetere Analyse?
Ueber artifacts mit when: on_failure in der Job-Definition, wodurch Log-Verzeichnisse oder generierte Reports auch bei einem gescheiterten Lauf als herunterladbares Artefakt erhalten bleiben.