VPS Snaps

How to back up and restore a Magento 2 store

A Magento 2 backup needs three things: a dump of the database, the pub/media directory, and app/etc/env.php, which holds the database credentials and the encryption key. vendor/, generated/, pub/static and the caches are rebuilt by Composer and bin/magento, and your code and app/etc/config.php belong in git. Lose the encryption key and the values Magento encrypted, such as payment and shipping integration passwords, cannot be decrypted.

10 min readUpdated Checked against official documentation

What to back up, and what to skip

This guide covers Magento Open Source, the free edition, and Adobe Commerce on your own servers; both are Magento 2. Adobe Commerce on cloud infrastructure has its own snapshots and ece-tools db-dump, and Adobe runs the servers for Adobe Commerce as a Cloud Service. The store here lives in /var/www/magento, owned by a file system owner named magento in the web server's www-data group.

PartWhereBack it up?
DatabaseMySQL or MariaDB, set in env.phpYes. Catalog, customers, orders and configuration.
Mediapub/mediaYes. Product images and other uploads cannot be rebuilt.
Resized image cachepub/media/catalog/product/cacheOptional. Rebuilding it is slow on large catalogs; see below.
env.phpapp/etc/env.phpYes, on its own and encrypted. Encryption key and every connection setting.
config.phpapp/etc/config.phpIn git. Modules, scopes and themes.
Codecomposer.json, composer.lock, app/code, app/designIn git. Back it up only if it is not in a repository.
auth.jsonNext to composer.json, or in Composer's home directoryKeep it with your secrets, or create new keys in Commerce Marketplace.
Rebuiltvendor/, generated/, pub/static, var/cache, var/page_cache, var/view_preprocessedNo.

Adobe calls config.php the shared configuration file and says to check it in to source control. env.php holds the settings for one installation.

env.php and the encryption key

Adobe's docs describe the key in env.php's crypt section as protecting passwords and other sensitive data with ChaCha20-Poly1305, including credit card data and payment and shipping integration passwords. It is set at install and, by default, stored only in env.php. A dump restored under a different key loads, but those values cannot be decrypted.

Rotating the key with bin/magento encryption:key:change (the Admin page for it was removed in 2.4.8) does not replace the old key. env.php keeps every key in the same value, separated by whitespace; Magento encrypts with the last one and records which key each value used. An env.php saved before a rotation therefore cannot read values written after it. Archive env.php again after every key change:

Terminal
install -d -m 700 /var/backups/magento
Terminal
tar -czf /var/backups/magento/shop-env-$(date +%F).tar.gz -C /var/www/magento app/etc/env.php

env.php also holds the database password and the cache, session and queue credentials. Store its archive apart from the database dumps and encrypt it. Adobe's advice for a new key applies to every key: keep a record of it in a secure location, because it is required to decrypt the data.

auth.json holds the 32-character keys Composer uses for repo.magento.com. Composer's docs say to keep it out of git. Keep a copy with your other secrets, or generate a new pair in Commerce Marketplace when you restore.

Dump the database with mysqldump

Adobe's docs say every bin/magento command must run as the file system owner. Read the connection settings from env.php the same way:

Terminal
cd /var/www/magento
Terminal
sudo -u magento php -r '$db = (include "app/etc/env.php")["db"]["connection"]["default"]; echo $db["host"], " ", $db["dbname"], " ", $db["username"], PHP_EOL;'

Put the user and password in an option file, so the password stays out of ps and your shell history:

/etc/mysql/magento-backup.cnf
[client]
user=magento
password="your-password-here"
host=localhost
Terminal
chmod 600 /etc/mysql/magento-backup.cnf
Terminal
mysqldump --defaults-extra-file=/etc/mysql/magento-backup.cnf --single-transaction --no-tablespaces magento | gzip > /var/backups/magento/shop-db-$(date +%F).sql.gz
  • --single-transaction dumps InnoDB tables, Magento's engine, from one consistent snapshot without locking them, so the store keeps taking orders.
  • --no-tablespaces avoids the global PROCESS privilege, which Adobe's recommended GRANT ALL ON magento.* does not include.
  • Triggers are dumped by default. Keep them: indexers set to Update by Schedule depend on database triggers. Never add --skip-triggers.
  • On MariaDB 11 and later, Adobe recommends mariadb-dump, since the mysqldump name is deprecated there. It is the same program under its new name.

A complete dump ends with a -- Dump completed on line:

Terminal
gunzip -c /var/backups/magento/shop-db-2026-10-04.sql.gz | tail -n 1

Adobe's own dump commands take the whole database, and its backup documentation lists no tables a backup may skip. Its knowledge base does note that index tables can be emptied and rebuilt with a reindex, which can take a long time on a large catalog, so a backup without them makes the restore slower. For a large database, consider a binary backup: Adobe's backup page itself recommends binary tools such as Percona XtraBackup, covered in the XtraBackup guide. The mysqldump guide covers the remaining options.

Archive pub/media

Dump the database first, then the media:

Terminal
tar -czf /var/backups/magento/shop-media-$(date +%F).tar.gz -C /var/www/magento pub/media

pub/media/catalog/product/cache holds the resized product images. Adding --exclude='pub/media/catalog/product/cache' before -C makes the archive much smaller, but the cache must then be rebuilt after a restore, or pages resize images as they load, which Adobe says slows the site for days to weeks. Adobe's playbook also warns that bin/magento catalog:images:resize is single-threaded, redoes images already resized and cannot be interrupted, and that its faster storefront method still needs under 8 hours for 100,000 images where the CLI takes 6 days. For a big catalog, keep the cache in the backup.

A full media archive every night gets heavy. Tools that only send changed files, such as restic or rsync, suit large pub/media directories better.

What about bin/magento setup:backup?

Magento 2.4.9 still ships bin/magento setup:backup (--code, --media, --db) and setup:rollback, but Adobe's docs mark the feature deprecated as of 2.1.16, 2.2.7 and 2.3.0. It is disabled until you run bin/magento config:set system/backup/functionality_enabled 1, keeps the store in maintenance mode for the whole run, and writes into var/backups on the same server. Adobe also warns that a rollback can silently fail and leave incomplete data. Use the commands above instead.

Restore on a new server

Restoring over the live store on the same server? Run bin/magento maintenance:enable and bin/magento cron:remove, stop any running queue:consumers:start processes, and drop and recreate the database as Adobe's restore article does. Then continue with the tar commands below.

On a new server, install the PHP, MySQL or MariaDB, search engine and Composer versions Adobe's system requirements list for your Magento release, plus Redis or Valkey and RabbitMQ if env.php uses them. Create an empty database and user as Adobe's install guide does:

MySQL / MariaDB prompt
CREATE DATABASE magento;
CREATE USER 'magento'@'localhost' IDENTIFIED BY 'your-password-here';
GRANT ALL ON magento.* TO 'magento'@'localhost';

Clone the repository into /var/www/magento, check out the commit that was live at backup time, put auth.json next to composer.json, and install the locked dependencies as the file system owner. --no-dev skips require-dev packages:

Terminal
sudo -u magento composer install --no-dev
Terminal
tar -xzf /var/backups/magento/shop-env-2026-10-04.tar.gz -C /var/www/magento
Terminal
tar -xzf /var/backups/magento/shop-media-2026-10-04.tar.gz -C /var/www/magento

Edit env.php for this server: the db connection, and the cache, session and queue hosts if they changed. Keep crypt exactly as it is. Then import the dump. Triggers in it name the original database user as their DEFINER, and MySQL 8.4 only lets you create objects for another account with the SET_ANY_DEFINER privilege. Adobe's knowledge base strips the clause on import:

Terminal
gunzip -c /var/backups/magento/shop-db-2026-10-04.sql.gz | sed -e 's/DEFINER[ ]*=[ ]*[^*]*\*/\*/' | mysql --defaults-extra-file=/etc/mysql/magento-backup.cnf magento

The triggers are then created for the importing user. If the domain changed, set the base URLs; the scheme and trailing slash are required:

Terminal
sudo -u magento bin/magento setup:store-config:set --base-url="https://shop.example.com/" --base-url-secure="https://shop.example.com/"

Websites or store views with their own base URLs need bin/magento config:set with --scope=websites or --scope=stores and --scope-code=<code>, for web/unsecure/base_url and web/secure/base_url. If the search engine runs on a different host, set catalog/search/opensearch_server_hostname with bin/magento config:set.

Rebuild code, static files and indexes

These follow Adobe's production deployment steps, run as the file system owner from /var/www/magento:

Terminal
sudo -u magento bin/magento setup:upgrade
Terminal
sudo -u magento bin/magento setup:di:compile
Terminal
sudo -u magento bin/magento setup:static-content:deploy en_US
Terminal
sudo -u magento bin/magento indexer:reindex
Terminal
sudo -u magento bin/magento cache:flush
  • setup:upgrade brings the database schema and data in line with the installed modules.
  • setup:di:compile generates factories, proxies, interceptors and the dependency injection configuration in generated/.
  • setup:static-content:deploy writes pub/static. Adobe's docs give en_US as its default language, so list every locale your store views use, such as en_US de_DE. It runs only in production mode unless you add -f.
  • indexer:reindex rebuilds every index, including catalog search in OpenSearch, which the dump does not contain.
  • cache:flush empties the whole cache storage, not just Magento's entries.

Set ownership with Adobe's shared-group commands, from /var/www/magento:

Terminal
find var generated vendor pub/static pub/media app/etc -type f -exec chmod g+w {} +
Terminal
find var generated vendor pub/static pub/media app/etc -type d -exec chmod g+ws {} +
Terminal
chown -R :www-data .
Terminal
chmod u+x bin/magento

Then install the Magento crontab with sudo -u magento bin/magento cron:install, and run bin/magento maintenance:disable if you enabled maintenance mode.

Verify the restore

Terminal
sudo -u magento bin/magento setup:db:status

All modules are up to date. means the schema matches the code. bin/magento indexer:status should show every indexer as Ready. Then fetch the storefront and expect 200:

Terminal
curl -sS -o /dev/null -w '%{http_code}\n' https://shop.example.com/

Log in to the Admin, open a product with images, run a storefront search, and test a payment method that uses stored credentials: that last check fails if env.php's key does not match the data. The restore testing guide shows how to repeat this on a schedule.

Automate it with cron

This script writes each file under a temporary name, checks the dump, and only then gives it its real name. tar exits with 1 when a file changed while it was read, so 1 counts as success.

/usr/local/bin/magento-backup.sh
#!/bin/bash
set -euo pipefail

APP="/var/www/magento"
BACKUP_DIR="/var/backups/magento"
KEEP_DAYS=14
STAMP="$(date +%Y-%m-%d_%H%M)"
DB="$BACKUP_DIR/shop-db-$STAMP.sql.gz"
MEDIA="$BACKUP_DIR/shop-media-$STAMP.tar.gz"

mkdir -p "$BACKUP_DIR"
trap 'rm -f "$DB.partial" "$MEDIA.partial"' EXIT

mysqldump --defaults-extra-file=/etc/mysql/magento-backup.cnf \
  --single-transaction --no-tablespaces magento | gzip > "$DB.partial"
gunzip -c "$DB.partial" | tail -n 1 | grep -q "Dump completed"
mv "$DB.partial" "$DB"

tar -czf "$MEDIA.partial" -C "$APP" pub/media || [ $? -eq 1 ]
mv "$MEDIA.partial" "$MEDIA"

find "$BACKUP_DIR" -type f \( -name "shop-db-*" -o -name "shop-media-*" \) -mtime +"$KEEP_DAYS" -delete
Terminal
chmod 755 /usr/local/bin/magento-backup.sh
/etc/cron.d/magento-backup
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

40 2 * * * root flock -n /run/lock/magento-backup.lock /usr/local/bin/magento-backup.sh >> /var/log/magento-backup.log 2>&1

-mtime +14 keeps about two weeks of nightly backups; the pattern leaves the env.php archives alone. Copy the backups off the server each night, for example with rclone; the cron guide covers logging and alerts.

Common errors

ErrorFix
Access denied; you need (at least one of) the PROCESS privilege(s) for this operationAdd --no-tablespaces to mysqldump.
The import fails on a CREATE TRIGGER line with access deniedThe dump's DEFINER names another account. Strip it with Adobe's sed command, or import as the same user and host.
The store redirects to the old domainRun setup:store-config:set with the new base URLs, check website and store view scopes, then flush the cache.
Every page returns 503Maintenance mode is still on. Check with bin/magento maintenance:status, then maintenance:disable.
Pages load without CSS or JavaScriptStatic files are missing for that theme or locale. Run setup:static-content:deploy with every locale code.
Catalog search returns nothingOpenSearch is not reachable or not indexed. Fix catalog/search/opensearch_server_hostname, then bin/magento indexer:reindex catalogsearch_fulltext.
Product images are missingpub/media was not restored, or the web server cannot write to it to rebuild resized images. Check ownership; catalog:images:resize rebuilds the cache.
Payment or shipping integrations fail to authenticateenv.php comes from another install or predates a key rotation. Restore the env.php that matches the dump.

Moving the store to another provider? See moving a server to a new provider.

Frequently asked questions

Is bin/magento setup:backup still supported?
It still ships in Magento 2.4.9, but Adobe has marked it deprecated since 2.1.16, 2.2.7 and 2.3.0. It is disabled by default, puts the store in maintenance mode, and saves to var/backups on the same server.
What happens if I lose the Magento encryption key?
The database still restores, but values Magento encrypted with that key, such as payment and shipping integration passwords, cannot be decrypted. You would have to enter them again. The key cannot be recovered from the data.
Do I need maintenance mode to back up Magento?
Not for mysqldump with --single-transaction, which reads InnoDB tables from a consistent snapshot while the store runs. The deprecated setup:backup command and the cloud ece-tools db-dump switch it on themselves.
Can I leave tables out of a Magento database dump?
Adobe documents no list of tables a backup may skip, and its own dump tools take the whole database. Index tables can be rebuilt with a reindex, but on a large catalog that makes the restore much slower.
Do I need to back up vendor, generated or pub/static?
No. composer install rebuilds vendor from composer.lock, setup:di:compile rebuilds generated, and setup:static-content:deploy rebuilds pub/static.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: