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.
Table of Contents
- 1. What the build stage has to deliver
- 2. Composer install: reproducible and safe
- 3. Node and frontend assets: npm ci instead of npm install
- 4. Tailwind CSS v4 in the build stage
- 5. setup:di:compile: why and when
- 6. Static content deploy in the build job
- 7. Cache strategy for build jobs in GitLab
- 8. Defining artifacts correctly
- 9. Build approaches compared
- 10. Summary
- 11. FAQ
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.