Percona XtraBackup copies the InnoDB data files of a running MySQL server without blocking reads or writes, so you can back up a production database without downtime. Unlike mysqldump, it produces a physical copy that restores in minutes even for databases of hundreds of gigabytes. In this tutorial you will install XtraBackup on Ubuntu 24.04, take full, incremental and compressed backups, stream a backup to another server over SSH, restore it, replay binary logs for point-in-time recovery, and schedule nightly backups with a systemd timer.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS with MySQL 8.0 from the Ubuntu repositories (sudo apt install mysql-server), for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • Free disk space for at least one full copy of the MySQL data directory. Check its size with sudo du -sh /var/lib/mysql.
  • Optional: a second server reachable over SSH to receive streamed backups.

Confirm the MySQL version first:

mysql --version
mysql  Ver 8.0.43-0ubuntu0.24.04.1 for Linux on x86_64 ((Ubuntu))

Step 1 - Installing Percona XtraBackup

XtraBackup is distributed through the Percona repository, which is managed with the percona-release helper. Download and install the helper package:

cd /tmp
curl -fsSLO https://repo.percona.com/apt/percona-release_latest.generic_all.deb
sudo apt install -y gnupg2 lsb-release ./percona-release_latest.generic_all.deb

Enable only the tools repository, so apt does not start offering Percona Server packages as replacements for your MySQL server:

sudo percona-release enable-only tools release
sudo apt update

Install XtraBackup 8.0 together with zstd, which is needed to decompress compressed backups:

sudo apt install -y percona-xtrabackup-80 zstd

Verify the installation:

xtrabackup --version
xtrabackup version 8.0.43-37 based on MySQL server 8.0.43 Linux (x86_64) (revision id: ...)

The exact numbers depend on the current releases. The XtraBackup version should be equal to or newer than your MySQL server version. If MySQL was upgraded to a newer 8.0 release than XtraBackup supports, the backup refuses to start, so keep both packages updated together.

Step 2 - Creating a backup user and credentials file

XtraBackup connects to MySQL to coordinate the backup, so it needs a dedicated account with the minimum privileges Percona documents for MySQL 8.0. Open the MySQL shell as root (Ubuntu authenticates the root account through the system socket):

sudo mysql

Create the user, replacing your_strong_password with a long random password:

CREATE USER 'bkpuser'@'localhost' IDENTIFIED BY 'your_strong_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';
EXIT;

Passing a password on the command line exposes it in the process list and shell history. Store it in an option file readable only by root instead:

sudo nano /etc/mysql/xtrabackup.cnf
[xtrabackup]
user=bkpuser
password=your_strong_password

Restrict the permissions and create the backup directory:

sudo chmod 600 /etc/mysql/xtrabackup.cnf
sudo mkdir -p /backup/mysql
sudo chmod 700 /backup/mysql

You will pass this file with --defaults-extra-file, which must always be the first option on the xtrabackup command line.

Step 3 - Taking and preparing a full backup

Run a full backup. --parallel copies several data files at once; four threads is a reasonable start on a 4-vCPU server:

sudo xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf \
  --backup --parallel=4 \
  --target-dir=/backup/mysql/full

The target directory must be empty or not exist. The last line of the output confirms success:

...
2026-09-25T02:00:41.512 0 [Note] [MY-011825] [Xtrabackup] Transaction log of lsn (20157034) to (20157044) was copied.
2026-09-25T02:00:41.731 0 [Note] [MY-011825] [Xtrabackup] completed OK!

A raw backup is not consistent yet: it contains data files copied at different moments plus the redo log written during the copy. The prepare phase replays that log so the files become consistent:

sudo xtrabackup --prepare --target-dir=/backup/mysql/full

Check the checkpoint file to confirm the backup is prepared:

sudo cat /backup/mysql/full/xtrabackup_checkpoints
backup_type = full-prepared
from_lsn = 0
to_lsn = 20157034
last_lsn = 20157044
flushed_lsn = 20157044

Only prepared backups can be restored. You can prepare right after the backup, or keep the raw copy and prepare it at restore time.

Step 4 - Taking incremental backups

An incremental backup copies only the pages that changed since a previous backup, using the log sequence number (LSN) recorded in xtrabackup_checkpoints. Start from a new, unprepared full backup as the base:

sudo xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf \
  --backup --parallel=4 --target-dir=/backup/mysql/base

Later, take the first incremental against the base:

sudo xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf \
  --backup --parallel=4 \
  --target-dir=/backup/mysql/inc1 \
  --incremental-basedir=/backup/mysql/base

Each following incremental uses the previous one as its base:

sudo xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf \
  --backup --parallel=4 \
  --target-dir=/backup/mysql/inc2 \
  --incremental-basedir=/backup/mysql/inc1

Verify the chain: the from_lsn of each incremental must equal the to_lsn of the backup before it:

sudo grep -H lsn /backup/mysql/{base,inc1,inc2}/xtrabackup_checkpoints | grep -E 'from_lsn|to_lsn'

Preparing an incremental chain

Incrementals are merged into the base in order. Every step except the last uses --apply-log-only, which keeps uncommitted transactions pending so the next incremental can still be applied:

sudo xtrabackup --prepare --apply-log-only --target-dir=/backup/mysql/base
sudo xtrabackup --prepare --apply-log-only --target-dir=/backup/mysql/base \
  --incremental-dir=/backup/mysql/inc1
sudo xtrabackup --prepare --target-dir=/backup/mysql/base \
  --incremental-dir=/backup/mysql/inc2

After the final command, /backup/mysql/base holds a prepared backup equivalent to the moment inc2 was taken, and backup_type in its checkpoint file reads full-prepared. Preparing modifies the base, so copy it first if you want to keep applying future incrementals to the original.

Step 5 - Compressing a backup

--compress compresses each file with zstd as it is copied, which typically shrinks InnoDB data by 50 to 80 percent:

sudo xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf \
  --backup --compress --compress-threads=4 --parallel=4 \
  --target-dir=/backup/mysql/compressed

Files in the target directory now end in .zst. A compressed backup must be decompressed before it can be prepared. --remove-original deletes the .zst files once they are extracted:

sudo xtrabackup --decompress --remove-original --parallel=4 \
  --target-dir=/backup/mysql/compressed
sudo xtrabackup --prepare --target-dir=/backup/mysql/compressed

Step 6 - Streaming a backup to another server

With --stream=xbstream, XtraBackup writes the backup to standard output instead of a directory, so you can send it to another host without using local disk space. The receiving server needs the xbstream tool, which comes with the percona-xtrabackup-80 package, so install XtraBackup there as in Step 1.

Because the command runs under sudo, SSH uses root's keys. Make sure /root/.ssh has a key authorized on the backup host, then run:

sudo xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf \
  --backup --stream=xbstream --compress --parallel=4 --target-dir=/tmp \
  | sudo ssh your_user@backup_host "mkdir -p /backup/db1 && xbstream -x -C /backup/db1"

Replace your_user and backup_host with the account and address of the receiving server. On the backup host, decompress and prepare it as shown in Step 5, then check xtrabackup_checkpoints.

To keep a single archive file instead, redirect the stream to a file and extract it later with xbstream -x -C /restore/dir < backup.xbstream.

Step 7 - Restoring a backup

Restoring replaces the whole MySQL data directory, so the server must be stopped. Stop MySQL and move the current data directory aside instead of deleting it:

sudo systemctl stop mysql
sudo mv /var/lib/mysql /var/lib/mysql.old

Copy the prepared backup back into place and fix ownership:

sudo xtrabackup --copy-back --datadir=/var/lib/mysql --target-dir=/backup/mysql/full
sudo chown -R mysql:mysql /var/lib/mysql

Start MySQL and check that your databases are back:

sudo systemctl start mysql
sudo mysql -e "SHOW DATABASES;"

If the service fails to start, read the error with sudo journalctl -u mysql -n 50. Once you have confirmed the restore, you can delete /var/lib/mysql.old, but keep it until you finish the next step if you need point-in-time recovery.

Step 8 - Recovering to a point in time with binary logs

A backup restores the database to the moment it was taken. To recover up to just before an incident (for example an accidental DROP TABLE at 14:30), replay the binary logs written after the backup. MySQL 8.0 enables binary logging by default and keeps the files, named binlog.000001 and so on, in the data directory for 30 days.

The backup records its binary log position:

sudo cat /backup/mysql/full/xtrabackup_binlog_info
binlog.000012	157

After restoring (Step 7), the binary logs from before the restore are in /var/lib/mysql.old. List them to see which files came after the backup:

sudo ls /var/lib/mysql.old/binlog.*

Replay events from the recorded position up to the time just before the incident. --start-position applies to the first file listed:

sudo mysqlbinlog --start-position=157 --stop-datetime="2026-09-25 14:29:59" \
  /var/lib/mysql.old/binlog.000012 /var/lib/mysql.old/binlog.000013 | sudo mysql

Verify that the data written after the backup is present and the dropped table exists again. If you use GTID-based replication, read the GTID set from xtrabackup_binlog_info as well and follow the MySQL documentation for GTID-aware replay.

Step 9 - Automating nightly backups

A small script run by a systemd timer is enough for a nightly compressed full backup with a retention period. Create the script:

sudo nano /usr/local/sbin/mysql-xtrabackup.sh
#!/usr/bin/env bash
set -euo pipefail

BACKUP_ROOT="/backup/mysql"
RETAIN_DAYS=7
TARGET="${BACKUP_ROOT}/full_$(date +%Y%m%d_%H%M%S)"

xtrabackup --defaults-extra-file=/etc/mysql/xtrabackup.cnf \
  --backup --compress --compress-threads=4 --parallel=4 \
  --target-dir="${TARGET}"

# Delete backups older than the retention period
find "${BACKUP_ROOT}" -mindepth 1 -maxdepth 1 -type d -name 'full_*' \
  -mtime +"${RETAIN_DAYS}" -exec rm -rf -- {} +

Because of set -e, a failed backup stops the script before old backups are removed. Make it executable:

sudo chmod 750 /usr/local/sbin/mysql-xtrabackup.sh

Create a service unit:

sudo nano /etc/systemd/system/mysql-xtrabackup.service
[Unit]
Description=Nightly MySQL backup with Percona XtraBackup
After=mysql.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/mysql-xtrabackup.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

Create the timer that runs it every night at 02:00:

sudo nano /etc/systemd/system/mysql-xtrabackup.timer
[Unit]
Description=Run MySQL XtraBackup nightly

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=10m

[Install]
WantedBy=timers.target

Enable the timer and run the service once to test it:

sudo systemctl daemon-reload
sudo systemctl enable --now mysql-xtrabackup.timer
sudo systemctl start mysql-xtrabackup.service
sudo journalctl -u mysql-xtrabackup.service -n 5
Sep 25 10:12:03 db1 mysql-xtrabackup.sh[4127]: ... [Xtrabackup] completed OK!
Sep 25 10:12:03 db1 systemd[1]: mysql-xtrabackup.service: Deactivated successfully.

Check the next scheduled run with systemctl list-timers mysql-xtrabackup.timer. Local backups do not protect against losing the server, so copy /backup/mysql to another location (a second server or object storage) as part of the same routine.

Troubleshooting

Access denied for user 'bkpuser'@'localhost': check that --defaults-extra-file is the first option and that the file contains the correct password. Confirm the grants with sudo mysql -e "SHOW GRANTS FOR 'bkpuser'@'localhost';".

Unsupported server version or a message that the server is newer than XtraBackup: MySQL was upgraded past the version your XtraBackup release supports. Run sudo apt update && sudo apt install --only-upgrade percona-xtrabackup-80.

Original data directory /var/lib/mysql is not empty! during copy-back: --copy-back never overwrites files. Move the existing directory aside as shown in Step 7.

Incremental prepare fails with an LSN error: the chain is broken or a middle step was prepared without --apply-log-only. Compare the from_lsn and to_lsn values of each backup and rebuild the chain from a fresh base backup.

Backups slow down production: lower --parallel, and keep the Nice and IOSchedulingPriority settings in the service unit so the backup yields to MySQL under load.

Conclusion

You now have XtraBackup taking consistent, non-blocking backups of MySQL 8.0, with full, incremental and compressed variants, streaming to a remote host, a tested restore procedure, binary log replay for point-in-time recovery and a nightly systemd timer. As next steps, copy backups off-site automatically, schedule a periodic restore test on a separate server, and add alerting when mysql-xtrabackup.service fails.