Backups and Recovery

Back up the database, the media and env.php, skip everything a build can rebuild, and rehearse the restore before you need it.

Syntaxmysqldump --single-transaction --quick --routines --triggers --events magento | gzip > magento-$(date -u +%F).sql.gz

Only two things in a Magento installation cannot be rebuilt: the database and pub/media. vendor/, generated/, pub/static, var/cache and var/page_cache all come back from a build. A backup that copies everything is slow and large and still misses app/etc/env.php, which holds the credentials the restore needs.

What to keep, how often

  • The database: a consistent dump at least nightly, plus binary logs between dumps if the business needs point-in-time recovery.
  • pub/media: continuously if it sits on object storage with versioning, otherwise a nightly sync. It only grows, so a sync is cheap.
  • app/etc/env.php: on every change, into a secret store. app/etc/config.php belongs in git, where it already is.

Not the built-in command

Magento still carries bin/magento setup:backup, disabled until you run bin/magento config:set system/backup/functionality_enabled 1. Adobe's documentation says plainly: "Backup functionality is deprecated as of 2.1.16, 2.2.7, and 2.3.0", and recommends binary tools such as Percona XtraBackup. It also writes into var/backups, on the disk you are trying to protect. Use mysqldump or XtraBackup instead.

A restore you have rehearsed

A backup you have never restored is a hypothesis. Rehearse on a scratch host: import the dump, copy media, write a fresh env.php, check bin/magento setup:db:status, then bin/magento cache:flush and bin/magento indexer:reindex. Time the whole thing. That number is your recovery time, and it is the one the business will ask for.

Example

Example · bash
#!/usr/bin/env bash
# bin/backup.sh - run from the Magento root as the file system owner.
set -euo pipefail

ROOT=/var/www/magento
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
DEST=/backups/$STAMP
mkdir -p "$DEST"
cd "$ROOT"

# Credentials come from env.php, not from a second copy in this script.
read_env() {
  php -r '$c = require "app/etc/env.php";
          echo $c["db"]["connection"]["default"]["'"$1"'"];'
}
DB_HOST=$(read_env host)
DB_NAME=$(read_env dbname)
DB_USER=$(read_env username)
MYSQL_PWD=$(read_env password)
export MYSQL_PWD              # keeps the password out of the process list

# Whole schema, one transaction, no table locks on InnoDB.
mysqldump --single-transaction --quick --routines --triggers --events \
  --default-character-set=utf8mb4 \
  -h "$DB_HOST" -u "$DB_USER" "$DB_NAME" | gzip -1 > "$DEST/db.sql.gz"

# The two files a restore cannot reconstruct.
tar -czf "$DEST/config.tar.gz" app/etc/env.php app/etc/config.php

# Media: additive sync, never --delete, because a deleted image is a lost image.
aws s3 sync pub/media s3://store-backups/media/

sha256sum "$DEST"/*.gz > "$DEST/SHA256SUMS"
find /backups -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +

# Writes /backups/<stamp>/db.sql.gz, config.tar.gz, SHA256SUMS
# and keeps 30 days of them.

When to use it

  • Replace a deprecated setup:backup cron job with something that survives disk loss.
  • Answer 'how long would a full restore take' with a measured number.
  • Hand a developer a production-shaped database without handing over customer data.
  • Recover a store after a failed upgrade without losing the day's orders.

More examples

The restore rehearsal, timed

Every step here is one someone would otherwise improvise during an incident, including the base URLs, which a restored database still points at production. The reindex is what makes the storefront usable, and it is usually the longest step, so measure it rather than estimating. Run the rehearsal on a schedule; a dump that stopped being valid three weeks ago is found here or in the outage.

Example · bash
#!/usr/bin/env bash
# Run monthly on a scratch host. The number it prints is your recovery time.
set -euo pipefail
START=$(date +%s)

mysql -e 'DROP DATABASE IF EXISTS magento_restore; CREATE DATABASE magento_restore'
gunzip -c /backups/latest/db.sql.gz | mysql magento_restore

cd /var/www/restore
tar -xzf /backups/latest/config.tar.gz
aws s3 sync s3://store-backups/media/ pub/media

bin/magento setup:config:set -n --db-name=magento_restore
bin/magento setup:db:status || true      # exit 2 means setup:upgrade is pending
bin/magento config:set web/unsecure/base_url http://restore.test/
bin/magento config:set web/secure/base_url   http://restore.test/
bin/magento cache:flush
bin/magento indexer:reindex

curl -s -o /dev/null -w 'health_check=%{http_code}\n' http://restore.test/health_check.php
echo "restore took $(( ($(date +%s) - START) / 60 )) minutes"

# health_check=200
# restore took 34 minutes

Making the restored copy safe to hand to developers

A production dump on a laptop is a data breach waiting for a lost laptop, and a restored copy that can still send mail will email real customers. Run this immediately after the import, before anyone connects, then bin/magento cache:flush and bin/magento indexer:reindex so the grids match. Keep the script in the repository next to the restore script, so the sanitised copy is the easy path.

Example · sql
-- On the RESTORED COPY only. Never against production.
UPDATE customer_entity
SET email = CONCAT('customer', entity_id, '@example.test'),
    firstname = 'Test';

UPDATE customer_grid_flat
SET email = CONCAT('customer', entity_id, '@example.test'),
    name = 'Test Customer';

UPDATE sales_order SET customer_email = CONCAT('order', entity_id, '@example.test');
UPDATE sales_order_address SET email = CONCAT('order', parent_id, '@example.test');

DELETE FROM admin_user WHERE username <> 'restore_check';
DELETE FROM oauth_token;

What to exclude from a filesystem backup

Excluding generated/ and pub/static keeps the archive small and prevents a restore from reviving half-built assets from an older release. pub/media is excluded here because it has its own, additive backup; app/code is included because a store with unversioned custom modules is more common than anyone admits. Use this on a legacy server before you trust its git history.

Example · bash
# Code and build output are rebuildable: back up the inputs, not the outputs.
tar -czf /backups/files-$(date -u +%F).tgz \
  --exclude='./var/cache' \
  --exclude='./var/page_cache' \
  --exclude='./var/session' \
  --exclude='./var/view_preprocessed' \
  --exclude='./var/tmp' \
  --exclude='./generated' \
  --exclude='./pub/static/frontend' \
  --exclude='./pub/static/adminhtml' \
  --exclude='./vendor' \
  --exclude='./pub/media' \
  ./app ./composer.json ./composer.lock ./auth.json

# Keep: var/log and var/report, if you need yesterday's stack traces.
# Rebuild instead: composer install --no-dev, setup:di:compile,
# setup:static-content:deploy.

Discussion

  • Be the first to comment on this lesson.