Set up automated MariaDB Galera cluster backups with compression

Intermediate 40 min Sep 23, 2026 159 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Learn how to build a cluster-safe backup strategy for MariaDB Galera using Mariabackup, compression, systemd timers and remote storage offloading, with verification and node restore procedures.

Prerequisites

  • A running MariaDB Galera cluster with at least one accessible node
  • Root or sudo access on the backup node
  • Basic familiarity with systemd units and shell scripting
  • An S3-compatible storage bucket for offsite backups (optional)

What this solves

Galera cluster nodes share the same dataset through synchronous replication, but that does not mean backups are optional. A bad schema migration, an accidental DELETE, or a corrupted table replicates to every node instantly. This tutorial builds a repeatable, compressed, automated backup strategy using Mariabackup, with rotation, integrity checks and offsite storage.

Note: This tutorial assumes you already have a working Galera cluster. If you need to set one up first, see Configure MariaDB Galera cluster for multi-master replication.

Choosing a backup strategy for Galera clusters

mysqldump produces logical SQL dumps. It is portable and human-readable, but slow to restore on large datasets and it locks tables during the dump unless you use single-transaction mode, which does not fully guarantee consistency on a busy Galera node.

Mariabackup performs physical, hot backups of InnoDB data files without blocking writes. It understands Galera's gtid_binlog_state and wsrep_position, which lets a restored node rejoin the cluster cleanly via State Snapshot Transfer (SST) or by seeding a fresh node. For clusters beyond a few gigabytes, Mariabackup is the right default.

CriteriaMariabackupmysqldump
Backup speed on large datasetsFast, file-level copySlow, row-by-row export
Restore speedFast, direct file copySlow, replays SQL statements
Locking impact on clusterNone, uses InnoDB redo log trackingCan lock tables briefly
Selective table/database restoreHarder, requires partial backup modeEasy, just re-import specific tables
Best use caseFull cluster or node backupsSchema migrations, small exports, dev seeding

In practice, most production Galera setups run Mariabackup as the primary backup mechanism and keep mysqldump available for occasional logical exports of specific schemas.

Step-by-step configuration

Install Mariabackup

Mariabackup ships as part of the MariaDB backup package. Install it on the node you plan to run backups from, ideally a non-primary node to avoid extra load on the write path.

sudo apt update
sudo apt install -y mariadb-backup gzip zstd
sudo dnf install -y MariaDB-backup gzip zstd

Create a dedicated backup user

Avoid using the root MariaDB account in scripts. Create a user scoped to only the privileges Mariabackup needs.

sudo mysql -u root -p
CREATE USER 'backupuser'@'localhost' IDENTIFIED BY 'Kx9#mPz2Trq!Lw7v';
GRANT RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT, BACKUP_ADMIN ON *.* TO 'backupuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Warning: Use a strong, unique password and store it outside the script itself using a credentials file with restricted permissions, shown in the next step.

Store credentials securely

Create a MariaDB option file readable only by the backup script's user, instead of embedding the password in the script or command history.

sudo mkdir -p /etc/mysql/backup
sudo tee /etc/mysql/backup/.mycnf > /dev/null <<'EOF'
[client]
user=backupuser
password=Kx9#mPz2Trq!Lw7v
EOF
sudo chown root:root /etc/mysql/backup/.mycnf
sudo chmod 600 /etc/mysql/backup/.mycnf

Setting permissions to 600 means only the file owner, root in this case, can read or write it. This prevents other local users from reading the database password.

Create the backup directory structure

Separate raw backups from compressed archives, and keep a dedicated log directory.

sudo mkdir -p /var/backups/mariadb/{staging,archives,logs}
sudo chown -R mysql:mysql /var/backups/mariadb
sudo chmod 750 /var/backups/mariadb
sudo chmod 750 /var/backups/mariadb/staging /var/backups/mariadb/archives /var/backups/mariadb/logs

The mysql system user needs to read and write here since Mariabackup runs under that account during the backup. 750 permissions allow the owner full access and the group read/execute, while blocking all other users.

Write the backup script with compression and rotation

This script takes a full Mariabackup snapshot, streams it through zstd for fast compression, timestamps the archive, and deletes archives older than the retention window.

#!/bin/bash
set -euo pipefail

BACKUP_USER_CNF="/etc/mysql/backup/.mycnf"
STAGING_DIR="/var/backups/mariadb/staging"
ARCHIVE_DIR="/var/backups/mariadb/archives"
LOG_DIR="/var/backups/mariadb/logs"
RETENTION_DAYS=7
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_NAME="galera-backup-${TIMESTAMP}"
LOG_FILE="${LOG_DIR}/${BACKUP_NAME}.log"

mkdir -p "${STAGING_DIR}/${BACKUP_NAME}"

echo "[$(date)] Starting Mariabackup for ${BACKUP_NAME}" >> "${LOG_FILE}"

mariabackup --defaults-extra-file="${BACKUP_USER_CNF}" \
  --backup \
  --target-dir="${STAGING_DIR}/${BACKUP_NAME}" \
  >> "${LOG_FILE}" 2>&1

mariabackup --defaults-extra-file="${BACKUP_USER_CNF}" \
  --prepare \
  --target-dir="${STAGING_DIR}/${BACKUP_NAME}" \
  >> "${LOG_FILE}" 2>&1

echo "[$(date)] Compressing backup with zstd" >> "${LOG_FILE}"
tar -cf - -C "${STAGING_DIR}" "${BACKUP_NAME}" | zstd -T0 -19 -o "${ARCHIVE_DIR}/${BACKUP_NAME}.tar.zst"

rm -rf "${STAGING_DIR}/${BACKUP_NAME}"

echo "[$(date)] Rotating archives older than ${RETENTION_DAYS} days" >> "${LOG_FILE}"
find "${ARCHIVE_DIR}" -name "galera-backup-*.tar.zst" -mtime +${RETENTION_DAYS} -delete
find "${LOG_DIR}" -name "galera-backup-*.log" -mtime +${RETENTION_DAYS} -delete

echo "[$(date)] Backup ${BACKUP_NAME} completed successfully" >> "${LOG_FILE}"

zstd at level 19 with multi-threading (-T0) gives strong compression without the long runtimes of gzip -9 on multi-gigabyte datasets. If you prefer gzip for compatibility with older tooling, replace the compression line with tar -czf "${ARCHIVE_DIR}/${BACKUP_NAME}.tar.gz" -C "${STAGING_DIR}" "${BACKUP_NAME}".

Set correct ownership and permissions on the script

The script needs to run as the mysql user since that account owns the data directory and staging path. It should not be world-writable.

sudo chown root:mysql /usr/local/bin/galera-backup.sh
sudo chmod 750 /usr/local/bin/galera-backup.sh
Never use chmod 777. It gives every user on the system full read, write and execute access to your backup script, including the ability to modify it to exfiltrate credentials or delete backups. Ownership by root with group execute for mysql, combined with 750 permissions, is the minimum access needed.

Test the script manually

Run it once by hand before automating it, so you can confirm output and catch permission errors early.

sudo -u mysql /usr/local/bin/galera-backup.sh
ls -lh /var/backups/mariadb/archives

Automating backups with systemd timers

systemd timers give you better logging, dependency management and failure visibility than plain cron, and they integrate cleanly with journalctl. If you already manage scheduled jobs with cron elsewhere, the same rotation principles apply, see Implement backup rotation policies with automated cleanup.

Create the systemd service unit

This defines what runs, and as which user.

[Unit]
Description=Galera cluster backup with Mariabackup
After=mariadb.service
Requires=mariadb.service

[Service]
Type=oneshot
User=mysql
Group=mysql
ExecStart=/usr/local/bin/galera-backup.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

Nice and IOSchedulingClass keep the backup from starving the mysqld process of CPU and disk I/O during peak hours.

Create the systemd timer unit

This schedules the service to run nightly at 02:15, with a randomized delay to avoid every cluster node hitting disk I/O at the exact same second.

[Unit]
Description=Run Galera backup nightly

[Timer]
OnCalendar=*-*-* 02:15:00
RandomizedDelaySec=600
Persistent=true

[Install]
WantedBy=timers.target

Enable and start the timer

sudo systemctl daemon-reload
sudo systemctl enable --now galera-backup.timer
systemctl list-timers galera-backup.timer

Stagger schedules across cluster nodes

Running full backups on every node simultaneously wastes disk I/O and network bandwidth for no benefit, since the data is identical. Pick one designated backup node per cluster, and only enable the timer there. Keep the package installed on other nodes as a manual fallback.

sudo systemctl disable --now galera-backup.timer

Run this on the nodes that should not perform scheduled backups. If your designated backup node goes down, you can enable the timer on a secondary node temporarily.

Verifying backup integrity and restoring a node

Verify archive integrity

Confirm the compressed archive is not truncated or corrupted before you rely on it.

zstd -t /var/backups/mariadb/archives/galera-backup-20250601-021500.tar.zst
echo $?

An exit code of 0 means the archive passed integrity checks. Any other value means the file is corrupted and you should investigate the backup job's logs.

Extract and prepare for restore

Decompress into a scratch location, separate from the live data directory.

mkdir -p /tmp/restore-test
zstd -d /var/backups/mariadb/archives/galera-backup-20250601-021500.tar.zst -o /tmp/restore-test/backup.tar
tar -xf /tmp/restore-test/backup.tar -C /tmp/restore-test

Restore a failed or new node

Stop MariaDB, clear the existing data directory and copy the prepared backup into place. Do this only on a node you are rebuilding, never on a healthy node still serving traffic.

sudo systemctl stop mariadb
sudo mv /var/lib/mysql /var/lib/mysql.bak
sudo mkdir /var/lib/mysql
sudo mariabackup --copy-back --target-dir=/tmp/restore-test/galera-backup-20250601-021500
sudo chown -R mysql:mysql /var/lib/mysql
sudo chmod 750 /var/lib/mysql
Warning: Do not run this against a node that is currently part of an active Galera quorum with good data. This procedure is for rebuilding a lost or corrupted node only.

Start the restored node and rejoin the cluster

Start MariaDB and confirm it rejoins via SST or reports a healthy wsrep state.

sudo systemctl start mariadb
sudo mysql -u root -p -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
sudo mysql -u root -p -e "SHOW STATUS LIKE 'wsrep_local_state_comment';"

You want wsrep_local_state_comment to report Synced. For full cluster setup details, see Configure MariaDB Galera cluster 10.11 for multi-master replication.

Offloading backups to remote or S3-compatible storage

Local backups protect against data corruption, but not against disk failure or a compromised host. Offload archives to a remote location as part of the same automation.

Install the S3-compatible client

sudo apt install -y awscli

Configure remote credentials

Store credentials in a restricted profile file, not inline in scripts.

sudo -u mysql aws configure --profile galera-backup

Provide your access key, secret key, and region when prompted. This writes to ~mysql/.aws/credentials, which should already inherit restrictive permissions from the mysql home directory.

Add the upload step to the backup script

Append this block to the end of the backup script, after the rotation step.

echo "[$(date)] Uploading archive to remote storage" >> "${LOG_FILE}"
aws s3 cp "${ARCHIVE_DIR}/${BACKUP_NAME}.tar.zst" \
  s3://example-galera-backups/nightly/ \
  --profile galera-backup \
  --endpoint-url https://s3.eu-central-1.example.com \
  >> "${LOG_FILE}" 2>&1
echo "[$(date)] Remote upload complete" >> "${LOG_FILE}"

Point --endpoint-url at your actual S3-compatible provider. For a self-hosted alternative, see Setup S3-compatible disaster recovery with cross-region replication using MinIO.

Encrypt archives before upload for sensitive workloads

If your data falls under GDPR or contains customer PII, encrypt the archive before it leaves the host, rather than relying solely on transport encryption.

gpg --batch --yes --passphrase-file /etc/mysql/backup/.gpgpass \
  --cipher-algo AES256 \
  --symmetric \
  --output "${ARCHIVE_DIR}/${BACKUP_NAME}.tar.zst.gpg" \
  "${ARCHIVE_DIR}/${BACKUP_NAME}.tar.zst"

For a full walkthrough of key rotation and secure passphrase handling, see Implement MariaDB backup encryption with Mariabackup and automated restoration.

Verify your setup

systemctl list-timers galera-backup.timer
journalctl -u galera-backup.service --since "1 day ago"
ls -lh /var/backups/mariadb/archives
aws s3 ls s3://example-galera-backups/nightly/ --profile galera-backup --endpoint-url https://s3.eu-central-1.example.com

Confirm at least one successful run exists in the journal, the archive directory contains recent files matching your retention window, and the remote bucket lists the same file count.

Common issues

SymptomCauseFix
Mariabackup fails with access deniedbackupuser missing BACKUP_ADMIN or RELOAD privilegeRe-run the GRANT statement and FLUSH PRIVILEGES
Backup succeeds but restore fails to start mariadbPrepare step was skipped or failed silentlyRe-run mariabackup --prepare against the extracted backup before copy-back
Timer never triggersTimer enabled but not started, or unit file typosystemctl status galera-backup.timer and check for load errors
Archive directory fills up diskRetention find command not matching filenamesCheck filename pattern matches exactly what the script produces
S3 upload fails with 403Wrong endpoint URL or expired credentialsRe-run aws configure and verify bucket policy allows PutObject
Restored node stuck at Joining stateCorrupted backup or mismatched Galera versionVerify archive with zstd -t before restore, confirm mariadb-server version matches cluster

Next steps

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Don't want to manage this yourself?

We handle infrastructure for businesses that depend on uptime. Fully managed, with one fixed contact who knows your setup.

You get one fixed contact who knows your setup

Rotterdam 01:47 · reachable in a message, no ticket form