The Definitive Magento Zero Downtime Guide with GitLab: From Your First Runner to a Safe Rollback
AI generated
CI/CD
.yml
GitLab · Magento · Zero Downtime · Runner · Rollback
Magento Zero Downtime with GitLab:
from your first runner to a safe rollback

This guide covers the complete path: setting up a GitLab Runner, building the pipeline step by step, creating the release structure on the server, implementing atomic symlink switching, configuring verify jobs and reliably rolling back in seconds. Zero downtime is the result of these concrete decisions.

25 min read Runner · Pipeline · Release · Symlink · Verify · Rollback Magento 2.4 · GitLab 17+ · PHP 8.4 · Docker

1. What zero downtime really means for Magento

In deployment discussions the term zero downtime is often stated as an absolute goal: a deployment where not a single request fails. In practice, zero downtime for Magento means something more nuanced. The time during which the shop is unreachable for users is reduced to zero or close to zero. That is achievable with the right process, but it requires explicit design decisions at every step of the deployment.

What zero downtime concretely enables: switching the file state to a new release without restarting the web server. An atomic symlink switch takes milliseconds. Requests that arrive during the switch are served either by the old release or already by the new one, so no request fails because of the switch itself. What limits zero downtime: database migrations with exclusive table locks, schema changes that are not backward compatible, and Magento caches that must be explicitly cleared after the switch. Strategies exist for these cases, such as expand/contract migrations and short maintenance windows scoped to the database alone, but there is no universal solution.

This guide builds the complete process that makes zero downtime possible for the vast majority of Magento deployments. It starts with the GitLab Runner and ends with a well practiced rollback, because together they form a resilient process that still works under time pressure.

2. The overall architecture at a glance

The deployment process consists of five layers that build on each other. The first layer is the GitLab repository with protected branches, protected tags and fully configured CI/CD variables. The second layer is the GitLab Runner that executes pipelines, produces build artifacts and starts deploy jobs. The third layer is the CI pipeline with build, test, package, deploy, verify and rollback stages. The fourth layer is the server side release structure with releases/, shared/ and the current symlink. The fifth layer covers the Magento specific steps (shared symlinks, setup:upgrade, cache:flush) that run in the correct order.

Each of these layers can be improved and swapped out independently without breaking the others. A team deploying with shell scripts today can move layer three to Deployer without touching layers one, two or four. That keeps the process maintainable long term and extendable for the whole team.

3. Setting up the GitLab Runner

Before a pipeline can run, a runner has to be in place. For Magento, a dedicated runner with a Docker executor on its own Linux host is recommended. Installing it through the official GitLab package repository and then registering it with Docker as the executor gives you control over the build environment and security that shared runners cannot offer. It is important that the build runner and the deploy runner are separate instances: the deploy runner has SSH access to the production server and needs to be secured accordingly more strictly.

Tags control which job lands on which runner. A job tagged tags: [magento-build] only runs on build runners, while a job tagged tags: [magento-deploy] only runs on deploy runners. Setting run-untagged = false on the deploy runner prevents unexpected jobs from landing on a system that has SSH access to production servers.

# Complete .gitlab-ci.yml for Magento Zero-Downtime Deployment
# Covers all stages: build, test, package, deploy, verify, rollback

stages:
  - build
  - test
  - package
  - deploy
  - verify
  - rollback

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

# Reusable SSH setup anchor
.ssh-setup: &ssh-setup
  - apt-get update -qq && apt-get install -y -qq openssh-client rsync
  - eval $(ssh-agent -s)
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  - mkdir -p ~/.ssh
  - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
  - chmod 600 ~/.ssh/known_hosts

build:magento:
  stage: build
  image: php:8.4-cli
  tags: [magento-build]
  cache:
    key: "composer-${CI_COMMIT_REF_SLUG}"
    paths: [".cache/composer/"]
  script:
    - apt-get update -qq && apt-get install -y -qq git unzip nodejs npm
    - composer install --no-dev --prefer-dist --no-interaction --quiet
    - npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind --silent
    - npm run build --prefix app/design/frontend/Mironsoft/default/web/tailwind --silent
    - php bin/magento setup:di:compile
  artifacts:
    paths: [vendor/, generated/, pub/static/]
    expire_in: 4 hours

4. Pipeline structure and stages

The pipeline stages follow a clear logic: each stage has a single responsibility and passes its result on to the next stage as an artifact or a state. build produces the deployable artifact. test runs the quality gates (PHPStan, PHPUnit, PHPCs). package assembles the final deployment package if it differs from the build artifact. deploy transfers the package and switches the symlink. verify confirms that the new state works. rollback restores the previous state if verify fails.

The order is not negotiable: deploy must never run before build and test have succeeded. Verify must run immediately after deploy. Rollback must exist as a pipeline job, not as a manual emergency procedure. Following this structure gives you a process that is predictable and understandable across the whole team.

5. Build stage: producing a reproducible artifact

The artifact is the core of the zero downtime approach: it is built once, tested on staging and deployed to production with no difference between them. There is no "it worked on staging, but production has a build environment problem". The artifact contains vendor/ (without dev dependencies), generated/ (DI compilation), the compiled Tailwind CSS assets and every other static file needed to run the shop.

What does not belong in the artifact: app/etc/env.php (lives in shared/), pub/media/ (shared content), var/ (runtime data), .git/ and all test and dev tools. A lean artifact transfers faster and makes the boundary between build output and server state explicit. Artifact size should be monitored regularly, since sudden jumps in size point to files that were accidentally included.

6. Server preparation: creating the release structure

The release structure on the target server is a prerequisite for every deployment and needs to be prepared once. The directory layout is simple and consistent: /var/www/magento/releases/ holds individual release states, /var/www/magento/shared/ holds persistent data, and /var/www/magento/current is a symlink pointing to the active release. The web server (Nginx or Apache) points its document root at /var/www/magento/current/pub.

Inside the shared/ directory, all persistent files and directories already exist before the first deployment ever runs: app/etc/env.php with the production configuration, pub/media/ with the current media state, and var/log/ and var/session/ for runtime data. This content is never overwritten, it is symlinked into every release instead. The first deployment job must find this structure in place, otherwise it fails.

# deploy:production, atomically switches the current symlink
deploy:production:
  stage: deploy
  image: debian:bookworm-slim
  tags: [magento-deploy]
  when: manual
  only:
    - tags
  environment:
    name: production
    url: https://shop.example.com
  before_script:
    - *ssh-setup
  script:
    - RELEASE=$(date +%Y%m%d-%H%M%S)
    - echo "Deploying release $RELEASE"
    # Transfer build artifact to new release directory
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p $DEPLOY_PATH/releases/$RELEASE"
    - rsync -az --delete
        --exclude='.git'
        --exclude='var/cache'
        ./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/releases/$RELEASE/"
    # Execute Magento release steps on server
    - |
      ssh "$DEPLOY_USER@$DEPLOY_HOST" bash << 'ENDSSH'
        set -euo pipefail
        DEPLOY_PATH="${DEPLOY_PATH}"
        RELEASE="${RELEASE}"
        RELEASE_PATH="$DEPLOY_PATH/releases/$RELEASE"

        # Link shared files and directories
        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/log"           "$RELEASE_PATH/var/log"
        ln -sfn "$DEPLOY_PATH/shared/var/session"       "$RELEASE_PATH/var/session"

        # Run setup:upgrade only if there are pending migrations
        cd "$RELEASE_PATH"
        php bin/magento setup:upgrade --keep-generated

        # Switch symlink atomically, the zero downtime moment
        ln -sfn "$RELEASE_PATH" "$DEPLOY_PATH/current"

        # Flush cache after switch
        php bin/magento cache:flush

        # Cleanup old releases, keep RELEASE_RETENTION most recent
        ls -1dt "$DEPLOY_PATH/releases/"* \
          | tail -n +$((RELEASE_RETENTION + 1)) \
          | xargs rm -rf
        echo "[OK] Deployed and switched to $RELEASE"
      ENDSSH

7. Deploy stage: symlink switching and Magento steps

The deploy job transfers the artifact to the server, runs the Magento specific steps and switches the current symlink. The order is exact: shared symlinks are set before any Magento command runs, because setup:upgrade depends on app/etc/env.php. Setup upgrade runs with --keep-generated so the generated/ configuration produced during the build is preserved. Only then does the symlink switch and the cache get flushed.

The release retention cleanup at the end of the deploy job removes old releases and keeps disk usage under control. The value should be at least five so that several rollback steps remain possible. The deploy job is set to when: manual so no automatic deployment ever reaches production. The team decides explicitly when a new state goes live.

8. Verify stage: confirming the deployment

The verify job is the last quality gate after the deployment. It automatically checks whether the new release is reachable and functioning. Minimum requirements: an HTTP 200 response on the health endpoint, HTTP 200 on the homepage, and a Magento cache status with no critical errors. These three checks cover the most common deployment failures: a web server that did not start, missing configuration, and a corrupted cache state.

The verify job runs with when: on_success directly after the deploy job and is not an optional extra. If verify fails, it signals to the pipeline that something went wrong and gives the team the chance to trigger the rollback job immediately. The window between a deployment failure and restoring the previous state is therefore seconds to a few minutes, not hours.

# verify:production, confirms new release is healthy
verify:production:
  stage: verify
  image: alpine:3.19
  tags: [magento-build]
  when: on_success
  needs: ["deploy:production"]
  before_script:
    - apk add --no-cache curl openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
  script:
    # Health endpoint must return HTTP 200
    - |
      for i in 1 2 3; do
        STATUS=$(curl --silent --output /dev/null --write-out "%{http_code}" \
          --max-time 10 "https://shop.example.com/health")
        if [ "$STATUS" = "200" ]; then
          echo "[OK] Health check passed (attempt $i)"
          break
        fi
        echo "[WARN] Health check returned $STATUS (attempt $i/3)"
        sleep 5
      done
      [ "$STATUS" = "200" ] || { echo "[FAIL] Health check failed"; exit 1; }
    # Homepage must respond
    - curl --fail --silent --output /dev/null --max-time 20
        "https://shop.example.com/"
    # Magento cache and module status via SSH
    - ssh "$DEPLOY_USER@$DEPLOY_HOST"
        "cd $DEPLOY_PATH/current && php bin/magento cache:status"
  allow_failure: false

9. Rollback stage: rolling back safely

The rollback job is the most important job and the one run least often, which is exactly why it must be tested most thoroughly before it is ever actually needed. It switches the current symlink back to the previous release state. The web server then answers again with the previous Magento state. Flushing the cache ensures that no stale cache entries from the new, faulty release remain visible.

Rollback has limits: database migrations that ran with the new release are not undone. If the migrations of the new release are not backward compatible, the database needs to be reset separately, a process that must be documented and practiced apart from the normal rollback routine. For teams using expand/contract migrations, this special case is rare. For everyone else, it is the topic that should dominate rollback planning.

After every rollback, the verify job must run again to confirm that the previous state actually works again. Rolling back to a state that itself had a problem is no progress at all. Whenever a rollback happens, the team should immediately investigate the root cause in the failed release and use the staging pipeline before attempting a new deployment to production.

Mironsoft

Magento zero downtime, GitLab CI/CD and deployment infrastructure

Want to build zero downtime deployment for Magento with GitLab?

We build the complete zero downtime process: setting up a GitLab Runner, structuring the pipeline, implementing the release structure, configuring verify jobs, and documenting and practicing the rollback path.

Runner and pipeline

Set up a self hosted runner, structure the pipeline stages and define the artifacts

Release architecture

Implement the server structure, shared paths, symlink switching and release rotation

Verify and rollback

Configure automatic verify jobs and document and practice the rollback process

10. Summary

Magento zero downtime deployments with GitLab are the result of design decisions applied consistently at every level of the process. A dedicated GitLab Runner with a Docker executor gives you control over the build environment and security. A clearly structured pipeline with build, test, deploy, verify and rollback stages makes the process understandable and workable for the whole team. A reproducible artifact from the build stage ensures that staging and production deploy identical states. The release structure with releases/, shared/ and the current symlink makes the symlink switch atomic and rollback trivial.

The verify job after deployment is not an optional luxury, it is the automatic proof that the new state actually works. The rollback job is not an emergency tool improvised while things are on fire, it is a regularly practiced process proven on staging before it is ever needed on production. This process takes effort to build and maintain. But it is exactly what makes the difference between a deployment process that keeps the team anxious and one that gives the team confidence.

Magento Zero Downtime with GitLab: the essentials at a glance

Runner and artifact

A dedicated runner with a Docker executor. A reproducible artifact from the build stage. Never build on production, always deploy.

Release structure

releases/, shared/ and the current symlink. Shared paths for env.php, pub/media and var/log. An atomic symlink switch means no downtime moment.

Pipeline stages

build to test to deploy to verify to rollback. Each stage has one responsibility. Deploy to production only with manual approval.

Rollback is mandatory

As a pipeline job, not a manual emergency. Practice it on staging. Run verify and analyze the cause after every rollback.

11. FAQ: Magento Zero Downtime with GitLab

1Can Magento really be deployed without interruption?
For the vast majority of cases, yes. The symlink switch is atomic. Database migrations with locks can cause interruptions, which is what expand/contract strategies are for.
2How long does a typical deployment take?
Build: 2 to 5 min. Deploy: 1 to 2 min. Verify: 30 sec. Total process: 5 to 10 minutes from trigger to a green verify job.
3What happens if setup:upgrade fails?
The deploy job fails before the symlink is switched. The previous state remains active. There is no visible impact on production.
4How many releases should I keep on the server?
At least five. That allows for several rollback steps. A minimum of three works if storage is limited, but it narrows your rollback room to maneuver.
5Staging versus production in this process?
Same artifacts, same pipeline logic. Only the CI/CD variables differ. Staging deploys automatically, production requires manual approval.
6How do I handle pub/media during deployment?
pub/media lives in shared/ and is never deployed. A symlink to shared/pub/media is set in every release. Media outlives every release.
7Deploy to multiple servers (web plus cron)?
Yes, the deploy job can run in parallel over SSH to multiple servers. Deployer natively supports parallel hosts for a coordinated switch.
8What if the rollback job itself fails?
Log in manually over SSH: ln -sfn /var/www/magento/releases/PREVIOUS_RELEASE /var/www/magento/current, then cache:flush. This procedure must be documented and known to the team.
9Integrating database backups into the deployment process?
As the first step of the deploy job, or as a separate job that runs before it. Store the backup timestamp in the release directory or as a pipeline variable.
10Does a hotfix need to go through the entire build process?
Yes. Reproducibility and verification apply to hotfixes too. The build takes only a few minutes and is not a real obstacle. Patching the server directly gets overwritten by the next deployment.