From Zero to Zero Downtime: The Complete GitLab Workflow for Magento in One Picture
AI generated
CI/CD
.yml
GitLab · Magento Deployment · CI/CD · Zero Downtime
From Zero to Zero Downtime:
The Complete GitLab Workflow for Magento

Repository governance, build artifacts, release directories, symlink switching, verify jobs and rollback as one closed, traceable process, with every building block explained in context.

15 min read GitLab CI/CD · Magento · Release Structure · Symlink · Rollback Magento 2.4 · PHP 8.4 · GitLab 17+

1. Why seeing the whole picture helps so much

Magento deployments rarely fail because of one single missing step. They fail because the individual steps are known but were never thought through as one connected system. One team knows the build job, another knows the release folders, and nobody has ever really rehearsed the rollback script. The complete GitLab workflow for Magento therefore needs to be presented as a whole, not as a collection of isolated configuration snippets.

The whole picture starts with repository governance: which branches are protected? Who is allowed to deploy to production? How are tags assigned for releases? Only once these questions are answered can a pipeline be built on top of them. The pipeline itself is then not an end in itself, but the mechanism that translates a defined approval into a reproducible deployment. Every step, build, package, deploy, verify, rollback, has a clearly defined responsibility. This chapter shows how those responsibilities work together.

The strength of the GitLab workflow for Magento does not lie in complex individual configurations, but in the consistency of the overall process. A deployment that was rehearsed on staging should run on production with the same variables, the same scripts and the same release structure. That is zero downtime not as a technical feature, but as organizational discipline.

2. Repository foundation: branches, tags and protection rules

The first building block of the complete workflow is the repository itself. Without protected branches, every pipeline runs on shaky ground. The main branch must be protected: no direct pushes, all changes go through merge requests, and approval is required from at least one other team member. The release/* branch is used for release candidates whose pipeline must be fully green before the deploy is approved.

Tags follow the v* scheme and are also protected. A production deploy is triggered exclusively by a signed, approved tag, never by a branch push. That prevents unintended commits from reaching production directly. The relationship between branch, tag, pipeline and deployment target must be explicitly documented in the project so new team members can follow it.

3. CI/CD variables as a configuration contract

Variables are the configuration contract of the pipeline. They separate what the code does from where it gets deployed. The most important variables for a Magento GitLab workflow are SSH_PRIVATE_KEY, SSH_KNOWN_HOSTS, DEPLOY_HOST, DEPLOY_USER, DEPLOY_PATH and COMPOSER_AUTH. Each of these variables must carry the correct environment scope, so staging secrets never end up in production jobs and vice versa.

In addition to the deployment secrets there are process variables such as RELEASE_RETENTION, which controls how many old release directories are kept on the server. A value of 5 is sufficient for most projects and allows a quick rollback to the last five releases. Pipeline level variables such as GIT_STRATEGY: fetch and COMPOSER_CACHE_DIR control the behavior of the GitLab runner and Composer caching globally and do not need to be set individually in every job.

# .gitlab-ci.yml: Global pipeline configuration for Magento deployment
stages:
  - build
  - test
  - package
  - deploy
  - verify
  - rollback

variables:
  GIT_STRATEGY: fetch
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
  NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.cache/npm"
  RELEASE_RETENTION: "5"

# Cache Composer and npm dependencies across pipeline runs
cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - .cache/composer/
    - .cache/npm/

4. Pipeline stages: order that carries meaning

The stages of a Magento pipeline are not cosmetic, they are the functional order of the deployment process. Build produces the artifact: Composer dependencies, frontend assets, DI compilation. Test checks the artifact for correctness: PHPStan, PHPCS, unit tests. Package bundles the verified artifact and prepares it for transfer. Deploy transfers the artifact to the server and switches the symlink. Verify checks whether the new release runs correctly. Rollback exists as a manual job for emergencies.

Every stage may only start once the previous one has completed successfully. That is the basic rule of the pipeline cascade. A deploy that happens without a preceding green test run is no longer a controlled deploy. The stages enforce this order mechanically and take the burden of manual discipline off the team. That is the difference between a pipeline as a tool and a pipeline as a guarantee of process.

5. Build stage: artifacts instead of server state

The build job is the most critical step in the entire workflow. It has to resolve all dependencies, run the frontend build and perform Magento's DI compilation step, all inside an isolated environment with no access to the production server. The result is an artifact that contains the complete, reproducible state of the release.

Artifacts are declared in GitLab using the artifacts directive. The paths vendor/, generated/ and pub/static/ are the minimum requirements for a Magento artifact. The artifact's expiry time should be set to at least one day, so the deploy job, which runs in a later stage, still has access to it. A build job that runs on the production server is no longer a build job, it is a maintenance step with uncontrolled side effects.

# Build job: produces the Magento release artifact
build:magento:
  stage: build
  image: php:8.4-cli
  before_script:
    - apt-get update -qq && apt-get install -y -qq git unzip curl nodejs npm
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
  script:
    # Install PHP dependencies without dev packages
    - composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
    # Install and build frontend assets
    - npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind
    - npm run build --prefix app/design/frontend/Mironsoft/default/web/tailwind
    # Compile Magento dependency injection
    - php bin/magento setup:di:compile
    # Deploy static content for English locale
    - php bin/magento setup:static-content:deploy en_US -f --jobs=4
  artifacts:
    name: "magento-release-$CI_COMMIT_SHORT_SHA"
    paths:
      - vendor/
      - generated/
      - pub/static/
      - app/etc/config.php
    expire_in: 2 days
  only:
    - tags
    - main

6. Release structure on the target server

The release structure on the target server is the foundation of zero downtime. It consists of three directories: releases/ holds all versioned releases as complete directories. shared/ holds files that are shared across releases: app/etc/env.php, pub/media/, var/log/, var/session/. current is a symlink pointing to the active release.

The atomic symlink switch is the core of the zero downtime approach. With ln -sfn /releases/NEW_RELEASE /current, the active code changes in a single, atomic system call. Running PHP-FPM processes keep their file handles on the old files, while new requests are served from the new release immediately. This principle only works if the release structure has been set up cleanly in advance and the shared paths are linked correctly. A server without this structure cannot perform a safe symlink switch.

7. Deploy: transfer the artifact, switch the symlink

The deploy job transfers the built artifact to the target server and runs the Magento specific release steps. The transfer uses rsync, which only transfers changed files and so minimizes network traffic. After the transfer, the shared paths are linked, the Magento configuration is pulled in from the shared/ directory, and the symlink is pointed at the new release.

The order of the Magento steps within the deploy is not arbitrary. setup:upgrade must run before cache:flush, since the upgrade invalidates the cache. Maintenance mode should be kept as short as possible, ideally only for the duration of the setup:upgrade call. For deployments without database migrations, maintenance mode can be skipped entirely, which maximizes the zero downtime approach. The symlink switch itself is the only step that runs with absolutely no downtime.

# Deploy job: transfers artifact and switches symlink on production server
deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://shop.mironsoft.de
  script:
    # Setup SSH authentication
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts && chmod 644 ~/.ssh/known_hosts
    # Create release directory and transfer artifact
    - RELEASE_ID=$(date +%Y%m%d-%H%M%S)
    - RELEASE_PATH="$DEPLOY_PATH/releases/$RELEASE_ID"
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p $RELEASE_PATH"
    - rsync -az --delete --exclude='.git' ./ "$DEPLOY_USER@$DEPLOY_HOST:$RELEASE_PATH/"
    # Link shared files and switch symlink
    - |
      ssh "$DEPLOY_USER@$DEPLOY_HOST" bash -s << 'SSH'
      set -euo pipefail
      RELEASE_PATH="$DEPLOY_PATH/releases/$RELEASE_ID"
      # Link environment-specific files from shared directory
      ln -sfn "$DEPLOY_PATH/shared/app/etc/env.php" "$RELEASE_PATH/app/etc/env.php"
      ln -sfn "$DEPLOY_PATH/shared/pub/media" "$RELEASE_PATH/pub/media"
      ln -sfn "$DEPLOY_PATH/shared/var" "$RELEASE_PATH/var"
      # Switch symlink atomically
      ln -sfn "$RELEASE_PATH" "$DEPLOY_PATH/current"
      # Post-deploy Magento steps
      cd "$DEPLOY_PATH/current"
      bin/magento cache:flush
      # Cleanup old releases, keep last 5
      ls -dt "$DEPLOY_PATH/releases"/*/ | tail -n +6 | xargs rm -rf
      SSH
  when: manual
  only:
    - tags

8. Verify: health checks after the switch

A deployment without verification is not a finished pipeline. The verify job checks, after the symlink switch, whether the new release is responding correctly. An HTTP health check against the /health endpoint confirms that the web server is answering. A smoke test against the homepage checks that Magento is not returning a 500 error. A cache status check over SSH makes sure the cache was rebuilt correctly after the flush.

If the verify job fails, the pipeline automatically triggers the rollback path. Rollback is not an unplanned extra step, it is part of the normal pipeline design. It has to be maintained just as carefully as the deploy job itself: tested scripts, a defined order, clear output. A rollback that gets executed for the first time under pressure is not a real rollback. The pipeline forces it to be prepared and tested in advance.

9. Improvised versus reproducible: a direct comparison

The difference between an improvised and a reproducible deployment process shows up in every incident. Improvised processes depend on undocumented assumptions about the server: which PHP version is installed? Which Composer version? Which Node version? A reproducible process answers these questions through the build job itself, which runs in a controlled Docker container with explicitly defined tool versions.

The table below shows the key differences between the two approaches at a glance. It makes clear why investing in a complete workflow, even though it requires more upfront effort, fundamentally improves operational stability.

Aspect Improvised Reproducible (GitLab workflow) Benefit
Build location Directly on production CI container, isolated No side effects on the live system
Rollback Manual, improvised Symlink to old release Seconds instead of hours
Secrets Hardcoded or handled manually GitLab variables with scope Separated by environment, auditable
Verification None or manual Automatic verify job Errors caught immediately
Downtime Uncontrolled, variable Only during DB migrations Plannable maintenance window

In quiet periods, an improvised process often looks exactly like a reproducible one. The difference shows up during an incident: when the wrong commit was deployed, when the backup is missing, when there is no documented rollback path. A complete GitLab workflow for Magento prepares the team for exactly these situations before they happen.

10. Summary

The complete GitLab workflow for Magento, from zero to zero downtime, is not a single feature, it is the result of consistently well thought out individual decisions: protected branches prevent uncontrolled deploys. Build jobs in isolated containers produce reproducible artifacts. Release directories with symlink switching allow atomic cutovers. Verify jobs check the new release immediately after the switch. Rollback scripts are prepared and tested, not improvised.

Zero downtime is the natural result of this process, not its goal. Anyone who implements every building block correctly automatically ends up with a deployment infrastructure that minimizes incidents, enables rollbacks, and gives teams the confidence to act in a controlled way even under pressure.

GitLab Workflow for Magento: The Essentials at a Glance

Repository foundation

Protected branches, protected tags, mandatory merge requests. Without this governance, every pipeline is only half secured.

Build artifact

Composer, DI compilation, frontend build in the CI container, never on production. The artifact is built once and deployed many times.

Symlink switch

Release directories with an atomic symlink switch are the basis of zero downtime and fast rollback.

Verify & rollback

No deployment without an automatic health check afterward. Rollback is not a special case, it is part of the normal process design.

11. FAQ: GitLab Workflow for Magento

1What is the most important step?
The atomic symlink switch: ln -sfn /releases/NEW /current. It is the only step with no downtime and forms the basis for a fast rollback.
2Do I always need maintenance mode?
No. Without database migrations it can be skipped. Maintenance mode only makes sense for setup:upgrade calls with DB changes.
3How many releases should I keep?
Five is a good default. It allows rollback to the last five releases without using too much storage. Define it as the RELEASE_RETENTION variable.
4What belongs in shared/?
app/etc/env.php, pub/media/, var/log/, var/session/. These paths exist once on the server and are linked into every release through a symlink.
5How do I keep staging secrets away from production?
Use environment scopes in GitLab CI/CD variables. Without a scope, the variable applies globally, which is a security risk.
6When does the verify job trigger a rollback?
When the health check does not respond or returns a 5xx. Define the rollback job with when: on_failure so it can run immediately.
7Same workflow for staging and production?
Yes, and it is explicitly recommended. Same process, same scripts, different variables. What works on staging also runs on production.
8How long does a deployment take?
Build 3 to 10 minutes, deploy under a minute (rsync plus symlink), verify 30 to 60 seconds. Total time typically 5 to 15 minutes.
9Most common mistake in GitLab deployments?
No verify job and no rehearsed rollback. The pipeline deploys, but nobody can roll back when something goes wrong. Rollback must be documented and tested.
10Static content in the build or on the server?
In the build job. The generated files from pub/static/ are transferred as part of the artifact. Production needs no Node or build tools.