Timing, Caches and Generated Code
setup:di:compile is one of the most time consuming steps in the Magento build process, and one of the most frequently misplaced. Run it too early and the full Composer base is missing; run it too late and it delays the deploy. This article shows when and how di:compile should run in a GitLab pipeline.
Table of Contents
- 1. What setup:di:compile actually does
- 2. Timing: when di:compile must run in the pipeline
- 3. Treating generated/ as a build artifact
- 4. Caching strategies for di:compile in GitLab
- 5. A complete build job with di:compile
- 6. Common compile errors and their causes
- 7. generated/ and zero downtime deployment
- 8. Comparison: di:compile in the build vs. on the server
- 9. Typical mistakes in the build process
- 10. Summary
- 11. FAQ
1. What setup:di:compile actually does
setup:di:compile is the Magento command that compiles the dependency injection framework. It analyzes every Magento module, reads its di.xml configuration, resolves dependencies, and generates PHP code that the application uses at runtime. The result is the generated/ directory containing proxy classes, factories, interceptors, and preference mappings. Without this generated code, Magento would either run noticeably slower (because classes would be generated dynamically) or, for certain DI configurations, not start at all.
The process is time consuming: on a typical Magento project with around 150 active modules, setup:di:compile takes between two and five minutes, depending on server performance and PHP version. On PHP 8.4 with JIT it is somewhat faster than on PHP 7.4, but it remains a significant share of the total build time. That is the first reason it needs to be positioned carefully in the CI/CD build process: place it wrong once and it either lengthens the pipeline unnecessarily or, in the worst case, has to run more than once.
A common misconception: setup:di:compile does not need a running database. It only reads PHP code and XML configuration. That makes it ideal for the build job in a CI pipeline that runs inside an isolated Docker container without a database connection. What it does need, however, is a complete vendor/ installation. So it has to run after composer install, but before the artifacts are packaged.
2. Timing: when di:compile must run in the pipeline
The correct order in a Magento build pipeline is unambiguous: first composer install, then setup:di:compile, then the frontend build (Tailwind CSS, NPM), then setup:static-content:deploy (if it runs in the build job), then packaging the artifacts. di:compile has to run after composer install because it needs access to every class in the vendor/ directory. If classes are missing, the compile process fails with a ClassNotFoundException or a broken DI graph.
setup:static-content:deploy can, but does not have to, run after di:compile in the build job. If Static Content Deploy runs in the build job and the pub/static/ directory is packaged as an artifact, that step is skipped in the deploy job entirely. That saves time on the production server but increases the artifact size. Many teams delegate setup:static-content:deploy to the deploy job because the command needs store view specific data that depends on the server. For plain Magento setups without store view specific dependencies in the build context, though, it can reliably run in the build job.
3. Treating generated/ as a build artifact
The generated/ directory is an artifact of the build process: it is generated from source code and does not need to be versioned in Git. In a symlink release structure, the generated/ directory comes out of the build artifact and is placed into the release directory. It must not live in the shared directory, because different releases may have different generated classes. Every release carries its own generated/ directory with it.
In a GitLab pipeline, the generated/ directory is declared in the artifacts block of the build job. GitLab stores it after the build job and makes it available in the deploy job. The size of the artifact is a critical factor: a typical generated/ directory amounts to 50 to 150 MB of PHP files. Combined with vendor/ (200 to 400 MB) and pub/static/ (optional, 100 to 500 MB), artifacts can quickly reach 500 MB to 1 GB. Large artifacts significantly slow down the upload after the build job and the download before the deploy job.
4. Caching strategies for di:compile in GitLab
GitLab caches differ from artifacts: artifacts are passed between jobs within the same pipeline. Caches are reused across different pipeline runs. For di:compile, directly caching the generated/ directory is not recommended, because the generated classes have to match the exact current state of the vendor/ directory and the app code. A stale cache from a different commit can lead to subtle DI errors that are hard to debug.
What does cache well, on the other hand, is the vendor/ directory produced by composer install (cache key: hash of composer.lock). If composer.lock has not changed, the build job can reuse the Composer cache and speed up composer install considerably. That significantly shortens the overall build time for commits without dependency changes, because the time consuming download step is skipped. di:compile itself then runs on the already present vendor/ directory, which can reduce the total time from a typical 8 to 12 minutes down to 3 to 5 minutes.
5. A complete build job with di:compile
The configuration below shows a complete build job for Magento 2.4 on PHP 8.4 that positions setup:di:compile correctly, uses caching for Composer, and provides the generated/ directory as an artifact. The comments explain the reasoning behind the ordering.
# .gitlab-ci.yml: Magento build stage with setup:di:compile
stages:
- build
- test
- package
- deploy
- verify
variables:
GIT_STRATEGY: fetch
COMPOSER_HOME: "/tmp/composer"
COMPOSER_CACHE_DIR: ".cache/composer"
PHP_MEMORY_LIMIT: "2G"
build:magento:
stage: build
image: php:8.4-cli
before_script:
# Install system dependencies for Magento build
- apt-get update -qq && apt-get install -y -qq git unzip libzip-dev libicu-dev
- docker-php-ext-install zip intl bcmath pdo_mysql
# Inject Composer auth credentials, never hardcode in .gitlab-ci.yml
- mkdir -p "$COMPOSER_HOME"
- echo "$COMPOSER_AUTH" > "$COMPOSER_HOME/auth.json"
script:
# Step 1: Install PHP dependencies, must run before di:compile
- composer install --no-dev --prefer-dist --no-interaction --no-progress \
--optimize-autoloader
# Step 2: DI compilation, runs after full vendor/ is available
- php -d memory_limit="${PHP_MEMORY_LIMIT}" bin/magento setup:di:compile
# Step 3: Static content deploy, runs after di:compile (uses generated classes)
- php bin/magento setup:static-content:deploy de_DE en_US \
-t Mironsoft/default --jobs=4 -f
# Step 4: Frontend build (Tailwind CSS)
- npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind
- npm run build --prefix app/design/frontend/Mironsoft/default/web/tailwind
# Remove auth credentials before artifact packaging
- rm -f "$COMPOSER_HOME/auth.json"
after_script:
# Cleanup sensitive data even on failure
- rm -f "/tmp/composer/auth.json"
cache:
# Cache vendor/ keyed by composer.lock hash, reuse on unchanged dependencies
key:
files:
- composer.lock
paths:
- .cache/composer/
policy: pull-push
artifacts:
paths:
- vendor/
- generated/
- pub/static/
expire_in: 4 hours
# Exclude test files from artifact to reduce size
exclude:
- vendor/**/Test/**
- vendor/**/Tests/**
- vendor/**/*.md
tags:
- build
- php84
- docker
Two details in this configuration matter in particular: first, setup:static-content:deploy runs after setup:di:compile, not before. In some Magento versions, Static Content Deploy touches generated classes (for example translation classes coming from generated code structures). Second, --optimize-autoloader is enabled in composer install: it improves autoload performance in production considerably and should always be set in build jobs. The --jobs=4 parameter on static-content:deploy parallelizes asset generation and makes use of the available CPU cores in the build container.
6. Common compile errors and their causes
The most common error with setup:di:compile in CI environments is a memory limit problem. By default, php runs with the standard php.ini value, which is often set to 128 MB or 256 MB. di:compile needs between 512 MB and 2 GB of RAM for large Magento installations with many third party modules. Without an explicit memory limit, the process fails with an Allowed memory size exhausted error. The fix: set php -d memory_limit=2G before the command, or configure it in the Docker image's php.ini.
The second common error is a missing PHP extension in the build container. di:compile analyzes PHP classes across all modules, and many of them require extensions such as intl, bcmath, or soap. If the extension is missing in the container, the compile process fails with a Fatal error: Uncaught Error: Call to undefined function. The fix: install every PHP extension in the build job image that Magento needs at runtime, not just the ones Composer requires for installation.
7. generated/ and zero downtime deployment
In a symlink release structure, the generated/ directory is part of the release directory. During the atomic symlink switch, the current symlink moves from one release directory to the next. The new release directory contains the complete generated/ directory that came from the build artifact. That guarantees the newly active release always has the generated code set that matches its code version, with no manual deletion or regeneration of generated/ on the server required.
One critical point: on the production server, the generated/ directory must not live in the shared directory. If two releases could be active at the same time (which does not happen in an atomic symlink switch, but does occur in other deployment models), different versions of the generated code would overwrite each other. In the symlink model this problem is structurally ruled out: every release directory carries its own generated/ directory, and the active symlink always points to exactly one of them.
8. Comparison: di:compile in the build vs. on the server
There are two approaches to where setup:di:compile runs in the deployment process. The build in CI approach runs it once in the build job and distributes the result via the artifact. The on server approach runs it on every target server, directly before the symlink switch. Both have advantages and drawbacks.
| Aspect | Build in CI (recommended) | On the server |
|---|---|---|
| Reproducibility | One time, consistent across all servers | Depends on the server's PHP version and extensions |
| Deploy duration | Shorter (no compile on the server) | Longer (2 to 5 min compile per server) |
| Error detection | In the build job, before the deploy | Only on the server, too late |
| Artifact size | Larger (generated/ included) | Smaller (no generated/ in the artifact) |
| Multi server deploy | Identical generated/ on every server | Compile runs separately on each server |
For nearly all production Magento setups, the build in CI approach is preferable: errors in the DI configuration are already caught in the build job, before a deploy even starts. On multi server setups (web clusters), the compile process runs exactly once instead of on every server individually, which shortens the deployment window considerably. The only solid reason for on server compilation is when the build container has a substantially different PHP environment than the production server, which is a separate problem and should be fixed by keeping CI and production on a consistent PHP environment.
9. Typical mistakes in the build process
A common mistake is running setup:di:compile without a prior composer install, or with an incomplete vendor installation. When composer install runs with --no-dev, development only dependencies are missing, which is intentional. But if it runs without --no-plugins and a plugin fails, the vendor installation can end up incomplete, which makes di:compile fail with cryptic errors. The fix: always verify that composer install completed cleanly before starting di:compile. The exit code of composer install signals that reliably.
A second mistake is caching the generated/ directory across pipeline runs with a cache key that is too generic. If the cache key does not include the current commit hash or a hash of all modules, a stale generated/ directory can be pulled from the cache that does not match the current code. That can cause Magento on the server to start with an interceptor set that belongs to an outdated class hierarchy, a subtle bug that often only becomes visible during specific user actions.
10. Summary
setup:di:compile belongs in the build job of the GitLab pipeline, after composer install and before the artifacts are packaged. The generated code directory is treated as a build artifact and shipped to the server with the release directory, not manually regenerated on the server. That way, compile errors are caught in the build job before a deploy even starts. Set the memory limit explicitly (php -d memory_limit=2G), install every PHP extension the build needs in the build container, and enable --optimize-autoloader for the Composer installation.
Caching the generated/ directory across pipeline runs is not a good idea, because the directory has to match the exact current commit state. The Composer cache, on the other hand, is worthwhile and significantly shortens build times for commits without dependency changes. With this configuration, setup:di:compile becomes a reliable, reproducible part of the build process, rather than a source of errors on the production server.
setup:di:compile in the Build Pipeline: the essentials at a glance
Order
composer install → di:compile → static-content:deploy → frontend build. Never run di:compile before composer install.
Memory limit
php -d memory_limit=2G bin/magento setup:di:compile. Without an explicit limit, di:compile fails on large setups.
generated/ as an artifact
Declare it in the artifacts block and deploy it with the release directory. Never in the shared directory. Never cached across pipeline runs.
Error detection
DI compile errors in the build job, before the deploy. Never discovered on the server first. Build job fails, no deploy starts.