for Magento releases in GitLab
A rollback that only gets written once things break is not a rollback at all. If the rollback script is not finished before the first deployment, it will be tested for the first time under pressure and stress, exactly when everything needs to happen fast.
Table of contents
- 1. The rollback philosophy: preparation instead of improvisation
- 2. Prerequisites: what a rollback needs in place
- 3. The complete rollback script
- 4. The rollback job in the GitLab pipeline
- 5. Determining the previous release automatically
- 6. Cache flush and queue behavior during rollback
- 7. Testing the rollback regularly
- 8. Rollback strategies compared
- 9. Summary
- 10. Common rollback mistakes
- 11. FAQ
1. The rollback philosophy: preparation instead of improvisation
A rollback is not a special case, it is a regular part of the deployment process. Anyone who runs deployments without a prepared rollback path does not have a deployment process, they have a hope. That sounds harsh, but it reflects the experience from many Magento projects: the rollback gets postponed until it is needed, and then it fails because of missing prerequisites, time pressure and lack of practice.
The rollback script must therefore be developed as the first part of the deployment system, not the last. It must be versioned in the repository, it must be available in the GitLab pipeline as a manual job, and it must be tested regularly on staging, at least once a quarter, ideally monthly. Only a rehearsed rollback is a real rollback.
The good news for Magento with a symlink based release structure: a file rollback is trivial. Switching the current symlink to the previous release directory takes less than a second. The hard part is not the technique, it is the decision, and the side effects such as cache state, queue messages and static content.
2. Prerequisites: what a rollback needs in place
For a rollback to work, three things must be in place. First, the previous release directory must exist on the server. If old releases are deleted too aggressively, there is nothing left to switch back to. At least the last release must always be kept, ideally three to five. Second, the symlink mechanism must be set up correctly. A rollback is only possible if deployment never overwrote files directly but always deployed into a new directory and then switched the symlink. Third, the rollback script must have the credentials to reach the server, the same SSH credentials the deploy script uses.
# Verify rollback prerequisites before any production deploy
rollback:check-prerequisites:
stage: verify
script:
- |
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s <<'SSH'
set -euo pipefail
APP_PATH="${DEPLOY_PATH}"
# Count available releases for rollback
release_count=$(ls -1d "${APP_PATH}/releases/"*/ 2>/dev/null | wc -l)
echo "[INFO] Available releases for rollback: ${release_count}"
if [[ ${release_count} -lt 2 ]]; then
echo "[WARN] Only ${release_count} release(s) available, rollback not possible after this deploy"
fi
# Verify current symlink exists and resolves correctly
current_target=$(readlink -f "${APP_PATH}/current" 2>/dev/null || echo "MISSING")
echo "[INFO] Current release: ${current_target}"
test -d "${current_target}" \
|| { echo "[FAIL] current symlink target does not exist"; exit 1; }
echo "[OK] Rollback prerequisites verified"
SSH
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
3. The complete rollback script
The rollback script has a clear structure: first the target release is determined, either through an explicitly passed parameter or automatically as the second to last release in the releases/ directory. Then all the necessary Magento steps are executed: enable maintenance mode, switch the symlink, flush the cache, align static content with the new release and disable maintenance mode. Every step is logged, errors cause an immediate abort.
#!/usr/bin/env bash
# scripts/rollback.sh: Roll back to a previous Magento release
# Usage: ./rollback.sh [release-identifier]
# If no release identifier is given, rolls back to the previous release
set -euo pipefail
readonly APP_PATH="${DEPLOY_PATH:?DEPLOY_PATH is not set}"
readonly DEPLOY_HOST_VAR="${DEPLOY_HOST:?DEPLOY_HOST is not set}"
readonly DEPLOY_USER_VAR="${DEPLOY_USER:?DEPLOY_USER is not set}"
# Determine target release, use argument or auto-detect previous
TARGET_RELEASE="${1:-}"
ssh "${DEPLOY_USER_VAR}@${DEPLOY_HOST_VAR}" bash -s <<SSH
set -euo pipefail
APP_PATH="${APP_PATH}"
# Auto-detect previous release if none specified
if [[ -z "${TARGET_RELEASE}" ]]; then
current_release="\$(readlink -f \${APP_PATH}/current)"
current_name="\$(basename \${current_release})"
# List releases sorted by name descending, skip current, take first
TARGET_RELEASE="\$(ls -1d \${APP_PATH}/releases/*/ \
| sort -r \
| grep -v "\${current_name}" \
| head -1 \
| xargs basename)"
echo "[INFO] Auto-detected rollback target: \${TARGET_RELEASE}"
fi
ROLLBACK_PATH="\${APP_PATH}/releases/\${TARGET_RELEASE}"
# Abort if target does not exist
test -d "\${ROLLBACK_PATH}" \
|| { echo "[FAIL] Release \${TARGET_RELEASE} not found on server"; exit 1; }
echo "[ROLLBACK] Switching from \$(basename \$(readlink \${APP_PATH}/current)) to \${TARGET_RELEASE}"
# Enable maintenance mode to prevent user requests during switch
cd "\${APP_PATH}/current"
bin/magento maintenance:enable || echo "[WARN] Could not enable maintenance mode"
# Atomic symlink switch to previous release
ln -sfn "\${ROLLBACK_PATH}" "\${APP_PATH}/current"
echo "[OK] Switched current to \${TARGET_RELEASE}"
# Flush all caches, the previous release may have different cache keys
cd "\${APP_PATH}/current"
bin/magento cache:flush
echo "[OK] Cache flushed"
# Disable maintenance mode
bin/magento maintenance:disable || echo "[WARN] Could not disable maintenance mode"
echo "[ROLLBACK COMPLETE] Now running \${TARGET_RELEASE}"
SSH
4. The rollback job in the GitLab pipeline
The rollback job is a manual job in the rollback stage of the pipeline. It does not run automatically, that would mean every pipeline could trigger a rollback. Instead, it can be triggered with a click in the GitLab interface without starting a new pipeline. The last pipeline that ran a successful deploy also contains the rollback job that rolls back on the same server.
The variant with when: manual and without allow_failure: false ensures that the pipeline is not marked as failed when the rollback job is not executed. The job is optional, it only runs in an emergency. In addition, a ROLLBACK_TARGET variable can be passed as a pipeline variable to specify a particular release as the rollback target.
rollback:production:
stage: rollback
environment:
name: production
url: https://shop.example.com
variables:
# Optional: override with specific release name via GitLab UI pipeline variable
ROLLBACK_TARGET: ""
script:
- chmod +x scripts/rollback.sh
- ./scripts/rollback.sh "${ROLLBACK_TARGET}"
rules:
# Available only on semver tags, same pipeline that deployed the release
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: manual
allow_failure: true
# Rollback job should run even if deploy verify failed
needs:
- job: deploy:production
optional: true
5. Determining the previous release automatically
Automatically determining the previous release is a critical part of the rollback script. The script must not make assumptions about how releases are named, it has to decide based on the directories that actually exist on the server. The most reliable approach is to list all release directories in sorted order and skip the currently active release.
If the tag format is SemVer, a semantic sort can also be implemented: sort -V sorts by version number correctly and ensures that v1.9.0 comes before v1.10.0. This sort is more robust than alphabetical sorting, which produces wrong results with two digit minor versions. The script always clearly states which release was chosen as the rollback target before it performs the switch.
6. Cache flush and queue behavior during rollback
After the symlink switch, the Magento cache must be flushed completely. This applies even if the previous release used the same cache keys, because Redis may have cached data from the broken release that is incompatible with the old code. A bin/magento cache:flush after the rollback is mandatory, not an optional step.
The queue is more complex. Messages that are already sitting in RabbitMQ or in the Magento queue may have been placed there by consumers of the new release. These messages might not be processed correctly by the old code. The rollback script should stop queue consumers before the symlink is switched, and start them again after the rollback. In Magento environments with Supervisor or systemd, the consumer processes are managed by a process manager anyway, these need to be restarted explicitly after the rollback so they pick up the new application path.
7. Testing the rollback regularly
The most important sentence in this article is: a rollback that has never been tested is not a rollback. The rollback script must be run regularly on staging, not in a simulated environment but with the real script against the real staging server. That means: deploy a release, then trigger the rollback job, then verify that the previous state is active again.
This test should be part of the regular team routine and documented in the deploy checklist. Ideally a chaos engineering approach is chosen: once a month, before the planned deployment, a rollback test is run on staging. The team sees how long the rollback takes, which steps run and whether the verify job is green afterward. This exercise is cheaper than an unplanned rollback under production pressure.
8. Rollback strategies compared
There are several rollback strategies that require different amounts of preparation and come with different constraints. The comparison helps decide which strategy fits your own Magento project.
| Strategy | Duration | Prerequisite | Suitability |
|---|---|---|---|
| Symlink rollback | < 1 minute | Release directory present | Always recommended |
| New deployment (old tag) | 5 to 15 minutes | Tag present, pipeline green | When no release directory is left |
| Database rollback | 30+ minutes | Backup before the deploy | Only if DB migrations require a rollback |
| git revert plus deploy | 15 to 30 minutes | New commit plus new pipeline | When no older release is available |
| Server snapshot | 5 to 20 minutes | Snapshot taken before the deploy | Emergency without a release structure |
9. Summary
A rollback script for Magento in GitLab is not an optional feature, it is a mandatory part of every deployment process. The script switches the current symlink to the previous release, flushes the cache, stops and starts queue consumers and clearly reports what is happening. The associated GitLab job can be triggered manually from the last deploy pipeline and does not require a new pipeline.
The key to a successful rollback lies in three things: preparation before the first deployment, testing on staging at regular intervals, and clear documentation of who is allowed to trigger the rollback job under which circumstances. A rollback that has not been rehearsed is a gamble in an emergency, not a plan.
Rollback script for Magento: the essentials at a glance
Core mechanism
Symlink switch to the previous release. Takes less than a second. Requires release directories to be present.
Mandatory steps
Enable maintenance mode, switch symlink, flush cache, restart queue consumers, disable maintenance mode. Keep the order.
GitLab integration
Manual job in the rollback stage of the deploy pipeline. No new pipeline needed. ROLLBACK_TARGET variable for specific releases.
Test cadence
At least monthly on staging. Rollback test as a fixed part of the deploy checklist. An unrehearsed rollback is not a real rollback.
10. Common rollback mistakes
The most common mistake is forgetting to flush the cache after the symlink switch. Redis may hold cached objects from the broken release that are incompatible with the old code. A Magento system that runs after a rollback without a cache flush behaves unpredictably, sometimes correctly, sometimes with 500 errors that are hard to diagnose.
A second mistake is missing verification that the rollback target actually exists. A rollback script that tries to switch to a release that is no longer present, leaving a half completed state, makes the damage worse. The script must check the target directory and abort with a clear error if it is absent, before the current symlink is changed. The order matters: check first, then switch.