Why You Should Never Build on Production: Artifacts, Reproducibility and Speed
AI generated
CI/CD
.yml
GitLab · Magento Deployment · Build Artifacts · Reproducibility
Why You Should Never Build on Production:
Artifacts, Reproducibility and Speed

A build job on production is not a deployment, it is an uncontrolled experiment on the live system. Artifacts from isolated CI jobs bring reproducibility, speed and safety at the same time.

12 min read Artifacts · Build Stage · Composer · DI Compile · Cache Magento 2.4 · PHP 8.4 · GitLab CI/CD

1. The Problem: Building on Production as a Hidden Risk

Anyone who runs composer install on the production server mixes two fundamentally different tasks: the build process and the deployment. The build creates dependencies, compiles code and generates assets. The deployment transfers a defined state to the server. When the two collapse into one, a dangerous gray zone appears: the server state during the build is neither fully old nor fully new. Running PHP-FPM processes access files that are being overwritten by Composer at that very moment.

The risk is not just theoretical. A composer install on production typically takes anywhere from 30 seconds to several minutes, depending on the network and the server. During that time, requests can hit a half finished vendor/ directory. Magento responds with fatal errors that do not always show up immediately. A setup:di:compile on production is even riskier: it regenerates all generated code while the application is actively responding to requests. The result is 500 errors, race conditions and hard to debug state failures.

At the core of the problem is a missing separation between the build environment and the production environment. Production should never need Composer, npm or Node installed. If these tools exist on production, that is a sign the build process is not cleanly isolated. The solution is consistent artifact orientation in the GitLab workflow.

2. What a Build Artifact Really Means

A build artifact is the complete, immutable result of a build process. It contains everything needed to run the application: vendor/, generated/, pub/static/ and app/etc/config.php. It explicitly does not contain anything environment specific: app/etc/env.php, database connections, API keys. This separation is the core of the artifact approach.

An artifact is built correctly exactly when it can be created without access to the target server and without any knowledge of the production configuration. In practice this means the build job in GitLab CI runs inside a Docker container with defined PHP and Node versions, installs dependencies from locked versions (composer.lock, package-lock.json) and compiles all generated files. The finished artifact is stored as a GitLab artifact and consumed by the following jobs, test and deploy.

3. Reproducibility: The Same Build, Every Time

Reproducibility means the same commit produces the same artifact today and in six months. That requires the build job to be deterministic: a fixed PHP version, a fixed Composer version, a committed composer.lock, a committed package-lock.json. If the build runs on an unversioned server where PHP patches get applied and Composer versions silently change, reproducibility is out of reach.

In practice, reproducibility shows up during debugging. When a production issue needs to be traced back to a specific release, the exact artifact of that release must be available or reproducible. With a cleanly isolated build job, the artifact can be regenerated from the commit at any time. With a build on production, the artifact is the result of whatever the server state happened to be at build time, and that state is neither versioned nor reproducible.

# Reproducible Magento build job, pinned tools, locked dependencies
build:magento:
  stage: build
  # Pinned Docker image ensures same PHP version across all builds
  image: php:8.4-cli-alpine
  variables:
    COMPOSER_VERSION: "2.7"
  before_script:
    - apk add --no-cache git unzip nodejs npm bash
    # Install pinned Composer version for reproducibility
    - curl -sS https://getcomposer.org/installer | php -- \
        --install-dir=/usr/local/bin \
        --filename=composer \
        --version=$COMPOSER_VERSION
    - composer --version
  script:
    # composer.lock must be committed, ensures same dependency versions
    - composer install --no-dev --prefer-dist --no-interaction \
        --optimize-autoloader --no-scripts
    - composer run-script post-install-cmd --no-interaction
    # package-lock.json must be committed, ensures same npm versions
    - npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind
    - npm run build --prefix app/design/frontend/Mironsoft/default/web/tailwind
    # Generate DI and static content in CI, not on production
    - php bin/magento setup:di:compile
    - php bin/magento setup:static-content:deploy en_US -f --jobs=4
  artifacts:
    name: "magento-$CI_COMMIT_SHORT_SHA"
    paths:
      - vendor/
      - generated/
      - pub/static/
      - app/etc/config.php
    exclude:
      - vendor/**/.git
      - vendor/**/test/**
      - vendor/**/tests/**
    expire_in: 3 days

4. Speed: Why Artifacts Are Faster

A common misconception is that building on production is faster because the server is already there. The opposite is true. A build on production competes with running PHP processes for CPU and I/O. Composer downloads depend on the server's own network connection. The DI compile step blocks code generation for minutes while live requests need that very code.

A GitLab build job runs in parallel with other pipeline jobs, uses dedicated CI resources and benefits from Composer caching between runs. If composer.lock has not changed, the build job does not download a single dependency, everything is already cached. The artifact is built once and can be deployed to as many servers as needed without repeating the build process. This difference is especially clear in multi server setups: building on three production servers costs three times the build time, while an artifact is built once and deployed three times.

5. Anatomy of a Magento Artifact in GitLab

The Magento artifact consists of four core components. vendor/ contains all PHP dependencies after composer install. generated/ contains the dependency injection generated code from setup:di:compile. pub/static/ contains the deployed static assets of all active themes. app/etc/config.php contains the module activation list, which should be generated by setup:upgrade and committed.

What does not belong in the artifact: app/etc/env.php with database connections and secrets, pub/media/ with uploaded media, var/ with logs and sessions. These files live in the shared/ directory on the server and are symlinked into every release. The sharp separation between what lives in the artifact and what lives on the server is the prerequisite for a safe symlink switch and for reproducible deployments.

6. Controlling Composer Cache and Artifact Size

Without cache configuration, every build job downloads all Composer packages again. That costs time and bandwidth. GitLab pipeline caches solve this problem: the .cache/composer/ folder is cached between builds, so only changed packages get downloaded again. The cache key should be tied to $CI_COMMIT_REF_SLUG or the hash of composer.lock, so cache incompatibilities after dependency updates are avoided.

Artifact size is another point to control. An uncompressed vendor/ directory can reach 500 MB or more on large Magento installations. The exclude directive in the artifact configuration removes test directories and .git subfolders from packages, which typically reduces the size by 20 to 30 percent. The expire_in setting controls how long the artifact stays stored in GitLab; three days is sufficient for most workflows.

# Composer and npm cache configuration for faster builds
variables:
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
  NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.cache/npm"

cache:
  # Separate cache per branch to avoid cross-branch conflicts
  key:
    files:
      - composer.lock
      - app/design/frontend/Mironsoft/default/web/tailwind/package-lock.json
  paths:
    - .cache/composer/
    - .cache/npm/
  policy: pull-push

# Artifact size reduction: exclude test directories and git metadata
build:magento:
  artifacts:
    paths:
      - vendor/
      - generated/
      - pub/static/
      - app/etc/config.php
    exclude:
      # Remove vendor test suites, not needed in production
      - "vendor/**/Test/**"
      - "vendor/**/Tests/**"
      - "vendor/**/.git/**"
      - "vendor/**/docs/**"
    expire_in: 3 days
    when: on_success

7. A Deploy Job Without Build Tools on the Server

The goal is a production server with no Composer, no npm and no Node installed. The deploy job only needs SSH access, rsync for transferring files and the Magento CLI (bin/magento) for deploy specific steps such as cache:flush. The build job has already prepared everything; the deploy job only transfers and links.

This approach carries an important security benefit: a server without build tools is harder to attack than a server with developer tools installed. An attacker who gains access to a server with Composer can cause far more damage than on a server that only runs PHP-FPM and Nginx. Separating build from deploy is therefore not just a matter of reproducibility, it is also a security measure.

8. Common Mistakes With the Artifact Approach

The most common mistake when switching to the artifact approach is forgetting app/etc/config.php in the artifact. This file is generated by setup:upgrade and contains the list of enabled modules. Without it, Magento cannot start after the deploy. A second typical mistake: app/etc/env.php accidentally ends up in the artifact and gets deployed to every environment, including production database connections that have no business being on staging.

A third mistake concerns the expire_in setting. If the artifact expires before the deploy job consumes it, the deploy fails with a cryptic "artifact not found" error. The expiry time must be chosen so the artifact is still available even with manual approvals and waiting periods. For workflows with a manual production release, three to five days is a realistic value.

9. Building on Production vs. the Artifact Approach Compared

Comparing building on production with the artifact approach shows there is no gray area. Building on production is not an acceptable compromise in professional Magento deployments, it is an anti-pattern that can be fully replaced by the artifact approach.

Criterion Building on Production Artifact Approach (GitLab CI) Advantage
Downtime Risk High (race conditions) Minimal (atomic switch) Stable response times during deploy
Reproducibility Depends on server state Fully deterministic Same commit equals same build
Build Duration Variable, burdens the server Cached, isolated No impact on production load
Server Requirements Composer, npm, Node required Only PHP-FPM + Nginx Smaller attack surface
Multi Server Deploy Build times number of servers Build once, deploy N times Linearly scalable

The most important takeaway from this comparison: the artifact approach is not more complex than building on production, it is cleaner. Setting up the build job and artifact configuration initially costs a few hours. Over the long run it saves hours per deploy and prevents incidents that occur regularly when building on production.

10. Summary

The core argument against building on production is clear: it mixes two separate processes, creates race conditions, is not reproducible and requires build tools on the production server. The artifact approach with GitLab CI cleanly separates build and deploy: the build runs once in an isolated container with defined versions, the artifact is stored in GitLab, and the deploy job transfers the artifact atomically to the server.

For Magento this separation is particularly important because the DI compile step and the static content generation are time consuming and have uncontrolled side effects on production. A cleanly built GitLab artifact contains all generated files and only needs to be deployed and activated via symlink on production. That is the difference between a deployment process that happens to work and one that is reproducibly reliable.

Build Artifacts for Magento: The Essentials at a Glance

Artifact Contents

vendor/, generated/, pub/static/, app/etc/config.php. Not env.php, not pub/media/, not var/.

Reproducibility

Pinned Docker image, composer.lock and package-lock.json committed, deterministic build with no server influence.

Speed

Composer cache between builds, DI compile done once in CI, no build overhead on production.

Security

No Composer, npm or Node on production. Smaller attack surface, clean separation of build and runtime environments.

11. FAQ: Why You Should Never Build on Production

1Biggest risk of building on production?
Race conditions: requests hit a half finished vendor/ directory while the Composer install is still running. Leads to fatal errors that do not show up immediately.
2Is Composer needed on production?
No. The artifact contains all dependencies. Composer only runs in the CI build job, not on production.
3What about app/etc/env.php?
Does not belong in the artifact. Lives in the shared/ directory on the server, symlinked into place. This keeps secrets environment specific.
4How large does a Magento artifact get?
300 to 800 MB uncompressed. With exclude directives for test directories and .git folders, a 20 to 30 percent reduction is possible.
5How long to set expire_in?
At least as long as the time that can pass between build and deploy. For manual approvals, at least 3 days.
6Same artifact to multiple servers?
Yes. Built once, deployed as often as needed. No repetition of the build process for each server.
7Composer cache vs. artifact, what is the difference?
Cache speeds up future builds. Artifact is the result of the build job and gets consumed by the deploy job. Both are independent of each other.
8Does setup:di:compile always run in the build job?
Yes. Without DI compile, requests are significantly slower. DI compile always belongs in the artifact, never on production.
9Exclude env.php from the artifact?
Add env.php to .gitignore and never commit it. In the artifacts configuration it can also be explicitly excluded via the exclude directive.
10Minimum artifact for Magento?
vendor/, generated/, pub/static/ and app/etc/config.php. Without any one of these directories, Magento does not start or runs with degraded performance.