done right in the pipeline
Running Static Content Deployment on the production server means long maintenance windows, server-dependent build results, and unpredictable runtimes. The right place for SCD is the CI build job, paired with the Hyva Tailwind build, setup:di:compile, and pub/static as a controlled artifact that can be deployed to any server.
Table of Contents
- 1. What Static Content Deployment does in Magento
- 2. Why SCD belongs in the CI build, not on the server
- 3. Correct order: di:compile before SCD
- 4. Building Hyva Theme and Tailwind CSS in GitLab CI
- 5. What belongs in the artifact: pub/static and what does not
- 6. var/view_preprocessed: delete it or not?
- 7. SCD on the server vs. SCD in the CI build compared
- 8. Cache behavior after SCD deployment
- 9. Static content during a rollback
- 10. Summary
- 11. FAQ
1. What Static Content Deployment does in Magento
The command bin/magento setup:static-content:deploy, or SCD for short, generates all the static files Magento needs for the frontend: CSS, JavaScript, fonts, images, Less-compiled stylesheets, and template files, all placed into pub/static. Depending on the number of themes, locales, and modules, the run can take several minutes. While SCD is running, the static files sit in a transitional state, partly old versions and partly new ones, which can lead to broken rendering if users load the page at exactly that moment.
SCD is not an optional operation. Any change to frontend code, theme files, or CSS requires a fresh SCD run. Anyone who skips SCD and hopes Magento will generate the files on demand will find that Magento does not perform dynamic compilation in production mode. This is a common mistake on a first zero downtime attempt: the deploy completes, the symlink switches, but the page shows missing CSS and broken JS bundles because SCD never ran.
The real question is not whether SCD must run, but when and where: in the CI build job that produces the artifact, or on the server during deployment. The answer has far-reaching consequences for deployment duration, reproducibility, and how long the maintenance window has to be.
2. Why SCD belongs in the CI build, not on the server
Running SCD on the production server has three fundamental downsides. First, it significantly extends the maintenance window: if SCD takes three minutes and maintenance mode is active during that time, or the old static files were already removed, that is three minutes of downtime. Second, the result is server-dependent: the PHP version, installed extensions, and Less compiler version all influence the SCD output. On a different server, for example after a migration, the output can look different. Third, SCD on the server cannot simply be repeated without running another full deployment.
In the CI build job, by contrast, SCD runs in a controlled Docker environment with defined PHP and Node versions. The result is reproducible: the same commit with the same Docker image always produces the same static files. The build job can be repeated as often as needed without touching the server. And the pub/static directory becomes part of the artifact that gets promoted from staging to production. SCD runs once, and the result is deployed multiple times.
3. Correct order: di:compile before SCD
setup:di:compile must run before setup:static-content:deploy. The DI compiler generates the generated/ folder, which contains interceptor classes, factory classes, and other code generation artifacts. SCD relies on these generated classes to resolve template files correctly and determine plugin-specific JavaScript configuration. If SCD runs before di:compile, parts of the generated code base are missing, and Static Content Deployment can produce incomplete or broken files.
With Hyva Theme and Tailwind CSS there is a third dependency: the Tailwind build must run before SCD, because Hyva includes the compiled CSS file as a regular theme file and SCD distributes it into pub/static. So the correct order in the build job is: composer install, npm ci and the Tailwind build, then di:compile, then SCD. Skip this order and you end up with a pub/static that references CSS that was never compiled, a broken build that only reveals itself as a problem once it is on the server.
build:magento:
stage: build
image: php:8.4-cli
variables:
COMPOSER_CACHE_DIR: .cache/composer
NPM_CONFIG_CACHE: .cache/npm
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .cache/composer
- .cache/npm
before_script:
- apt-get update -qq && apt-get install -y -qq git unzip nodejs npm libzip-dev
- docker-php-ext-install zip pdo_mysql bcmath intl
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
script:
# Step 1: PHP dependencies
- composer install --no-dev --prefer-dist --no-interaction --quiet
# Step 2: Frontend build, Tailwind must run before SCD
- npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind
- npm run build --prefix app/design/frontend/Mironsoft/default/web/tailwind
# Step 3: DI compile, must run before SCD
- php bin/magento setup:di:compile --quiet
# Step 4: Static content, runs after DI and Tailwind are ready
- rm -rf var/view_preprocessed pub/static/frontend
- php bin/magento setup:static-content:deploy de_DE en_US \
-t Mironsoft/default \
--force \
--jobs $(nproc) \
--quiet
artifacts:
paths:
- vendor/
- generated/
- pub/static/
- app/etc/config.php
exclude:
- pub/static/**/*.map
expire_in: 7 days
only:
- main
- tags
4. Building Hyva Theme and Tailwind CSS in GitLab CI
Hyva Themes rely on Tailwind CSS v4 with a CSS-first approach. The tailwind.config.js and the entry CSS file live under app/design/frontend/Mironsoft/default/web/tailwind/. The build command, typically npm run build, produces the compiled CSS file that Magento then distributes into pub/static. The CI build job needs the correct Node.js version available, ideally a Docker image that includes both PHP 8.4 and Node 20+, or a multi-stage build approach with separate images.
One important detail in the Hyva build: the Tailwind CSS purge step scans all PHP templates for the CSS classes they use. If templates are missing during that purge run, for example because only part of the source code is present in the build context, CSS classes that are actually used get stripped from the final bundle. This leads to missing styles on the page without any build error being raised. The CI build job therefore needs the full repository, including all templates, available in the workspace before the Tailwind build starts.
5. What belongs in the artifact: pub/static and what does not
The build artifact for a Magento deployment with CI-based SCD contains: vendor/, generated/, pub/static/, and app/etc/config.php. What explicitly does not belong in the artifact: app/etc/env.php (holds database-specific credentials and comes from the shared directory), pub/media/ (user-generated content, kept in the shared directory), var/ (logs, sessions, cache, all server-dependent), and var/view_preprocessed/ (an intermediate result of SCD, not meant for transfer).
Source maps (*.map files) can be excluded from the artifact if they are not needed in production. That significantly reduces artifact size with little downside. For debugging purposes, source maps can be stored as a separate artifact with a longer retention period that only gets downloaded when needed. app/etc/config.php contains the list of enabled modules and should be part of the artifact: it is environment-independent and controls which modules Magento considers on startup.
6. var/view_preprocessed: delete it or not?
The var/view_preprocessed directory is an internal cache folder Magento uses as an intermediate store during the SCD process. When SCD runs in the CI build job, var/view_preprocessed on the server should not be carried over from a previous deployment. Its content is specific to the last local SCD run and can cause inconsistencies if it gets mixed with a new pub/static state.
The recommended approach: in the build job, delete var/view_preprocessed before the SCD run (rm -rf var/view_preprocessed) so a clean SCD run takes place. The directory does not go into the artifact. On the target server, var/view_preprocessed lives in the shared directory that is shared between releases. After a release switch, var/view_preprocessed on the server can either be cleared or left as is, since it is only an intermediate cache that Magento repopulates as needed, both options are fine. For maximum cleanliness, clear it after the symlink switch.
7. SCD on the server vs. SCD in the CI build compared
The choice between SCD on the server and SCD in the CI build is one of the most important architecture decisions in a Magento deployment process. It shapes the downtime profile of the deployment, its reproducibility, and how complex a rollback ends up being.
| Criterion | SCD on the server | SCD in the CI build job | Recommendation |
|---|---|---|---|
| Maintenance window | Longer (SCD runtime on the server) | Minimal (only the symlink switch) | CI build |
| Reproducibility | Server-dependent | Controlled CI environment | CI build |
| CPU load on production | High during deploy | None | CI build |
| Rollback of static content | SCD must run again | Old release directory already has its own pub/static | CI build is better for rollback |
| Artifact size | Small (no pub/static) | Larger (pub/static included) | Acceptable tradeoff |
The larger artifact size with CI-based SCD is the only real downside. Depending on the number of themes and locales, pub/static can grow to 50 to 300 MB. That is manageable with external artifact storage and by excluding source maps. The benefits, a minimal maintenance window, a reproducible build, and a simple rollback, far outweigh it.
8. Cache behavior after SCD deployment
After the symlink switch and the deployment of a new pub/static, the Magento cache must be flushed. The full_page cache (whether Varnish or Magento's built-in FPC is used) may contain cached HTML pages that reference file paths from the old static content. Since new static content lives under new hash-based paths, Magento generates versioned paths under pub/static/version[hash]/, cached pages can end up pointing at paths that no longer exist in the new release.
Running cache:flush after the symlink switch is therefore not optional. With Varnish integration, the Varnish cache must also be flushed, either via varnishadm ban.url '.' or through Magento's built-in Varnish invalidation using PURGE requests. Anyone running a CDN (Cloudflare, Fastly) in front of Magento also needs to invalidate the relevant paths after deployment so users do not receive cached static files from the old version.
9. Static content during a rollback
One of the strongest arguments for CI-based SCD is rollback behavior. When every release directory ships its own pub/static as part of the artifact, rolling back to any previous release is trivial: the symlink points to the old release directory, and its pub/static is automatically active. No SCD run needed, no maintenance window for the rollback itself.
If SCD instead runs on the server and pub/static is a shared directory (shared between releases), every new deploy overwrites its content. A rollback then means running SCD again for the old release, which takes minutes and produces inconsistent static files during that time. With CI-based SCD and a per-release pub/static, rollback becomes a millisecond operation: the ln -sfn command. The cache flush afterward takes seconds. That is the level of rollback quality that makes zero downtime possible.
deploy:production:
stage: deploy
before_script:
- eval $(ssh-agent -s)
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" bash -s <<'REMOTE'
set -euo pipefail
readonly RELEASE_ID="$(date +%Y%m%d-%H%M%S)"
readonly RELEASE_PATH="$DEPLOY_PATH/releases/$RELEASE_ID"
readonly SHARED="$DEPLOY_PATH/shared"
# Create new release directory and unpack artifact
mkdir -p "$RELEASE_PATH"
cd "$RELEASE_PATH"
tar -xzf "/tmp/$ARTIFACT_NAME" 2>/dev/null
# Link shared directories: env.php, media, logs, sessions
ln -sfn "$SHARED/app/etc/env.php" app/etc/env.php
rm -rf pub/media var/log var/session
ln -sfn "$SHARED/pub/media" pub/media
ln -sfn "$SHARED/var/log" var/log
ln -sfn "$SHARED/var/session" var/session
# Run schema upgrades if needed
bin/magento setup:upgrade --keep-generated --no-interaction
# Switch symlink atomically, pub/static already in release dir from CI build
ln -sfn "$RELEASE_PATH" "$DEPLOY_PATH/current"
# Flush cache, full_page cache must be cleared after static content switch
bin/magento cache:flush
echo "[OK] Release $RELEASE_ID deployed"
# Cleanup old releases, keep last 5
ls -1dt "$DEPLOY_PATH/releases"/*/ | tail -n +6 | xargs rm -rf
REMOTE
environment:
name: production
only:
- tags
when: manual
10. Summary
Static Content Deployment in Magento belongs in the CI build job, not on the production server. The reasoning is both technical and operational: the build environment is controllable and reproducible, deployment runtime on the server becomes minimal, and rollback becomes a millisecond operation because every release ships its own pub/static. The correct order, composer install, Tailwind build, di:compile, SCD, is not negotiable, because each step depends on the result of the one before it.
Hyva Themes with Tailwind CSS reinforce this argument: the Tailwind build is a Node.js process that has no business running on a PHP server. It belongs in the CI pipeline, where the right Node version is available and the result becomes a stable file that is part of the artifact. Once this process is set up cleanly, deployments become faster, safer, and come with a clear rollback path, regardless of whether the deployment targets staging or production.
Static Content Deployment in GitLab CI, the essentials at a glance
Order
Composer install, Tailwind build, di:compile, then SCD. Each step depends on the one before it, the order cannot be changed.
Artifact content
vendor/, generated/, pub/static/, app/etc/config.php. No env.php, no pub/media, no var/view_preprocessed.
Hyva + Tailwind
npm ci and npm run build before di:compile and SCD. Node.js in the build Docker image, not on the server. Full templates needed for correct purging.
Rollback advantage
Per-release pub/static enables rollback via a symlink switch without rerunning SCD. Flush the cache afterward, done.