Defining the Magento Build Stage: Composer, Node, Assets, DI Compile
AI generated
CI/CD
.yml
GitLab · Magento Build · Composer · DI Compile · Assets
Defining the Magento Build Stage
Composer, Node, Assets, DI Compile

The build stage is the foundation of the entire deployment process. Anyone who relies on server dependencies here, carries an undefined Node cache along, or runs DI Compile on the production machine creates risks that later surface as deployment problems. A clean build stage produces a complete artifact that does not depend on the server.

12 min read Composer · npm · Tailwind CSS · DI Compile · Static Content Magento 2.4 · PHP 8.4 · Node 20 · GitLab CI

1. What the build stage has to deliver

The build stage in a GitLab pipeline for Magento has a single responsibility: turning the repository content into a complete, deployable artifact. Complete means that all dependencies are installed, all assets are compiled, the DI code is generated and all static content has been produced. Independent of the server means that the artifact can be activated on any correctly configured target server without build tools such as Composer or Node needing to be installed there.

Many teams start with a build job that does too little, for example running only Composer and leaving the rest to the server. That works at first, but it causes the server state and the artifact to become mixed together. If a different PHP version, a different Composer version or a different Node version is installed on the server, subtle differences appear between staging and production. The build stage therefore has to be fully controlled and reproducible: same input, same output, no matter which GitLab runner executes the job.

The order of the build steps in Magento is not arbitrary. Composer has to run first because DI Compile needs to know the installed vendor classes. DI Compile has to run before static content deploy because the static content deployment process builds on the generated classes. Node assets can run in parallel with Composer, but must be finished before static content deploy if frontend templates reference CSS classes that only exist once the Tailwind build has run.

2. Composer install: reproducible and safe

The Composer install command in the build stage must always run with --no-dev, --prefer-dist, --no-interaction and --optimize-autoloader. --no-dev excludes development dependencies from the build artifact, since those are only needed in the test job. --prefer-dist prefers tar.gz downloads over git clones, which is faster for many packages. --optimize-autoloader generates a flat autoload map that performs faster in production than the regular PSR-4 resolution.

The composer.lock file must be checked into the repository and must never be modified by composer update inside the pipeline. Only composer install based on the lock file guarantees that the build stage installs exactly the same packages in the same versions on every run. The Composer auth token for private packages and Magento repositories must never live in the repository, it has to be stored as a GitLab CI/CD variable and passed in through the COMPOSER_AUTH environment variable or an auth.json file.

# 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 and frontend assets: npm ci instead of npm install

The difference between npm install and npm ci is decisive in a build stage: npm ci always installs exactly the versions pinned in package-lock.json, without modifying the lock file. npm install can update the lock file and thereby produce different versions between local development and the CI build. In the build stage, npm ci is mandatory, it is the Node equivalent of running composer install from the lock file.

The Node cache in GitLab works through the node_modules directory or the npm cache folder. Since npm ci always deletes node_modules before installing, caching the npm cache folder (.npm) makes more sense: it holds downloaded packages and avoids network access when the lock file has not changed. The cache key should include package-lock.json as its basis, so the cache is invalidated automatically whenever the lock file changes.

4. Tailwind CSS v4 in the build stage

Tailwind CSS v4 uses a CSS-first approach, where the configuration lives in a CSS file instead of tailwind.config.js. The build command is npx @tailwindcss/cli build or the npm script defined in the theme directory's package.json. In the build stage, the production build has to run: no watch mode, no sourcemap, no unoptimized CSS.

The output CSS is expected at pub/static/frontend/Mironsoft/default/de_DE/css/styles.css, or distributed there through the static content deploy process. The pipeline has to make sure the CSS is generated before static content deploy runs, so the Magento deployment process picks up the finished CSS file. The Tailwind output directory has to be included in the artifact path.

5. setup:di:compile: why and when

setup:di:compile is one of the most time-consuming and critical steps in the Magento build process. It analyzes every PHP class in the vendor and code directories, generates interceptors for plugins, creates proxy classes for circular dependencies and produces factory classes for all interfaces. The result lives in generated/ and lets Magento run in production mode without runtime code generation.

In the build job, setup:di:compile has to run after composer install and after a minimal app/etc/config.php has been generated, it does not need a full env.php since it requires no database connection. A common failure: DI Compile fails because a plugin references a class that does not exist in vendor, and that failure is welcome in the build job, because otherwise it would only surface in production. The generated/ directory has to be fully included in the artifact and must never be regenerated on the server.

6. Static content deploy in the build job

The setup:static-content:deploy command copies and minifies all static files from the theme, the modules and the vendor packages into the pub/static directory. In the build stage it runs with --force, so it works even without a full database connection. The locales have to be specified explicitly, at least the ones configured in the production environment.

Static content deploy is time-consuming and can be parallelized with --jobs=4. In Magento 2.4.x with Hyva themes, the Hyva-compatible deploy process has to be used. The pub/static directory belongs in the artifact and is not regenerated on the server, which is why it matters that the build job covers every locale that is needed.

# 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 strategy for build jobs in GitLab

GitLab CI cache is a mechanism for persisting directories between pipeline runs and reducing network access. For Magento build jobs, two caches make sense: the Composer cache folder (.cache/composer/) and the npm cache folder (.cache/npm/). Both should carry a cache key that includes the respective lock file as its basis, so a new cache is created whenever the lock file changes while the old one remains available until it expires.

The cache policy pull-push is correct for the build job: the job reads the existing cache and writes an updated cache back after the build. Subsequent jobs (test, package) can set the policy to pull, since they do not modify the cache. The vendor/ folder itself should not live in the cache, it should always be transferred as an artifact, which avoids subtle inconsistencies between the cache and the actual Composer state.

8. Defining artifacts correctly

The artifacts block of the build job defines which files get passed on to subsequent jobs in the pipeline. For Magento, that is at minimum: vendor/, generated/, pub/static/ and app/etc/config.php. The vendor/ folder is large, typically several hundred MB. The exclude field makes it possible to strip out unnecessary files: .git directories inside vendor, markdown files and test folders in vendor packages.

The artifact name field should include the commit SHA and the pipeline ID, so artifacts can be traced back unambiguously to a pipeline. The expire_in field prevents old artifacts from filling up GitLab storage. For build artifacts, 1 day is usually enough, since the deploy job typically runs within the same pipeline run. For package-stage artifacts that need to be kept around for manual rollbacks, 7 days makes more sense.

9. Build approaches compared

How the build stage is defined has a direct impact on deployment reliability and error detection. Running DI Compile on the production server risks interrupting the live shop if a compile error occurs. Running Composer without a lock file risks producing different dependencies between pipelines.

Build step Risky / slow Recommended Benefit
Composer composer update in CI composer install from lock Reproducible, identical dependencies
Node npm install (modifies lock) npm ci from package-lock.json Exactly the same Node packages in every pipeline
DI Compile On the production server In the build job, as an artifact Errors caught before deploy, no risk on prod
Static Content In the deploy job on the server In the build job, transferred as an artifact No PHP CLI access needed on the server, faster deploy
Artifact scope Upload the whole repository Only the generated directories Smaller artifact, faster transfer

The comparison for static content deploy in particular shows the systemic advantage of the build stage: when the deployment server does not need PHP CLI because all static files are already prebuilt, a critical dependency disappears entirely. The target server then becomes a pure delivery server instead of a build server, which is the real strength of the artifact-based deployment approach.

10. Summary

A Magento build stage in GitLab CI consists of four core steps in a defined order: Composer install from the lock file, Node assets via npm ci and the Tailwind build, DI Compile for the generated code, and static content deploy for all configured locales. The result is a complete artifact that does not depend on the server and can be activated on any target server without further build steps.

The most important rules: composer install instead of composer update, npm ci instead of npm install, DI Compile in the build job instead of on the server, and defining artifacts precisely instead of shipping the entire repository. A build job that follows these rules is the foundation of a reliable deployment process, every subsequent stage benefits when this step is clean and reproducible.

Magento Build Stage: The Essentials at a Glance

Composer rule

composer install --no-dev --prefer-dist --optimize-autoloader, always from the lock file, never composer update inside the pipeline.

Node rule

npm ci from package-lock.json, does not modify the lock file and guarantees identical Node packages in every pipeline.

DI Compile

Run in the build job, transfer as an artifact. Compile errors are caught before deploy, not at runtime on production.

Artifact scope

vendor/, generated/, pub/static/ and app/etc/config.php. No .git in vendor, no markdown files, no test code in the deployment artifact.

11. FAQ: Magento Build Stage in GitLab CI

1Why DI Compile in the build job instead of on the server?
DI Compile on production can interrupt the live shop. In the build job an error is caught before the artifact reaches the server, safer and earlier.
2Does DI Compile need a database connection?
No. Only PHP class analysis, no DB needed. A minimal config.php is required, generate it via setup:config:set or a template.
3npm ci vs. npm install in CI?
npm ci reads package-lock.json exactly, without changing it. npm install can modify the lock file, leading to different versions between dev and CI.
4How to keep auth.json out of the artifact?
Delete auth.json in after_script, or set COMPOSER_AUTH as an environment variable, Composer reads it automatically without writing a file.
5How large is the Magento build artifact?
vendor: 300 to 500 MB, pub/static: 100 to 300 MB. With exclude rules for .git and test code: reducible to roughly 200 to 400 MB overall.
6Static content on the server or in the build job?
In the build job. The server then needs no PHP CLI, it is a pure delivery server, not a build system. Faster deploy, fewer server dependencies.
7Configuring the Composer cache correctly in GitLab?
Set COMPOSER_CACHE_DIR to .cache/composer, list the path in cache.paths, cache key equals branch plus composer.lock hash. Build job: pull-push. Subsequent jobs: pull.
8Which PHP extensions does the build job need?
At minimum: zip, intl, mbstring, bcmath, soap, xsl, gd/imagick, pdo_mysql, opcache. Without these, Composer or DI Compile fails.
9How long does a full build job take?
With a warm cache: 5 to 10 min. Without a cache: 15 to 25 min. DI Compile alone: 2 to 5 min. Depends on module count and network.
10Vendor in the artifact or cached server side?
Always in the artifact. A server-side vendor cache mixes server state and artifact, which destroys reproducibility and makes rollbacks riskier.