What Belongs in the Release Build, and What Doesn't
Anyone who packs directories into GitLab artifacts without a plan ends up with bloated packages, non reproducible deployments, and unnecessary server risk. Clean separation between build output, shared data, and server state is the core of a resilient release process for Magento.
Table of Contents
- 1. What a GitLab Artifact Really Is
- 2. Build Output vs. Server State: The Core Divide
- 3. Magento Directories Checked Against the Artifact Rule
- 4. vendor/ and generated/: Build or Deploy?
- 5. pub/static and Frontend Assets in the Build
- 6. Shared Data: What Never Belongs in the Artifact
- 7. Configuring artifacts in .gitlab-ci.yml Correctly
- 8. Controlling Package Size: expire_in and exclude
- 9. Comparison: Bloated vs. Cleanly Separated
- 10. Summary
- 11. FAQ
1. What a GitLab Artifact Really Is
A GitLab artifact is not a backup and not a server copy, it is the defined output package of a CI job that gets passed on to subsequent jobs in the pipeline or on to deployment. GitLab stores artifacts in the CI system and makes them available for download on demand. That sounds technically simple, but it has far reaching consequences for the entire deployment process: whatever ends up in the artifact must be reproducible from the source code. Whatever cannot be produced reproducibly does not belong in the package.
The decisive idea is the separation between build output and operational state. Build output arises from the repository through defined commands: Composer, npm, di:compile, SCD. Operational state, on the other hand, is the result of the running system: logs, sessions, uploaded files, configuration with real credentials. Anyone who mixes these two categories ends up with artifacts that are neither secure nor consistent. For Magento projects this separation is especially important because the framework has many layers that look like build output at first glance but are actually server specific.
2. Build Output vs. Server State: The Core Divide
The basic rule is: everything that can be deterministically generated from the repository is potential build output. Everything that is environment specific or arises at runtime is server state and has no business in the artifact. For Magento that concretely means: vendor/ is the result of composer install with a fixed composer.lock, deterministic, hence build output. The file app/etc/env.php, on the other hand, contains database passwords and differs per environment, that is server state, never goes into the artifact.
This separation has a direct effect on rollback capability. An artifact that contains no server specific data can be rolled back to an earlier release without endangering credentials or user data. An artifact that includes env.php carries production data through the CI system, a significant security risk. The clean dividing line is therefore not an academic question but has direct security and operational relevance for every Magento store.
3. Magento Directories Checked Against the Artifact Rule
Magento has a complex directory structure, and each directory has to be evaluated individually. vendor/ contains all Composer dependencies and is clearly build output. generated/ contains the compiled DI code and is likewise build output, provided setup:di:compile runs in the build job. pub/static/ contains the deployed frontend assets after setup:static-content:deploy and is build output. These three directories are the core components of a Magento release artifact.
On the other side are directories that must remain on the server: pub/media/ contains uploaded product images and customer photos. var/log/, var/session/, and var/cache/ are operational data. app/etc/env.php and app/etc/config.php carry environment specific configuration. These directories are linked in through the shared concept of the release model, symlinks from the release directory to shared paths outside the release structure.
4. vendor/ and generated/: Build or Deploy?
Whether vendor/ should be shipped in the artifact or reinstalled on the server is a question that comes up often in Magento teams. The clearer answer: build it in the CI system and deploy it as an artifact. That avoids having to run Composer on the production server, which brings network dependencies, auth.json secrets, and differing Composer versions into play. A vendor/ directory built once in the pipeline is reproducible and safe, the production server does not need Composer at all.
generated/ follows the same logic. bin/magento setup:di:compile runs in the build job, the result is packed into the artifact. That way no compile process runs on the production server. This is especially relevant because di:compile is CPU intensive and would have a significant impact on a web server under load. Together, both directories make the artifact complete: it contains the entire PHP code, all dependencies, and the compiled DI container.
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 sockets
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
script:
# Install PHP dependencies, no dev packages, locked versions
- composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
# Compile Dependency Injection container
- php bin/magento setup:di:compile
artifacts:
name: "magento-build-${CI_COMMIT_SHORT_SHA}"
paths:
- vendor/
- generated/
expire_in: 2 days
when: on_success
5. pub/static and Frontend Assets in the Build
The directory pub/static/ is produced by bin/magento setup:static-content:deploy and contains all static frontend files: CSS, JavaScript, images, fonts, Hyva Tailwind output. It is clearly build output and belongs in the artifact, but with one important caveat: setup:static-content:deploy needs a full Magento installation including database access to read theme configuration. That makes this step difficult in an isolated CI container.
The pragmatic approach for Magento teams without database access in the build container: process Tailwind and node_modules in the build job, but run setup:static-content:deploy as the first step of the deploy job on the server, before the symlink is switched. In that case pub/static/ does not belong in the CI artifact at all, it is generated server side instead. That decision has to be made explicitly and documented by the team, the most common mistake is an implicit assumption that works on staging but fails on production under different paths.
6. Shared Data: What Never Belongs in the Artifact
Shared data are files and directories that remain unchanged across multiple releases and are not regenerated with every deployment. In Magento those are primarily: app/etc/env.php with the database connection and cache configuration, pub/media/ with all uploaded media files, var/log/ for application logs, and var/session/ for active user sessions. This data must never end up in the artifact.
Technically this is implemented through a shared/ directory outside the release structure and symlinks from every release directory to these shared paths. When a new release is deployed, the symlinks immediately point to the correct shared data, without anything needing to be copied. The artifact build stays clean: it contains exclusively versioned code and deterministically generated output, no credentials, no user data, no environment specific settings.
7. Configuring artifacts in .gitlab-ci.yml Correctly
The artifacts directive in GitLab CI has more options than most teams use. The paths array defines which directories and files are stored. expire_in sets when the artifact is automatically deleted, important for storage cost and data protection. The keyword when: on_success ensures artifacts are only stored when the job succeeds. With exclude, individual subdirectories can be excluded from paths, which is useful for test dependencies inside vendor/.
An often overlooked point: artifacts between jobs are downloaded automatically whenever dependencies is set or no explicit dependencies: [] is present. That can slow down pipelines if large artifacts get loaded into many jobs unnecessarily. For Magento it is recommended to point the deploy job explicitly only at the build artifacts and configure all other jobs with dependencies: [] to avoid unnecessary downloads.
package:release:
stage: package
# Only download artifacts from the build job
dependencies:
- build:magento
- build:frontend
script:
# Create a timestamped release package
- export RELEASE_ID="$(date +%Y%m%d-%H%M%S)-${CI_COMMIT_SHORT_SHA}"
- mkdir -p "dist/${RELEASE_ID}"
# Copy build outputs into release directory
- rsync -a --exclude='.git' --exclude='var/' --exclude='pub/media/' ./ "dist/${RELEASE_ID}/"
- tar -czf "release-${RELEASE_ID}.tar.gz" -C dist "${RELEASE_ID}"
artifacts:
name: "release-${CI_COMMIT_SHORT_SHA}"
paths:
- release-*.tar.gz
expire_in: 7 days
when: on_success
# Quality check job that does NOT need build artifacts
test:phpcs:
stage: test
dependencies: []
script:
- vendor/bin/phpcs --standard=Magento2 app/code/
8. Controlling Package Size: expire_in and exclude
Uncontrolled artifact sizes are a common problem in Magento pipelines. vendor/ can be 200 to 400 MB in size, and if this artifact is retained for 30 days in every pipeline run, that quickly adds up to gigabytes of GitLab storage. The solution is a short expire_in time for build artifacts, one to two days is sufficient for most workflows, since release packages can be stored separately with a longer retention period.
The exclude option lets you keep known large subdirectories out of the artifacts. For vendor/ it is worth excluding test directories inside the packages: vendor/*/*/Test/, vendor/*/*/tests/, and similar patterns. For Tailwind and npm, node_modules/ is almost always excluded entirely, the npm build output in the form of CSS files is the only thing that belongs in the artifact, not the entire node_modules directory with hundreds of megabytes.
9. Comparison: Bloated vs. Cleanly Separated
The difference between a poorly and a well configured artifact shows up directly in pipeline performance and deployment security.
| Directory | In the artifact? | Reasoning | Where instead? |
|---|---|---|---|
vendor/ |
Yes | Deterministic from composer.lock | N/A |
generated/ |
Yes | DI compile output from the build job | N/A |
pub/static/ |
Depends on context | Needs DB access for SCD | First step in the deploy job |
app/etc/env.php |
Never | Credentials, environment specific | shared/ on the server |
pub/media/ |
Never | User data, not versioned | shared/ on the server |
node_modules/ |
Never | Hundreds of MB, only needed for the build | GitLab Cache |
The table shows the decision logic: deterministically producible from source code and needed for production means artifact. Environment specific or user data means a shared directory on the server. Only needed for the build process means GitLab Cache, not artifact. These three categories cover every Magento directory and give a clear answer for every individual case.
10. Summary
Deciding what belongs in a GitLab release artifact and what does not is one of the most fundamental design decisions in the deployment process. For Magento the rule is: vendor/ and generated/ are produced in the build job and passed on as an artifact. pub/static/ ideally belongs in the build too, if database access is available in CI, otherwise it is the first deployment step on the server. Everything environment specific and all user data stay in the shared area of the server.
The consequence of this clean separation is a deployment process that is reproducible, rollback capable, and secure. Reproducible, because the artifact always arises from the same inputs. Rollback capable, because an earlier release directory can be reactivated without touching credentials. Secure, because production data never flows through the CI system. Once a team has consistently introduced this separation, they will expect it as a matter of course in every future Magento project.
GitLab Artifacts for Magento: The Essentials at a Glance
Build output goes into the artifact
vendor/ and generated/ are deterministic build output and belong in the artifact. No Composer on production.
Server state stays on the server
env.php, pub/media/, and var/ are shared data and are never packed into the artifact.
Control package size
Keep expire_in short, exclude node_modules/, filter out test directories inside vendor/ with exclude.
dependencies: [] in non deploy jobs
Configure jobs that don't need an artifact with dependencies: [], it saves pipeline time and avoids unnecessary downloads.