Rolling Back

Code rolls back with a symlink; the database does not, so keep schema changes additive and restore a backup before you need to.

Syntaxln -sfn $PREVIOUS /var/www/current.tmp && mv -Tf /var/www/current.tmp /var/www/current

Rolling back code is easy. Rolling back a database is not. With releases in their own directories, the code rollback is the swap in reverse: point the symlink at the previous release, reload PHP-FPM, flush the cache, restart the workers. It takes seconds, and it works because the previous release is still on disk, complete, with its own vendor/ and generated/.

What does not come back

A data patch that has run is recorded in the patch_list table, and moving the symlink neither removes that record nor undoes the rows it wrote. Declarative schema changes have already been applied. pub/media is shared, so uploads made since the deploy stay. Orders placed in those minutes are real orders.

Deploy so that you can roll back

Make schema changes additive: add a nullable column in one release, write to it in the next, drop it a release later. Then a code rollback is a complete rollback, and the unused column is harmless. The rollbacks that hurt are the ones where a single release both changed the schema and depended on the change.

The backup

The bundled setup:backup and setup:rollback commands were deprecated in 2.3.0; do not build a plan on them. Take a mysqldump --single-transaction before the migration, and restore it into staging on a schedule. A dump nobody has restored is not a backup.

Example

Example · bash
# deploy/rollback.sh - put the previous release back on the symlink
set -e

PREVIOUS=$(readlink -f /var/www/previous)   # written by release.sh
test -d "$PREVIOUS"

/usr/local/bin/workers.sh stop

ln -sfn "$PREVIOUS" /var/www/current.tmp
mv -Tf /var/www/current.tmp /var/www/current

systemctl reload php8.4-fpm
/var/www/current/bin/magento cache:flush

/usr/local/bin/workers.sh start

echo "code is back on $PREVIOUS - the database is not"

# Flushed cache types:
# config
# layout
# block_html
# ...
# code is back on /var/www/releases/20261004T1733 - the database is not

When to use it

  • Put the previous release back after a bad deploy, in seconds, with one symlink.
  • Plan a release whose schema change can be undone by rolling back the code alone.
  • Prove a nightly database dump can actually be restored, before you need it.
  • Work out what a rollback will not recover: orders, uploads, applied patches.

More examples

The dump you take before the migration

Run this as its own deploy step so the dump is tied to a release name and you know exactly which code it belongs to. --single-transaction keeps InnoDB consistent without a global lock, which matters because this runs while the site is up. Skipping the session and report_event tables cuts the size a long way and neither is worth restoring. The dump lands off the application disk, because a backup on the volume you are trying to recover is not one.

Example · bash
# deploy/dump.sh <release> - before migrate.sh, from the node that migrates
set -e

OUT="/backups/pre-$1-$(date -u +%Y%m%dT%H%M).sql.gz"

# --single-transaction: consistent snapshot without locking the storefront out
# --no-tablespaces: avoids needing the PROCESS privilege
mysqldump --single-transaction --quick --no-tablespaces \
    --ignore-table=magento.session \
    --ignore-table=magento.report_event \
    -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" magento | gzip > "$OUT"

ls -lh "$OUT"

# -rw-r--r-- 1 deploy deploy 1.8G /backups/pre-20261005T0910.sql.gz

An additive schema change, so the code rollback is enough

This is the shape to aim for in any release you might have to undo: the column can exist while the old code runs, so rolling the code back needs no database work at all. `bin/magento setup:upgrade` creates the column; adding it does not depend on the whitelist, because Magento consults db_schema_whitelist.json only for destructive operations. Regenerate it anyway with `bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_Loyalty`, since a later release cannot drop or shrink the column unless it is listed in app/code/Vendor/Loyalty/etc/db_schema_whitelist.json.

Example · xml
<!-- app/code/Vendor/Loyalty/etc/db_schema.xml -->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="sales_order" resource="sales" engine="innodb">
        <!-- nullable, with no default behaviour change: old code ignores it -->
        <column xsi:type="int" name="loyalty_tier_id" unsigned="true" nullable="true"
                comment="Loyalty tier at the time of purchase"/>
        <index referenceId="SALES_ORDER_LOYALTY_TIER_ID" indexType="btree">
            <column name="loyalty_tier_id"/>
        </index>
    </table>
</schema>

Rehearse the restore on a schedule

The point of the drill is to find the problems that only appear on restore: a dump that was truncated, a privilege the restore user lacks, an indexer that will not rebuild. Run it on a schedule and treat a failure as an incident, because the alternative is discovering it during one. Reindexing afterwards is part of the test; search and category listings come from the indexes, not the tables, and a restored database with stale indexes is not a working site.

Example · bash
# deploy/restore-drill.sh - weekly, into staging, against the newest dump
set -e

DUMP=$(ls -1t /backups/pre-*.sql.gz | head -n1)
echo "restoring $DUMP into staging"

gunzip -c "$DUMP" | mysql -h "$STAGING_HOST" -u "$STAGING_USER" -p"$STAGING_PASS" magento_staging

cd /var/www/staging
bin/magento setup:upgrade --keep-generated
bin/magento indexer:reindex
bin/magento cache:flush

# count one thing you can check against production
mysql -h "$STAGING_HOST" -u "$STAGING_USER" -p"$STAGING_PASS" magento_staging \
    -e 'SELECT COUNT(*) AS orders FROM sales_order'

# restoring /backups/pre-20261005T0910.sql.gz
# orders
# 418233

Discussion

  • Be the first to comment on this lesson.