VPS Snaps

How to take physical MySQL and MariaDB backups with XtraBackup

Install the Percona XtraBackup series that matches your MySQL major version (8.4 for MySQL 8.4), run xtrabackup --backup while the server keeps running, then xtrabackup --prepare the copy so it is consistent. A restore is a file copy: stop MySQL, empty the data directory, run --copy-back, give the files back to the mysql user and start the server. MariaDB uses its own fork, mariadb-backup, with the same three steps and a few different options.

10 min readUpdated Checked against official documentation

When a physical backup beats mysqldump

mysqldump writes SQL. Restoring it makes the server run every statement again and rebuild every index, which is why mysqldump restores get slow on big databases. XtraBackup copies InnoDB's data files while MySQL keeps serving queries, plus the redo log written during the copy. A restore is a file copy, so Percona's docs put the limit at disk or network speed.

mysqldumpXtraBackup or mariadb-backup
CopiesSQL that recreates the dataThe data files, indexes included
RestoreReplays every statement, rebuilds every indexCopy the files back and start the server
Restore ontoAny version, any machineThe same MySQL series (Percona: same engine, often the same minor version)
One table backEasy, it is textPossible, with extra steps
IncrementalNoYes, changed pages only

Many servers run both: XtraBackup for fast full restores, a logical dump for moving between versions. Size the disks first:

Terminal
du -sh /var/lib/mysql

Say it reports 60G: an uncompressed backup needs about 60 GB on the backup disk, and the prepare step works in place. A restore with --copy-back writes another 60 GB into the data directory; --move-back moves the files instead, but the backup is gone afterwards.

Match XtraBackup to your MySQL version

Each XtraBackup series reads one MySQL series. Percona's rule is to use the XtraBackup version equal to the server's major version. Any 8.4 release works with any MySQL 8.4 release, and XtraBackup 8.4 does not back up MySQL 8.0 or 9.x servers. Check the server first:

Terminal
mysql -e "SELECT VERSION()"
ServerXtraBackupPackageRepository command
MySQL 8.0 (Ubuntu 24.04's mysql-server)8.0, marked end of life in Percona's docspercona-xtrabackup-80percona-release setup pxb-80
MySQL 8.4 LTS (Ubuntu 26.04's mysql-server)8.4percona-xtrabackup-84percona-release enable pxb-84-lts
MySQL 9.79.7, a release candidate (9.7.1-rc1) when this was checkedpercona-xtrabackup-97percona-release enable pxb-97-lts
MariaDBNot supported: use mariadb-backup (below)mariadb-backupYour distribution or MariaDB's repository

--no-server-version-check pushes a backup past the version check. Percona lists what can happen next: the backup fails, it is corrupted, or it works. Install the matching series instead.

Install XtraBackup

Run every command in this guide as root (sudo -i): xtrabackup reads the data directory directly. Percona's packages come through its percona-release tool. For MySQL 8.4:

Terminal
curl -O https://repo.percona.com/apt/percona-release_latest.generic_all.deb
Terminal
apt install -y gnupg2 lsb-release ./percona-release_latest.generic_all.deb
Terminal
percona-release enable pxb-84-lts
Terminal
apt install -y percona-xtrabackup-84 zstd
Terminal
xtrabackup --version

zstd decompresses compressed backups later. Percona XtraBackup's four tools are xtrabackup, xbstream (streams), xbcloud (object storage) and xbcrypt (encryption).

Create a backup user

XtraBackup reads the files from disk but also connects to MySQL to take locks and record the binary log position. Percona's minimum grants for full backups:

MySQL prompt
CREATE USER 'bkpuser'@'localhost' IDENTIFIED BY 'long-random-password';
GRANT BACKUP_ADMIN, PROCESS, RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'bkpuser'@'localhost';
GRANT SELECT ON performance_schema.log_status TO 'bkpuser'@'localhost';
GRANT SELECT ON performance_schema.keyring_component_status TO 'bkpuser'@'localhost';
GRANT SELECT ON performance_schema.replication_group_members TO 'bkpuser'@'localhost';
  • BACKUP_ADMIN runs LOCK INSTANCE FOR BACKUP and reads log_status; PROCESS runs SHOW ENGINE INNODB STATUS, which XtraBackup requires.
  • RELOAD and LOCK TABLES cover its flush and lock statements; REPLICATION CLIENT reads the binary log position.
  • The other two SELECT grants let it check the keyring component and Group Replication membership.

Keep the password off the command line, in a file only root can read:

/etc/mysql/xtrabackup.cnf
[xtrabackup]
user=bkpuser
password=long-random-password
Terminal
chmod 600 /etc/mysql/xtrabackup.cnf

xtrabackup reads the [mysqld] and [xtrabackup] groups, so it finds datadir in the server's own config. --defaults-extra-file must come first on the command line.

Take a full backup

Terminal
xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf --backup --parallel=4 --target-dir=/var/backups/mysql/full-2026-10-03
  • --backup copies the data files while MySQL keeps serving reads and writes, plus the redo log written meanwhile.
  • --target-dir is created if missing and must be empty: xtrabackup never overwrites files, it fails with operating system error 17.
  • --parallel=4 copies four files at a time (default 1).
  • --lock-ddl is on by default: inserts and updates carry on, but CREATE, ALTER and DROP wait for the backup lock. From 8.4.0-2, --lock-ddl=REDUCED takes it only after the .ibd files are copied. Keep migrations out of the backup window.

A good run ends with completed OK!. With binary logging on, xtrabackup_binlog_info in the backup records the binlog file and position at the moment of the backup: the starting point for point-in-time recovery. Cancelling is safe; xtrabackup does not modify the database.

backup-my.cnf in the backup is not a copy of your my.cnf. It only holds the InnoDB settings the prepare step needs. Back up /etc/mysql with your file backups.

Prepare the backup

The files were copied at different moments, so InnoDB would refuse them as corrupt. --prepare runs crash recovery on the copy and makes it consistent at a single instant. Do it soon after the backup: it proves the copy works and shortens the restore. (Building incrementals on top? Wait; see below.)

Terminal
xtrabackup --prepare --use-memory=1G --target-dir=/var/backups/mysql/full-2026-10-03
  • --use-memory is the prepare's buffer pool: 100MB by default, 1 to 2 GB recommended by Percona.
  • It doesn't need the MySQL server, so any machine with the same XtraBackup series can do it.
  • Don't interrupt it; an interrupted prepare can leave the backup unusable.
  • XtraBackup 8.4.0-6 and newer accept --check-tables, which validates every index's B-tree during the prepare and exits non-zero if it finds corruption.

Restore a full backup

MySQL must be stopped and the data directory empty. Move the old one aside instead of deleting it: it is your way back, and it holds the binary logs (MySQL writes them into the data directory by default) that you need to replay changes made after the backup.

Terminal
systemctl stop mysql
Terminal
mv /var/lib/mysql /var/lib/mysql.old
Terminal
mkdir /var/lib/mysql
Terminal
xtrabackup --copy-back --target-dir=/var/backups/mysql/full-2026-10-03
Terminal
chown -R mysql:mysql /var/lib/mysql
Terminal
chmod --reference=/var/lib/mysql.old /var/lib/mysql
Terminal
systemctl start mysql
  • --copy-back keeps the backup; --move-back moves the files out of it.
  • Restored files keep the owner of whoever took the backup (root here), so chown hands them back to mysql; chmod --reference copies the old directory's permissions.

If MySQL doesn't start, read /var/log/mysql/error.log (Ubuntu's log_error). Once it runs, count rows in a few important tables and compare them with what you expect.

Compressed and streamed backups

--stream=xbstream sends the whole backup to standard output as one stream, for a single file, another host or object storage. --compress compresses each file with zstd (level 1 by default, --compress-zstd-level up to 19) or lz4 (--compress=lz4), using --compress-threads threads.

Terminal
xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf --backup --stream=xbstream --compress --compress-threads=4 --parallel=4 > /var/backups/mysql/full-2026-10-03.xbstream

Streaming never prepares. To restore, unpack the stream, decompress, then prepare:

Terminal
mkdir -p /var/backups/mysql/restore
Terminal
xbstream -x -C /var/backups/mysql/restore < /var/backups/mysql/full-2026-10-03.xbstream
Terminal
xtrabackup --decompress --target-dir=/var/backups/mysql/restore
Terminal
xtrabackup --prepare --use-memory=1G --target-dir=/var/backups/mysql/restore

--copy-back and --move-back skip compressed files, so a backup restored without --decompress is missing its data. Always decompress, prepare, then restore.

xbcloud put uploads a stream straight to S3 or S3-compatible storage as 10 MB chunks, so the backup never lands on the server's disk. It needs list, put, get and delete on the bucket. Keep the keys in a root-only file (chmod 600):

/etc/mysql/xbcloud.env
AWS_ACCESS_KEY_ID=<access-key-id>
AWS_SECRET_ACCESS_KEY=<secret-access-key>
AWS_DEFAULT_REGION=<region>

For an S3-compatible provider, also set AWS_ENDPOINT. The nightly script below uploads. To restore, load the keys (set -a; . /etc/mysql/xbcloud.env; set +a), download and unpack in one pipe, then decompress and prepare as above:

Terminal
xbcloud get --storage=s3 --s3-bucket=my-mysql-backups full-db1-2026-10-03 | xbstream -x -C /var/backups/mysql/restore

Incremental backups

An incremental backup copies only pages changed since the previous backup, found by comparing log sequence numbers with the xtrabackup_checkpoints file of the backup it builds on. A weekly full and daily incrementals is a common pattern.

Terminal
xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf --backup --target-dir=/var/backups/mysql/base
Terminal
xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf --backup --target-dir=/var/backups/mysql/inc1 --incremental-basedir=/var/backups/mysql/base
Terminal
xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf --backup --target-dir=/var/backups/mysql/inc2 --incremental-basedir=/var/backups/mysql/inc1

To restore, apply the chain to the base in order. Every step except the last uses --apply-log-only, which skips rolling back uncommitted transactions; once they are rolled back, no further increment can be applied.

Terminal
xtrabackup --prepare --apply-log-only --target-dir=/var/backups/mysql/base
Terminal
xtrabackup --prepare --apply-log-only --target-dir=/var/backups/mysql/base --incremental-dir=/var/backups/mysql/inc1
Terminal
xtrabackup --prepare --target-dir=/var/backups/mysql/base --incremental-dir=/var/backups/mysql/inc2

The prepare rewrites the base directory, and Percona doesn't support applying the same incremental directory twice. Prepare a copy of the chain, never your only one. Then restore base as a full backup.

MariaDB: use mariadb-backup

mariadb-backup (formerly mariabackup) began as a fork of XtraBackup 2.3.8; XtraBackup 8.4 supports only MySQL and Percona Server. Install the mariadb-backup package at the same version as the server: MariaDB's example backs up a 10.6 server with the 10.6 mariadb-backup, even before an upgrade.

MariaDB prompt
CREATE USER 'mariadb-backup'@'localhost' IDENTIFIED BY 'long-random-password';
GRANT RELOAD, PROCESS, LOCK TABLES, BINLOG MONITOR ON *.* TO 'mariadb-backup'@'localhost';
/etc/mysql/mariadb-backup.cnf
[mariadb-backup]
user=mariadb-backup
password=long-random-password
Terminal
mariadb-backup --defaults-extra-file=/etc/mysql/mariadb-backup.cnf --backup --target-dir=/var/backups/mariadb/full-2026-10-03
Terminal
mariadb-backup --prepare --target-dir=/var/backups/mariadb/full-2026-10-03

Restore as for MySQL: systemctl stop mariadb, move the data directory aside, mariadb-backup --copy-back --target-dir=..., chown -R mysql:mysql /var/lib/mysql, systemctl start mariadb. Where the two tools differ:

XtraBackupmariadb-backup
Option file group[xtrabackup][mariadb-backup]
Built-in compression--compress (zstd or lz4)--compress is deprecated (QuickLZ). Pipe the stream through gzip or zstd.
Unpack a streamxbstream -xmbstream -x
Incremental prepare--apply-log-only on all but the last stepPlain --prepare each time; --apply-log-only is no longer needed or supported
Checkpoint filextrabackup_checkpointsmariadb_backup_checkpoints after MariaDB 11.0, xtrabackup_checkpoints before

To compress, stream through gzip, then unpack with mbstream and prepare as usual:

Terminal
mariadb-backup --defaults-extra-file=/etc/mysql/mariadb-backup.cnf --backup --stream=xbstream | gzip > /var/backups/mariadb/full-2026-10-03.xb.gz
Terminal
mkdir -p /var/backups/mariadb/restore && cd /var/backups/mariadb/restore && gunzip -c /var/backups/mariadb/full-2026-10-03.xb.gz | mbstream -x

Run it every night

This script streams a compressed full backup of MySQL to S3. set -o pipefail makes a failed xtrabackup fail the script even though xbcloud is last in the pipe.

/usr/local/bin/mysql-xtrabackup.sh
#!/bin/bash
set -euo pipefail
set -a
. /etc/mysql/xbcloud.env
set +a

NAME="full-$(hostname -s)-$(date +%F)"
mkdir -p /var/backups/mysql/tmp

xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf --backup \
  --stream=xbstream --compress --compress-threads=4 --parallel=4 \
  --target-dir=/var/backups/mysql/tmp \
  | xbcloud put --storage=s3 --s3-bucket=my-mysql-backups "$NAME"

echo "$(date -Is) uploaded $NAME"
Terminal
chmod 700 /usr/local/bin/mysql-xtrabackup.sh
/etc/cron.d/mysql-xtrabackup
30 2 * * * root /usr/local/bin/mysql-xtrabackup.sh >> /var/log/mysql-xtrabackup.log 2>&1

Expire old backups with a bucket lifecycle rule (S3 guide); cron schedules covers timing and alerts. Monthly, restore a backup onto a spare server with the same MySQL version (prepare with --check-tables), start it and compare row counts with production. Time it: that is your real restore time (testing restores).

Common errors

ErrorFix
xtrabackup: Error: missing required privilege LOCK TABLES on *.*Grant the privileges listed above to the backup user.
The backup stops at the server version checkThe XtraBackup series doesn't match the server. Install the matching package.
Operating system error 17, file existsThe target directory isn't empty. Use a new directory per backup.
Error 24, too many open filesRaise the open-files limit (ulimit -n, or /etc/security/limits.conf). For mariadb-backup, set open_files_limit in its option group.
[FATAL] InnoDB: An optimized (without redo logging) DDL operation has been performed.An index was built during the backup. Run it again with --lock-ddl left on.
Redo log records overwritten before they were copiedBack up at a quieter time, or add --register-redo-log-consumer so the server keeps redo files until xtrabackup has read them (Percona notes writes block while it waits).
MySQL won't start after a restoreCheck ownership, and that the backup was decompressed and prepared. The error log says which.

Frequently asked questions

Does XtraBackup lock tables?
Not for InnoDB reads and writes; they continue throughout. With the default --lock-ddl, statements such as ALTER TABLE wait until the backup lock is released.
Can I restore an XtraBackup backup onto a newer MySQL version?
Restore it onto the same MySQL series it came from, then upgrade that server. To move data between versions directly, use a logical dump.
Does Percona XtraBackup work with MariaDB?
No. Current XtraBackup releases support MySQL and Percona Server for MySQL. Use mariadb-backup, at the same version as the MariaDB server.
Is Percona XtraBackup free?
Yes. It is open source and installs from Percona's public repository.

How this was checked

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