CI/CD
.yml
GitLab · Magento Build · Composer · DI Compile · Assets
Magento Build-Stage definieren
Composer, Node, Assets, DI Compile

Die Build-Stage ist der Grundstein des gesamten Deployment-Prozesses. Wer hier auf Serverabhängigkeiten setzt, einen undefinierten Node-Cache mitschleppt oder DI Compile auf der Produktionsmaschine ausführt, schafft Risiken, die später als Deployment-Probleme auftauchen. Eine saubere Build-Stage erzeugt ein vollständiges, server-unabhängiges Artefakt.

12 Min. Lesezeit Composer · npm · Tailwind CSS · DI Compile · Static Content Magento 2.4 · PHP 8.4 · Node 20 · GitLab CI

1. Was die Build-Stage leisten muss

Die Build-Stage in einer GitLab-Pipeline für Magento hat eine einzige Verantwortung: aus dem Repository-Inhalt ein vollständiges, deployfähiges Artefakt zu erzeugen. Vollständig bedeutet dabei, dass alle Abhängigkeiten installiert, alle Assets kompiliert, der DI-Code generiert und alle statischen Inhalte erzeugt sind. Server-unabhängig bedeutet, dass das Artefakt auf jedem korrekt konfigurierten Zielserver aktiviert werden kann, ohne dass dort noch Build-Tools wie Composer oder Node installiert sein müssen.

Viele Teams beginnen mit einem Build-Job, der zu wenig tut – etwa nur Composer ausführt und den Rest dem Server überlässt. Das funktioniert anfangs, führt aber dazu, dass der Serverzustand und das Artefakt vermischt werden. Wenn auf dem Server ein anderes PHP installiert ist, eine andere Composer-Version oder ein anderer Node, entstehen subtile Unterschiede zwischen Staging und Production. Die Build-Stage muss deshalb vollständig kontrolliert und reproduzierbar sein: gleicher Input, gleicher Output, unabhängig davon, welcher GitLab-Runner den Job ausführt.

Die Reihenfolge der Build-Schritte in Magento ist nicht beliebig. Composer muss zuerst laufen, weil DI Compile die installierten Vendor-Klassen kennen muss. DI Compile muss vor Static Content Deploy laufen, weil der Static Content Deployment-Prozess auf generierten Klassen aufbaut. Node-Assets können parallel zu Composer laufen, müssen aber vor Static Content Deploy abgeschlossen sein, wenn Frontend-Templates CSS-Klassen referenzieren, die erst durch den Tailwind-Build entstehen.

2. Composer install: reproduzierbar und sicher

Der Composer-install-Befehl in der Build-Stage muss immer mit --no-dev, --prefer-dist, --no-interaction und --optimize-autoloader ausgeführt werden. --no-dev schließt Development-Abhängigkeiten aus dem Build-Artefakt aus – diese werden nur im Test-Job benötigt. --prefer-dist bevorzugt tar.gz-Downloads statt git-Klone, was bei vielen Paketen schneller ist. --optimize-autoloader generiert eine flache Autoload-Map, die im Produktionsbetrieb schneller ist als die reguläre PSR-4-Auflösung.

Die composer.lock-Datei muss im Repository eingecheckt sein und darf in der Pipeline niemals durch composer update verändert werden. Nur composer install auf Basis der Lock-Datei garantiert, dass die Build-Stage bei jedem Lauf exakt dieselben Pakete in denselben Versionen installiert. Der Composer-Auth-Token für private Pakete und Magento-Repos darf niemals im Repository stehen, sondern muss als GitLab CI/CD Variable hinterlegt und per COMPOSER_AUTH-Umgebungsvariable oder auth.json übergeben werden.

# Build stage: PHP dependencies, Node assets, DI compile, static content
build:magento:
  stage: build
  image: php:8.4-fpm-alpine
  services:
    - name: node:20-alpine
      alias: node
  cache:
    - key: "composer-${CI_COMMIT_REF_SLUG}"
      paths: [".cache/composer/"]
      policy: pull-push
    - key: "npm-${CI_COMMIT_REF_SLUG}"
      paths: [".cache/npm/"]
      policy: pull-push
  variables:
    COMPOSER_CACHE_DIR: ".cache/composer"
    NPM_CONFIG_CACHE: ".cache/npm"
    COMPOSER_MEMORY_LIMIT: "-1"
  before_script:
    # Install system dependencies for PHP extensions
    - apk add --no-cache git unzip libzip-dev icu-dev oniguruma-dev
    - docker-php-ext-install zip intl mbstring bcmath
    # Install composer from official image
    - EXPECTED_CHECKSUM="$(php -r 'copy(\"https://composer.github.io/installer.sig\", \"php://stdout\");')"
    - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
    - php composer-setup.php --install-dir=/usr/local/bin --filename=composer
    # Write Composer auth for private Magento repositories
    - echo "${COMPOSER_AUTH}" > auth.json
  script:
    # Step 1: Install PHP packages from composer.lock (never update)
    - composer install
        --no-dev
        --prefer-dist
        --no-interaction
        --optimize-autoloader
        --no-scripts
    # Step 2: Run Magento setup scripts after vendor is ready
    - composer run-script post-install-cmd
    # Step 3: Install Node dependencies exactly from package-lock.json
    - npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind
    # Step 4: Build Tailwind CSS (production mode — no sourcemaps)
    - npm run build:prod
        --prefix app/design/frontend/Mironsoft/default/web/tailwind
    # Step 5: Compile Magento DI — must run after vendor is installed
    - php bin/magento setup:di:compile --no-interaction
    # Step 6: Deploy static content for all configured locales
    - php bin/magento setup:static-content:deploy de_DE en_US
        --force --jobs=4 --no-html-minify
  after_script:
    # Remove auth credentials from build context before artifact upload
    - rm -f auth.json
  artifacts:
    name: "magento-${CI_COMMIT_SHORT_SHA}-${CI_PIPELINE_ID}"
    paths:
      - vendor/
      - generated/
      - pub/static/
      - app/etc/config.php
    exclude:
      - "vendor/**/.git/**"
      - "vendor/**/*.md"
    expire_in: 1 day

3. Node und Frontend-Assets: npm ci statt npm install

Der Unterschied zwischen npm install und npm ci ist in einer Build-Stage entscheidend: npm ci installiert immer exakt die in package-lock.json festgelegten Versionen, ohne die Lock-Datei zu verändern. npm install kann die Lock-Datei aktualisieren und damit unterschiedliche Versionen zwischen lokaler Entwicklung und CI-Build erzeugen. In der Build-Stage ist npm ci Pflicht – es ist der Node-Äquivalent zu composer install auf Basis der Lock-Datei.

Der Node-Cache in GitLab funktioniert über das node_modules-Verzeichnis oder den npm-Cache-Ordner. Da npm ci node_modules vor der Installation immer löscht, ist das Cachen des npm-Cache-Ordners (.npm) sinnvoller: Er enthält heruntergeladene Pakete und vermeidet Netzwerkzugriffe bei gleichem Lock-File. Der Cache-Key sollte package-lock.json als Basis einschließen, damit der Cache bei Lock-File-Änderungen automatisch invalidiert wird.

4. Tailwind CSS v4 in der Build-Stage

Tailwind CSS v4 nutzt einen CSS-first-Ansatz, bei dem die Konfiguration in einer CSS-Datei statt in tailwind.config.js stattfindet. Der Build-Befehl ist npx @tailwindcss/cli build oder über das npm-Script, das im package.json des Theme-Verzeichnisses definiert ist. In der Build-Stage muss der Produktions-Build ausgeführt werden – kein Watch-Modus, kein Sourcemap, kein unoptimiertes CSS.

Das Output-CSS wird in pub/static/frontend/Mironsoft/default/de_DE/css/styles.css erwartet, oder über den Static Content Deploy-Prozess dorthin verteilt. In der Pipeline muss sichergestellt werden, dass das CSS vor dem Static Content Deploy erzeugt wird, damit der Magento-Deployment-Prozess die fertige CSS-Datei aufnimmt. Das Tailwind-Output-Verzeichnis muss im Artefakt-Pfad enthalten sein.

5. setup:di:compile: warum und wann

setup:di:compile ist einer der zeitaufwändigsten und kritischsten Schritte im Magento-Build-Prozess. Er analysiert alle PHP-Klassen im Vendor- und Code-Verzeichnis, generiert Interceptors für Plugins, erstellt Proxy-Klassen für zirkuläre Abhängigkeiten und erzeugt Factory-Klassen für alle Interfaces. Das Ergebnis liegt in generated/ und ermöglicht Magento, im Produktionsmodus ohne Laufzeit-Code-Generierung zu arbeiten.

Im Build-Job muss setup:di:compile nach composer install und nach der Erzeugung einer minimalen app/etc/config.php ausgeführt werden – es braucht keine vollständige env.php, weil es keine Datenbankverbindung benötigt. Ein häufiger Fehler: DI Compile schlägt fehl, weil ein Plugin auf eine Klasse verweist, die in Vendor nicht existiert – dieser Fehler ist im Build-Job erwünscht, weil er sonst erst auf Production auftaucht. Das generated/-Verzeichnis muss vollständig im Artefakt enthalten sein und darf auf dem Server nicht mehr neu generiert werden.

6. Static Content Deploy im Build-Job

Der Befehl setup:static-content:deploy kopiert und minifiziert alle statischen Dateien aus dem Theme, den Modulen und den Vendor-Paketen in das pub/static-Verzeichnis. In der Build-Stage wird er mit --force ausgeführt, damit er auch ohne eine vollständige Datenbankverbindung läuft. Die Locales müssen explizit angegeben werden – mindestens die in der Produktionsumgebung konfigurierten.

Der Static Content Deploy ist zeitaufwändig und kann mit --jobs=4 parallelisiert werden. In Magento 2.4.x mit Hyvä Themes muss der Hyvä-kompatible Deploy-Prozess verwendet werden. Das pub/static-Verzeichnis gehört ins Artefakt und wird auf dem Server nicht neu generiert – deshalb ist es wichtig, dass der Build-Job alle nötigen Locales abdeckt.

# Optimized static content deployment configuration
.static_content_deploy: &static_deploy
  script:
    - |
      # Clear previously generated static files before fresh deploy
      rm -rf var/view_preprocessed/* pub/static/frontend/*

      # Deploy static content — parallel jobs speed up large themes
      php bin/magento setup:static-content:deploy \
        de_DE en_US \
        --theme Mironsoft/default \
        --force \
        --jobs=4 \
        --no-html-minify \
        --strategy=quick

      # Verify that critical CSS file was generated correctly
      if [ ! -f "pub/static/frontend/Mironsoft/default/de_DE/css/styles.css" ]; then
        echo "ERROR: Critical CSS file missing after static content deploy"
        exit 1
      fi
      echo "Static content deploy completed successfully"

7. Cache-Strategie für Build-Jobs in GitLab

GitLab-CI-Cache ist ein Mechanismus, um Verzeichnisse zwischen Pipeline-Läufen zu speichern und Netzwerkzugriffe zu reduzieren. Für Magento-Build-Jobs sind zwei Caches sinnvoll: der Composer-Cache-Ordner (.cache/composer/) und der npm-Cache-Ordner (.cache/npm/). Beide sollten mit einem Cache-Key versehen werden, der die jeweilige Lock-Datei als Basis einschließt – bei Änderungen der Lock-Datei wird ein neuer Cache angelegt, der alte bleibt bis zum Ablauf erhalten.

Die Cache-Policy pull-push ist für den Build-Job korrekt: Der Job liest den vorhandenen Cache und schreibt nach dem Build einen aktualisierten Cache zurück. Nachfolgende Jobs (Test, Package) können die Policy auf pull setzen, weil sie den Cache nicht verändern. Der vendor/-Ordner selbst sollte nicht im Cache liegen, sondern immer als Artefakt übertragen werden – das vermeidet subtile Inkonsistenzen zwischen Cache und tatsächlichem Composer-Zustand.

8. Artefakte richtig definieren

Der artifacts-Block des Build-Jobs definiert, welche Dateien an nachfolgende Jobs in der Pipeline weitergegeben werden. Für Magento sind das mindestens: vendor/, generated/, pub/static/ und app/etc/config.php. Der vendor/-Ordner ist groß – typischerweise mehrere hundert MB. Das exclude-Feld ermöglicht es, unnötige Dateien auszuschließen: .git-Verzeichnisse innerhalb von Vendor, Markdown-Dateien und Test-Ordner in Vendor-Paketen.

Das Artefakt-Name-Feld sollte die Commit-SHA und die Pipeline-ID enthalten, damit Artefakte eindeutig einer Pipeline zugeordnet werden können. Das expire_in-Feld verhindert, dass alte Artefakte den GitLab-Speicher belasten. Für Build-Artefakte reicht 1 day, weil der Deploy-Job in der Regel im selben Pipeline-Lauf ausgeführt wird. Für Package-Stage-Artefakte, die für manuelle Rollbacks aufbewahrt werden sollen, sind 7 days sinnvoller.

9. Build-Ansätze im Vergleich

Die Art, wie die Build-Stage definiert wird, hat direkte Auswirkungen auf die Deployment-Zuverlässigkeit und die Fehlererkennung. Wer DI Compile auf dem Produktionsserver ausführt, riskiert, dass ein Compile-Fehler den laufenden Shop unterbricht. Wer Composer ohne Lock-Datei ausführt, riskiert unterschiedliche Abhängigkeiten zwischen Pipelines.

Build-Schritt Unsicher / Langsam Empfohlen Vorteil
Composer composer update in CI composer install aus Lock Reproduzierbare, identische Abhängigkeiten
Node npm install (modifiziert Lock) npm ci aus package-lock.json Exakt gleiche Node-Pakete in jeder Pipeline
DI Compile Auf dem Produktionsserver Im Build-Job, als Artefakt Fehler vor dem Deploy erkannt, kein Risiko auf Prod
Static Content Im Deploy-Job auf dem Server Im Build-Job, als Artefakt übertragen Kein PHP-CLI-Zugriff auf Server nötig, schneller Deploy
Artefakt-Scope Ganzes Repository hochladen Nur erzeugte Verzeichnisse Kleineres Artefakt, schnellere Übertragung

Besonders der Vergleich beim Static Content Deploy zeigt den systemischen Vorteil der Build-Stage: Wenn der Deployment-Server kein PHP CLI benötigt, weil alle statischen Dateien vorgebaut sind, entfällt eine kritische Abhängigkeit. Der Zielserver wird damit zum reinen Auslieferungsserver und nicht mehr zum Build-Server – das ist die eigentliche Stärke des Artefakt-basierten Deployment-Ansatzes.

10. Zusammenfassung

Eine Magento Build-Stage in GitLab CI besteht aus vier Kernschritten in definierter Reihenfolge: Composer install aus der Lock-Datei, Node-Assets per npm ci und Tailwind-Build, DI Compile für den generierten Code und Static Content Deploy für alle konfigurierten Locales. Das Ergebnis ist ein vollständiges, server-unabhängiges Artefakt, das auf jedem Zielserver ohne weitere Build-Schritte aktiviert werden kann.

Die wichtigsten Regeln: composer install statt composer update, npm ci statt npm install, DI Compile im Build-Job statt auf dem Server, und Artefakte präzise definieren statt das gesamte Repository zu übertragen. Ein Build-Job, der diese Regeln einhält, ist die Grundlage eines zuverlässigen Deployment-Prozesses – alle nachfolgenden Stages profitieren davon, wenn dieser Schritt sauber und reproduzierbar ist.

Magento Build-Stage — Das Wichtigste auf einen Blick

Composer-Regel

composer install --no-dev --prefer-dist --optimize-autoloader – immer aus der Lock-Datei, niemals composer update in der Pipeline.

Node-Regel

npm ci aus package-lock.json – modifiziert die Lock-Datei nicht und garantiert identische Node-Pakete in jeder Pipeline.

DI Compile

Im Build-Job ausführen, als Artefakt übertragen. Compile-Fehler werden vor dem Deploy erkannt – nicht auf Production zur Laufzeit.

Artefakt-Scope

vendor/, generated/, pub/static/ und app/etc/config.php. Kein .git in Vendor, keine Markdown-Dateien, kein Test-Code im Deployment-Artefakt.

11. FAQ: Magento Build-Stage in GitLab CI

1Warum DI Compile im Build-Job und nicht auf dem Server?
DI Compile auf Production kann den laufenden Shop unterbrechen. Im Build-Job wird ein Fehler erkannt, bevor das Artefakt auf den Server kommt – sicherer und früher.
2Braucht DI Compile eine Datenbankverbindung?
Nein. Nur PHP-Klassen-Analyse, keine DB. Eine minimale config.php wird benötigt – per setup:config:set oder Vorlage erzeugen.
3npm ci vs. npm install in CI?
npm ci liest package-lock.json exakt ohne Änderung. npm install kann die Lock-Datei modifizieren – unterschiedliche Versionen zwischen Dev und CI sind die Folge.
4Wie auth.json aus dem Artefakt fernhalten?
auth.json im after_script löschen oder COMPOSER_AUTH als Umgebungsvariable setzen – Composer liest sie automatisch ohne Datei zu schreiben.
5Wie groß ist das Build-Artefakt für Magento?
vendor: 300–500 MB, pub/static: 100–300 MB. Mit exclude-Regeln für .git und Test-Code: 200–400 MB insgesamt reduzierbar.
6Static Content auf dem Server oder im Build-Job?
Im Build-Job. Der Server braucht dann kein PHP CLI – er ist reiner Auslieferungsserver, kein Build-System. Schnellerer Deploy, weniger Serverabhängigkeiten.
7Composer-Cache in GitLab richtig konfigurieren?
COMPOSER_CACHE_DIR auf .cache/composer setzen, Pfad im cache.paths angeben, Cache-Key = Branch + composer.lock-Hash. Build-Job: pull-push. Nachfolgende Jobs: pull.
8Welche PHP-Extensions braucht der Build-Job?
Mindestens: zip, intl, mbstring, bcmath, soap, xsl, gd/imagick, pdo_mysql, opcache. Ohne diese schlägt Composer oder DI Compile fehl.
9Wie lange dauert ein vollständiger Build-Job?
Mit warmem Cache: 5–10 Min. Ohne Cache: 15–25 Min. DI Compile allein: 2–5 Min. Abhängig von Modulanzahl und Netzwerk.
10Vendor im Artefakt oder Server-seitig cachen?
Immer im Artefakt. Server-seitiger Vendor-Cache mischt Serverzustand und Artefakt – das zerstört Reproduzierbarkeit und macht Rollbacks riskanter.