Deployment guide

Percona XtraBackup

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. There is no read lock and no downtime for the backup window.

On its own, XtraBackup is a command-line tool. This SecureAnyCloud 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 you get:

  • 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/percona holding the database, the backup repository and the archived binary logs
  • Scheduled backups from first boot: 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
  • A loopback-only listener on 3306, not exposed publicly
  • 24/7 SecureAnyCloud support

This is a database product. MySQL listens on 127.0.0.1:3306 only, and the port is not opened to the internet. Connect locally on the VM or over an SSH tunnel.

SecureAnyCloud 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 SecureAnyCloud and choose Create. Select the recommended Standard_B2s size or larger.

Leave the data disk defined by the image in place, because 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 image terms accept --urn sac1647283583153:percona-xtrabackup:default:latest
az vm create \
  --resource-group my-resource-group \
  --name my-xtrabackup-vm \
  --image sac1647283583153: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 and MySQL 8.4. Check both versions and the MySQL listener:

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. Only root can read the credentials file.

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, print 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

This guide passes the password through MYSQL_PWD instead of -p, because this keeps the password out of the process table.

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

systemd timers drive the backups, not cron. A full backup runs weekly and an incremental runs daily. Both timers are Persistent=true, so a VM that was deallocated over its scheduled window runs the job at the next boot instead of 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 backup 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/

The 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 with 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 backup.

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. This 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 and takes an incremental. It then restores twice into a scratch directory, starting a second MySQL server against each restored copy and comparing row counts and table checksums.

Run the self-test:

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. The two restores land on different row counts, which proves that the incremental carried the later changes rather than the full backup 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 differ on your VM. The overall shape must always hold. 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 with 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. 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

To restore just the full backup without its incrementals, add --base-only:

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 it opens the log:

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 command stops any inspection server left running from a previous attempt. This matters because a running server holds a lock on the restored redo log files, so a later restore into the same directory cannot overwrite them.

--mysqlx=OFF is required so that the second server does not collide with the live one on port 33060. --defaults-file keeps the second server from inheriting the live server's datadir, socket and binary log settings.

Query the restored copy, then shut the inspection server 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, restore into /srv/percona/mysql, then start MySQL 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

The 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. List the archive, then replay from the recorded position up to the moment you want:

sudo ls -1 /srv/percona/binlog-archive | head -5

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 backup always deletes its incrementals, because an orphaned incremental cannot be restored.
  • Adjust the schedule by editing xtrabackup-full.timer and xtrabackup-incremental.timer, then run sudo systemctl daemon-reload.
  • Adjust retention with RETAIN_FULLS in /usr/local/lib/xtrabackup-common.sh.
  • For off-box durability, copy /srv/percona/backups to Azure Blob Storage, or stream backups directly with xbcloud. A backup that lives only on the machine it protects is not a disaster recovery plan.
  • unattended-upgrades applies security updates automatically, including updates from 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

SecureAnyCloud publishes this image and provides 24/7 support. Percona XtraBackup and MySQL Community Server are open source software distributed under the GNU General Public License version 2.

Deploy on Azure

Launch Percona XtraBackup 8.4 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure