Percona XtraBackup 8.4 on Ubuntu 24.04 LTS on Azure User Guide
Overview
Percona XtraBackup is an open source, GPL-2.0 licensed tool that takes hot physical backups of a running MySQL database. It copies InnoDB data files while the server keeps serving traffic, then replays the redo log so the copy is transactionally consistent, so there is no read lock and no downtime for the backup window.
XtraBackup on its own is a command line tool. This cloudimg image is a complete MySQL backup appliance: XtraBackup 8.4 is installed alongside the MySQL Community Server 8.4 LTS database it protects, already wired to a scheduled backup regime, with a documented and proven restore path.
What is included:
- Percona XtraBackup 8.4 (GPL-2.0) from the official Percona repository
- MySQL Community Server 8.4 LTS (GPL-2.0 with the Universal FOSS Exception)
- A dedicated 64 GiB Azure data disk at
/srv/perconaholding the database, the backup repository and the archived binary logs - Scheduled backups out of the box: a weekly full backup and a daily incremental, with retention pruning
xtrabackup-selftest.sh, which proves a restore end to end by rebuilding the database in a scratch directory and comparing row counts and checksums- A least privilege backup user, separate from
root - Unique per VM credentials generated on first boot, with no default login
- Loopback only listener on
3306, not exposed publicly - 24/7 cloudimg support
This is a database product: MySQL listens on 127.0.0.1:3306 only. The port is not opened to the internet. Connect locally on the VM or over an SSH tunnel.
cloudimg is not affiliated with, endorsed by, or sponsored by Percona. "Percona" and "XtraBackup" are trademarks of Percona LLC, used here nominatively to identify the open source software included in this image.
Prerequisites
- An Azure subscription with permission to create virtual machines
- An SSH key pair
- The Azure CLI, if you deploy from the command line
Step 1 - Deploy from the Azure Marketplace
In the Azure portal, search the Marketplace for Percona XtraBackup 8.4 on Ubuntu 24.04 LTS by cloudimg, choose Create, and select the recommended Standard_B2s size or larger. Leave the data disk defined by the image in place: it carries the database and the backup repository.
Allow inbound SSH (22) only. Do not open 3306.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group my-resource-group \
--name my-xtrabackup-vm \
--image cloudimg:percona-xtrabackup:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
Step 3 - Connect to your VM
ssh azureuser@<vm-ip>
Step 4 - Confirm MySQL and XtraBackup are installed and version matched
XtraBackup must match the major version of the server it backs up. This image pins a matched pair: XtraBackup 8.4 against MySQL 8.4.
mysqld --version
xtrabackup --version 2>&1 | grep -m1 'xtrabackup version'
systemctl is-active mysql.service
ss -tln | grep ':3306'
Expected output:
/usr/sbin/mysqld Ver 8.4.10 for Linux on x86_64 (MySQL Community Server - GPL)
xtrabackup version 8.4.0-6 based on MySQL server 8.4.0 Linux (x86_64) (revision id: 46a895ed)
active
LISTEN 0 100 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 70 127.0.0.1:33060 0.0.0.0:*
Both listeners are bound to 127.0.0.1, never 0.0.0.0, so the database is not reachable from the network.

Step 5 - Retrieve your per VM credentials
Credentials are generated on first boot and are unique to your VM. There is no default password. The file is readable only by root.
sudo grep -E '^(MYSQL_USERNAME|MYSQL_HOST|MYSQL_PORT|XTRABACKUP_USER)=' /root/percona-xtrabackup-credentials.txt
Expected output:
MYSQL_USERNAME=root
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
XTRABACKUP_USER=xtrabackup
To see the passwords themselves, read the whole file:
sudo cat /root/percona-xtrabackup-credentials.txt
The file contains two secrets: MYSQL_ROOT_PASSWORD for the database administrator, and XTRABACKUP_PASSWORD for the dedicated backup user. Backups never run as root.
Step 6 - Connect with the mysql client
MYSQL_PWD keeps the password out of the process table, which is why it is used instead of -p throughout this guide.
MYSQL_PWD='<MYSQL_ROOT_PASSWORD>' mysql -h127.0.0.1 -uroot -e "SELECT VERSION(); SHOW DATABASES; SELECT * FROM demo.items;"
Expected output:
VERSION()
8.4.10
Database
demo
information_schema
mysql
performance_schema
sys
id name qty created_at
1 widgets 10 2026-07-26 22:03:13
2 gadgets 25 2026-07-26 22:03:13
3 sprockets 7 2026-07-26 22:03:13
The demo database is seeded on first boot so there is data to back up and restore immediately.

Step 7 - Review the backup schedule
Backups are driven by systemd timers, not cron. A full backup runs weekly and an incremental runs daily. Both are Persistent=true, so a VM that was deallocated over its scheduled window runs the job at the next boot instead of silently skipping it.
systemctl list-timers 'xtrabackup-*' --no-pager
Expected output:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-07-27 01:09:43 UTC 3h 5min - - xtrabackup-incremental.timer xtrabackup-incremental.service
Sun 2026-08-02 01:05:24 UTC 6 days - - xtrabackup-full.timer xtrabackup-full.service
2 timers listed.
The two jobs are serialised with a lock, so a full and an incremental can never overlap.
Step 8 - Take a full backup on demand
sudo systemctl start xtrabackup-full.service
ls -1 /srv/percona/backups/
Expected output is one generation directory per full backup, named by UTC timestamp:
full-20260726T220320Z
Each generation holds the full backup in base/ plus every incremental taken against it:
sudo ls -1 /srv/percona/backups/*/
base
inc-20260726T220325Z
Backups live on the dedicated data disk and are mode 0700, owned by mysql. They contain full table data, so they are never world readable.

Step 9 - Take an incremental backup
An incremental copies only the pages that changed since the previous backup in the chain, so it is much smaller and faster than a full.
sudo systemctl start xtrabackup-incremental.service
sudo ls -1 /srv/percona/backups/*/
Each incremental records the log sequence number it starts from, and that value matches the previous backup's end point exactly. That is what makes the chain restorable.
Step 10 - Prove the restore
A backup you have never restored is not a proven backup. This image ships a self test that performs a complete round trip on the running VM: it seeds a table, takes a full backup, writes more rows, takes an incremental, then restores twice into a scratch directory, starting a second MySQL server against each restored copy and comparing row counts and table checksums.
sudo /usr/local/sbin/xtrabackup-selftest.sh
Expected output:
== Percona XtraBackup appliance self-test ==
OK: per-VM secrets present (root 32 chars, backup user 32 chars), 0600 root:root
OK: positive control: 10.0.0.22:22 accepts connections (the address IS routable and reachable)
OK: negative: 10.0.0.22:3306 is refused - and the control above proves that is the bind, not the network
OK: MySQL answers on 127.0.0.1:3306 only
OK: per-VM root password authenticates
OK: wrong root passwords are rejected (two independent negative controls)
OK: positive auth still works after the negative controls (so the rejections were genuine)
OK: xtrabackup --check-privileges is satisfied for the least-privilege backup user
OK: backup user is least-privilege (no ALL PRIVILEGES)
== round trip: seed -> full -> more rows -> incremental -> restore x2 -> compare ==
OK: point in time A: 3 rows, checksum 2289224306
OK: full backup taken: full-20260726T220320Z/base
OK: point in time B: 7 rows, checksum 1984645744
OK: incremental taken and its from_lsn (19428310) chains exactly off the base to_lsn
OK: RESTORE 1 (base only) reproduced point in time A exactly: 3 rows, checksum 2289224306
OK: RESTORE 2 (base + incremental) reproduced point in time B exactly: 7 rows, checksum 1984645744
OK: backup schedule armed: both timers enabled AND active with a next elapse
OK: PITR metadata present: backup stops at binlog.000003 @ 158, 5 binlog file(s) archived
OK: backup repository is 0700 and contains no world-readable paths
SELFTEST_OK
The two restores are the important part. Restoring the base alone reproduces the earlier point in time, and restoring base plus incremental reproduces the later one. Because the two land on different row counts, the incremental is proven to have carried the later changes rather than the full simply being replayed twice.
The self test seeds fresh data on every run, so the checksums, log sequence numbers, generation timestamps and binary log positions above will differ on your VM. What must always hold is the shape: every line reports OK, the two restores match their respective points in time, and the run ends with SELFTEST_OK. Any problem is reported as a single BROKEN-<reason> line and a non zero exit status, so the self test is safe to run from monitoring.

Step 11 - Restore a backup yourself
xtrabackup-restore.sh prepares a backup generation and restores it into a target directory. It copies the generation before preparing it, because --prepare rewrites a backup in place and is not repeatable, so your stored backup is never consumed.
Restore the newest generation into a scratch directory:
GEN=$(sudo find /srv/percona/backups -maxdepth 1 -type d -name 'full-*' | sort | tail -1)
sudo /usr/local/sbin/xtrabackup-restore.sh "$GEN" /srv/percona/restore/manual
sudo ls -1 /srv/percona/restore/manual | head -10
Add --base-only to restore just the full backup without its incrementals:
GEN=$(sudo find /srv/percona/backups -maxdepth 1 -type d -name 'full-*' | sort | tail -1)
sudo /usr/local/sbin/xtrabackup-restore.sh "$GEN" /srv/percona/restore/manual --base-only
To bring a restored copy online for inspection without touching your live database, start a second MySQL server against it on another port.
Start the inspection server. The error log must exist and be owned by mysql, because MySQL drops privileges before opening it:
sudo install -d -o mysql -g mysql -m 0750 /srv/percona/run /srv/percona/log
sudo bash -c '[ -f /srv/percona/run/manual.pid ] && kill "$(cat /srv/percona/run/manual.pid)" 2>/dev/null; sleep 3; rm -f /srv/percona/run/manual.sock /srv/percona/run/manual.pid; true'
sudo install -o mysql -g mysql -m 0640 /dev/null /srv/percona/log/manual.log
sudo /usr/sbin/mysqld --defaults-file=/etc/mysql/restore-verify.cnf \
--datadir=/srv/percona/restore/manual --port=3307 \
--socket=/srv/percona/run/manual.sock --pid-file=/srv/percona/run/manual.pid \
--log-error=/srv/percona/log/manual.log --user=mysql --mysqlx=OFF --skip-log-bin --daemonize
The second line stops any inspection server left running from a previous attempt. That matters: while one is running it holds a lock on the restored redo log files, so a later restore into the same directory will not be able to overwrite them.
--mysqlx=OFF is required so the second server does not collide with the live one on port 33060, and --defaults-file keeps it from inheriting the live server's datadir, socket and binary log settings.
Now query the restored copy, then shut it down:
sudo MYSQL_PWD='<MYSQL_ROOT_PASSWORD>' mysql --socket=/srv/percona/run/manual.sock -uroot -e 'SELECT COUNT(*) AS restored_rows FROM demo.items;'
sudo MYSQL_PWD='<MYSQL_ROOT_PASSWORD>' mysqladmin --socket=/srv/percona/run/manual.sock -uroot shutdown
To replace your live database with a restored copy, stop MySQL first, restore into /srv/percona/mysql, then start it again. The target directory must be empty before a restore, and the restored files must be owned by mysql.
Step 12 - Point in time recovery
A physical backup restores you to the moment the backup completed. To roll forward past that moment, replay the binary logs.
Every backup records exactly where it stopped:
sudo cat /srv/percona/backups/*/base/xtrabackup_binlog_info
Expected output is the binary log file and position at the backup's consistent point:
binlog.000003 158
Binary logs are archived to /srv/percona/binlog-archive alongside each backup, because a physical restore does not include them. Replay from that position up to the moment you want:
sudo ls -1 /srv/percona/binlog-archive | head -5
Then, against a restored copy, apply the logs with mysqlbinlog, starting at the recorded position and stopping at your chosen time. List the files in order beginning with the one named in xtrabackup_binlog_info, because --start-position applies to the first file only.
Maintenance
- Backups are pruned automatically: the two most recent full generations are kept, each with its whole incremental chain. Deleting a full always deletes its incrementals, because an orphaned incremental cannot be restored.
- Adjust the schedule by editing
xtrabackup-full.timerandxtrabackup-incremental.timer, then runsudo systemctl daemon-reload. - Adjust retention with
RETAIN_FULLSin/usr/local/lib/xtrabackup-common.sh. - For off box durability, copy
/srv/percona/backupsto Azure Blob Storage, or stream backups directly withxbcloud. A backup that lives only on the machine it protects is not a disaster recovery plan. - Security updates are applied automatically by
unattended-upgrades, including the MySQL and Percona repositories. - The database and all backups live on the data disk at
/srv/percona. Grow that disk in the Azure portal as your data grows.
Support
This image is published and supported by cloudimg with 24/7 support. Percona XtraBackup and MySQL Community Server are open source software distributed under the GNU General Public License version 2.