Rollback Strategies for Magento: Files, Database, Queue State, Assets
AI generated
CI/CD
.yml
GitLab · CI/CD · Magento · Rollback Strategies
Rollback Strategies for Magento
Files, Database, Queue & Assets

A Magento rollback is more than flipping a symlink. The database, queue messages, static content, and Elasticsearch indexes each follow their own rules. Anyone who rolls back only the files risks inconsistencies that are harder to fix than the original problem.

15 min read File Rollback · DB Rollback · Queue · Static Content · Elasticsearch GitLab CI/CD · Magento 2 · Zero Downtime

1. The Four Layers of a Magento Rollback

A Magento system consists of four independent layers that need to be treated differently during a rollback. The first layer is the application files: PHP code, Hyva templates, Composer dependencies, compiled DI code, and static content. This layer can be rolled back via symlink in seconds. The second layer is the database: Magento configuration, product data, orders, and every table touched by setup:upgrade. This layer is not automatically rollback capable.

The third layer is queue state: messages in RabbitMQ or the MySQL-based Magento queue that were placed by consumers of the new release and that may not be processable by the old code. The fourth layer is external services: Elasticsearch indexes, Redis cache contents, and Varnish cache objects. This layer is often the least considered, yet it can lead to hard-to-diagnose errors after a rollback.

The key insight: a rollback that only addresses the file layer is sufficient for releases without database migrations or queue changes. For releases with database migrations, a pure file rollback is dangerous. The old code may encounter tables or columns it does not expect, or it may be missing columns it needs. The rollback strategy must therefore be planned explicitly before every release.

2. File Rollback: Symlink, Generated Code, and Static Content

The file rollback via symlink switch is the fastest and safest part of the rollback process. The release directory of the previous state is fully present, including the Composer vendor folder, compiled DI code, and static content. The symlink switch is atomic and takes under a second. A cache flush afterward is mandatory, because Redis may have cached objects from the faulty release.

The generated/ folder contains the dependency injection code compiled by Magento. This code is release specific and lives in the release directory, not in the shared area. After a rollback, Magento automatically uses the generated code of the previous release, no manual step required. The same applies to pub/static/: the static content of the previous release lives in the previous release directory and is served again once the symlink switches back.

#!/usr/bin/env bash
# scripts/rollback-files.sh: file-only rollback via symlink switch
# Safe for releases WITHOUT database migrations
set -euo pipefail

readonly APP_PATH="${DEPLOY_PATH:?}"
readonly ROLLBACK_TARGET="${1:-}"

ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s <<SSH
set -euo pipefail

# Auto-detect previous release if no target specified
if [[ -z "${ROLLBACK_TARGET}" ]]; then
  current="\$(basename \$(readlink -f ${APP_PATH}/current))"
  TARGET="\$(ls -1d ${APP_PATH}/releases/*/ \
    | sort -Vr | grep -v "\${current}/" | head -1 | xargs basename)"
else
  TARGET="${ROLLBACK_TARGET}"
fi

echo "[ROLLBACK] Target release: \${TARGET}"
test -d "${APP_PATH}/releases/\${TARGET}" \
  || { echo "[FAIL] Release \${TARGET} not found"; exit 1; }

# Brief maintenance window for cache consistency
cd "${APP_PATH}/current"
bin/magento maintenance:enable --ip="${ALLOWED_IP:-127.0.0.1}" 2>/dev/null || true

# Atomic symlink switch: zero-downtime file rollback
ln -sfn "${APP_PATH}/releases/\${TARGET}" "${APP_PATH}/current"
echo "[OK] Switched to \${TARGET}"

# Full cache flush: mandatory after any rollback
cd "${APP_PATH}/current"
bin/magento cache:flush
echo "[OK] Cache flushed"

# Restart queue consumers to pick up new application path
supervisorctl restart magento-consumer:* 2>/dev/null \
  || systemctl restart magento-queue-consumer 2>/dev/null \
  || echo "[WARN] Could not restart queue consumers, manual restart required"

bin/magento maintenance:disable 2>/dev/null || true
echo "[OK] File rollback complete, now running \${TARGET}"
SSH

3. Database Rollback: Backup Strategy and Expand-Contract

The database rollback is the hardest part of a Magento rollback. In the standard implementation, Magento database migrations are not reversible, there is no setup:downgrade. Anyone who needs to roll back a release that contained database migrations has three options: restore a full database backup (time consuming, data loss since the backup), apply an expand-contract pattern, or manually revert the migration.

The expand-contract pattern is the most elegant solution: before the actual release, a compatible database change is deployed that both the old and the new code can handle. Only once the new code is running stably and no rollback is needed anymore is the "contract" step deployed, which removes the transitional constructs. This pattern requires discipline during development, but it makes database migrations fully rollback capable without restoring a backup.

# scripts/db-backup.sh: create a timestamped database backup before deployment
# Run this BEFORE any release that includes database migrations
#!/usr/bin/env bash
set -euo pipefail

readonly BACKUP_DIR="${DEPLOY_PATH}/shared/backups/db"
readonly TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
readonly BACKUP_FILE="${BACKUP_DIR}/${CI_COMMIT_TAG:-manual}-${TIMESTAMP}.sql.gz"

ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s <<SSH
set -euo pipefail
mkdir -p "${BACKUP_DIR}"

echo "[BACKUP] Creating database backup: ${BACKUP_FILE}"
mysqldump \
  --single-transaction \
  --quick \
  --lock-tables=false \
  --routines \
  --triggers \
  "${MAGENTO_DB_NAME}" \
  | gzip -6 > "${BACKUP_FILE}"

backup_size="\$(du -sh "${BACKUP_FILE}" | cut -f1)"
echo "[OK] Backup created: ${BACKUP_FILE} (\${backup_size})"

# Keep only last 10 DB backups
ls -1t "${BACKUP_DIR}"/*.sql.gz 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null || true
echo "[OK] Old backups cleaned up"
SSH

4. Queue State During Rollback: What Happens to RabbitMQ Messages

The queue is the most frequently forgotten rollback layer. If a new release was active for a short time and placed messages in RabbitMQ or the Magento MySQL queue during that window, those messages need to be processed after the rollback, by the old code. That is possible if the message format is compatible. It becomes a problem if the new release introduced new message types or changed payload structures.

The practical approach: stop the queue consumers before the rollback, switch the symlink, flush the cache, and restart the consumers with the old code. Messages that the old code cannot process end up in the dead letter queue or an error table. These need to be reviewed manually after the rollback. In production systems with high queue throughput, the decision may also be to purge the affected queue for a short time, accepting the risk that a few messages are lost, rather than letting inconsistent messages be processed.

5. Static Assets and the Elasticsearch Index After a Rollback

Static content lives in the release directory and switches automatically with the symlink. That means Nginx immediately serves the static content of the previous release after the rollback, with no manual intervention and no cache warmup. Browser-cached assets from the faulty release are a short-lived problem that resolves itself through cache-busting mechanisms (asset hashes in filenames).

The Elasticsearch index, on the other hand, is not release specific. It is not rolled back when the symlink switches. If the new release changed the index mapping or the indexing routines, the index may become incompatible with the old code after the rollback. In that case a full reindex must be run: bin/magento indexer:reindex catalogsearch_fulltext. For large catalogs this takes anywhere from several minutes to hours, an important factor when planning a rollback.

6. Decision Tree: Which Rollback Strategy Fits?

The right rollback strategy depends on what the release changed. This decision should be made deliberately and documented before every release, not only once a rollback becomes necessary. The following logic helps with the classification:

If the release contains no database migrations and no queue changes, a pure file rollback via symlink is sufficient. If the release contains compatible database migrations (additive, without removing columns), a file rollback with a DB backup as a safety net afterward is sufficient. If the release contains incompatible database migrations (removing columns, changing types, breaking changes), a full DB backup must be made before deployment, and the rollback consists of a file rollback plus a DB restore.

# .gitlab-ci.yml: database backup job before migration-heavy releases
db:backup:before-migrate:
  stage: deploy
  script:
    - ./scripts/db-backup.sh
  rules:
    # Only run when the release is tagged as containing DB migrations
    # Set CONTAINS_DB_MIGRATIONS=true in GitLab pipeline variables when needed
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ && $CONTAINS_DB_MIGRATIONS == "true"
      when: always
  # This job must succeed before the deploy job runs
  allow_failure: false

rollback:with-db-restore:
  stage: rollback
  script:
    - |
      # Full rollback: files + database restore
      BACKUP_FILE="${DEPLOY_PATH}/shared/backups/db/${RESTORE_BACKUP_FILE}"
      ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s <<'SSH'
      set -euo pipefail
      # Step 1: stop consumers
      supervisorctl stop magento-consumer:* 2>/dev/null || true
      # Step 2: file rollback
      ln -sfn "${DEPLOY_PATH}/releases/${ROLLBACK_TARGET}" "${DEPLOY_PATH}/current"
      # Step 3: database restore
      zcat "${BACKUP_FILE}" | mysql "${MAGENTO_DB_NAME}"
      echo "[OK] Database restored from ${BACKUP_FILE}"
      # Step 4: cache and search reindex
      cd "${DEPLOY_PATH}/current"
      bin/magento cache:flush
      bin/magento indexer:reindex
      # Step 5: restart consumers
      supervisorctl start magento-consumer:* 2>/dev/null || true
      SSH
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual
      allow_failure: true

7. Mapping Rollback Strategies in the GitLab Pipeline

The GitLab pipeline maps the different rollback strategies as separate manual jobs. There are at least two jobs: one for the simple file rollback and one for the full rollback with DB restore. Which job gets triggered is decided by the deployment team during an incident, based on information documented in advance about whether the release contained database migrations.

This information must be captured in the deployment documentation or as a pipeline variable per release. A practical convention: releases with database migrations get a -migrate suffix or a pipeline variable CONTAINS_DB_MIGRATIONS=true. The rollback job for DB releases is then only visible when that variable is set. This prevents accidental DB rollbacks for releases that contain only code changes.

8. Rollback Scenarios Compared

Different release types call for different rollback strategies. The overview below shows which strategy fits which scenario and which side effects need to be considered.

Release Type Rollback Strategy Duration Data Loss Risk
Code/templates only Symlink + cache flush < 2 minutes None
Additive DB migration Symlink + cache + backup as a net < 3 minutes Low (new data in new column)
Breaking DB migration Symlink + DB restore 30-90 minutes All data since backup lost
Queue format change Symlink + purge queue + consumer restart < 5 minutes Pending queue messages lost
ES index change Symlink + reindex 10 min to several hours None (index gets rebuilt)

9. Summary

A complete rollback for Magento covers four layers: files, database, queue, and external services. The file rollback via symlink is the simplest and fastest part, and it is fully sufficient for releases without database migrations. For releases with additive migrations, a DB backup as a safety net makes sense. For releases with breaking database changes, a full DB restore is the only safe option, with the risk of data loss since the backup.

The most important practice is classifying the rollback strategy before every release: what does this release contain, and which rollback strategy does it therefore require? This information must be documented and visible to the whole team. GitLab pipeline variables offer a pragmatic way to make this classification machine readable and to activate the right rollback jobs.

Rollback Strategies for Magento: The Essentials at a Glance

Four layers

Files (symlink), database (backup/restore), queue (stop/restart), and external services (cache, ES index). Each layer has its own rollback requirements.

Decide before the release

Classify the rollback strategy before every release. Does it contain DB migrations? Queue changes? That answer determines which steps are needed during an incident.

DB backup is mandatory

Create a full DB backup before every release with database migrations. Without a backup there is no real rollback path for DB changes.

Queue and ES separately

Always restart queue consumers after a rollback. Fully reindex Elasticsearch when the index changed. Automate both steps as part of the rollback script.

10. Common Mistakes in Rollback Strategies

The most dangerous mistake is a file rollback for a release that contained breaking DB migrations. The result: the old code runs against the new database structure. Magento tries to access columns that were removed, or it ignores columns that should not exist yet. The errors are subtle and sometimes only appear under load, for example when a checkout process accesses a column in sales_order that no longer exists.

A second mistake is a rollback without a cache flush afterward. Redis holds cached objects that may be incompatible with the old code, especially if the faulty release introduced new cache tags or changed object structures. A cache flush after a rollback is always mandatory, even when it seems unnecessary for releases without caching changes. The cost of a cache flush (a brief performance dip) is minimal compared to the cost of inconsistent cache data.

11. FAQ: Rollback Strategies for Magento

1Just switch the symlink back after a DB migration?
Only for additive migrations. For breaking changes a DB restore must be performed, otherwise the old code runs against an incompatible DB structure.
2What is the expand-contract pattern?
Expand: deploy compatible DB changes, old code keeps running. Contract: remove old columns only once the new release is stable. Makes rollback possible without a DB restore.
3How long does a DB restore take?
Depends on DB size. A typical shop at 5 to 20 GB takes 30 to 90 minutes with mysqldump. Percona XtraBackup is noticeably faster.
4What happens to orders placed during the incident?
Lost during a DB restore. Unavoidable. Backup time should be as close as possible to the deploy time. Orders from the error window need to be re-entered manually.
5Detect DB migrations in a release?
Code review of db_schema.xml changes plus git diff. Set an explicit pipeline variable CONTAINS_DB_MIGRATIONS=true.
6Always reindex after a rollback?
Only if the rolled-back release changed Elasticsearch mapping or indexing routines. Otherwise not necessary.
7What happens to open sessions?
Sessions in shared/var/session are preserved. If the new release changed session structures, clear the session directory when problems occur.
8Enforce that no rollback runs without a DB backup?
Yes. Configure the DB backup job as a needs: requirement of the deploy job. GitLab then prevents a deploy without a prior backup.
9Can queue messages get lost during a rollback?
Yes, if the queue is purged. A deliberate decision: a controlled loss of a few messages is better than inconsistent processing.
10Document the rollback strategy per release?
Note it explicitly in the CHANGELOG and the GitLab release object. Set it as a pipeline variable. The information needs to be accessible before the rollback, not only afterward.