Static Content Deployment for Magento: Getting It Right in the Pipeline
AI generated
CI/CD
.yml
GitLab · Magento · Hyva · SCD · Tailwind CSS
Static Content Deployment for Magento
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.

12 min read SCD · Hyva Tailwind · pub/static · var/view_preprocessed GitLab CI/CD · Magento 2.4 · Zero Downtime

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.

11. FAQ: Static Content Deployment for Magento in GitLab CI

1Does SCD always need to run?
Yes, for every change to frontend code, templates, or CSS. In production mode Magento does not generate static files on demand. Skipping SCD means deploying with a broken frontend.
2Why does di:compile need to run before SCD?
SCD relies on generated code in generated/. Without it, SCD cannot resolve template dependencies and produces incomplete static files.
3How long does SCD typically take?
1 to 10 minutes depending on the number of modules, themes, and locales. With --jobs $(nproc), SCD runs in parallel. In the CI build job this is not critical for deployment duration on the server.
4Can pub/static be a shared directory?
Technically possible, but not recommended with CI-based SCD. As a shared directory, all releases share the same static content, complicating rollback. Per-release is better.
5Does Tailwind purge remove classes that are actually used?
Yes, if templates are missing during the purge run. All PHP templates must be present in the CI workspace when the Tailwind build runs, so purge works correctly.
6Should var/view_preprocessed go into the artifact?
No. It is an internal SCD intermediate cache, server-dependent and large. Delete it before the SCD run in CI, do not include it in the artifact.
7Which locales should be deployed with SCD?
Only the ones actually used, for example de_DE en_US. Every locale increases SCD runtime and artifact size. Check the locales in use in Magento under Stores > Configuration > Locale Options.
8Does cache:flush need to run after every deployment?
Yes. The full_page cache may contain references to static file paths from the old release. Flushing after the symlink switch is mandatory.
9How does a CDN affect SCD?
Magento generates versioned paths (pub/static/version[hash]/). New static files get new URLs automatically, so the CDN fetches them fresh. Old paths are no longer referenced.
10Should --force be used for SCD in the CI build?
Yes, always use --force in the CI build job, since the workspace may contain leftovers from a previous build. It ensures all files are regenerated.