Databases Azure

Percona XtraBackup 8.4 on Ubuntu 24.04 LTS on Azure User Guide

| Product: Percona XtraBackup 8.4 on Ubuntu 24.04 LTS on Azure

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/percona holding 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.

MySQL and Percona XtraBackup versions with the loopback only listener

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.

MySQL version, database list and seeded demo data

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.

Scheduled backup timers and the backup repository on the data disk

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.

The self test restoring to two verified points in time

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.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.
  • 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.