MariaDB Backups

Backing up MariaDB / MySQL to Proxmox Backup Server: a tested method, measured figures

One dump per database, sent encrypted to Proxmox Backup Server with proxmox-backup-client, plus binlogs every 5 minutes: that's the method we chose after comparing it with six others on our own database. Here are the scripts, point-in-time recovery, and the figures from our bench.

Measurements in progress: The first full backup (6 databases, 29.73 GiB) is measured below. Day-to-day deduplication on all 6 databases is measured below (third backup). An ordinary night's lock duration for the physical copy will be added here.

Why PBS for MariaDB databases

  • Inline deduplication: only new chunks are sent. On our internal database, a full daily dump costs only a few more MiB from one pass to the next.
  • Client-side encryption: the key stays with you. Without it, nobody can read your dumps, not even us.
  • Off-site: backups leave the production server, and the client account has no delete rights by default on NimbusBackup. An air-gapped copy is available on top.
  • An application-level dump rather than a VM freeze: a well-known Proxmox forum thread describes MariaDB hanging on overlapping fsfreeze calls. A dump doesn't depend on that mechanism.

Prerequisites: the client and the encryption key

On Debian, Proxmox's official package is enough; on Ubuntu, use Proxmox's static package, from its repository or ours. On Fedora, RHEL, Rocky, Alma, Arch or Alpine, Proxmox ships no package: we maintain an unofficial proxmox-backup-client repository (RPM, Arch, Alpine, arm64). Thanks to this repository, we can provide this method on all of these distributions. Step-by-step installation is in our proxmox-backup-client on Linux tutorial.

We recommend client-side encryption in every case. Keep the key and its paper copy off the server: without it, nothing can be restored.

Encryption key and repository credentials

# 1. Client-side encryption key (keep it OUTSIDE the server too)
proxmox-backup-client key create /root/.config/proxmox-backup/encryption-key.json

# 2. Paper copy of the key (print it, store it offline)
proxmox-backup-client key paperkey /root/.config/proxmox-backup/encryption-key.json --output-format text

# 3. Optional: a master key (RSA) to recover backups if the key is lost
proxmox-backup-client key create-master-key
proxmox-backup-client key import-master-pubkey master-public.pem

# Repository credentials, readable by root only
install -d -m 700 /etc/mariadb-pbs
cat > /etc/mariadb-pbs/env <<'EOF'
export PBS_REPOSITORY='user@pbs!token@your-host.nimbus.rdem-systems.com:8007:your-datastore'
export PBS_PASSWORD='token-secret'
EOF
chmod 600 /etc/mariadb-pbs/env

The recommended method: one dump per database, as pxar

Each database is exported to its own file, uncompressed, with --skip-dump-date and --order-by-primary so the content stays identical from one day to the next wherever nothing changed. The directory is then sent as a pxar archive, chunked by content.

Each database is consistent on its own (one transaction per database), not with the others. The script stops as soon as a dump fails, before anything is sent.

Daily dump script

#!/bin/bash
# /usr/local/sbin/mariadb-pbs-dump : daily logical dump -> Proxmox Backup Server
set -euo pipefail
. /etc/mariadb-pbs/env
ID="$(hostname --fqdn)"
STAGE=/var/backups/mariadb-pbs
KEY=/root/.config/proxmox-backup/encryption-key.json
CHUNK=1024  # KiB: same dedup as 256 within 0.1 point on our test, 4x fewer chunks

rm -rf "$STAGE"; install -d -m 700 "$STAGE"
mapfile -t DBS < <(mariadb -N -e "SELECT schema_name FROM information_schema.schemata
  WHERE schema_name NOT IN ('information_schema','performance_schema','sys','mysql')")
[ "${#DBS[@]}" -gt 0 ] || { echo "no database listed, aborting" >&2; exit 1; }
# binlog position in the dump header (needed for point-in-time recovery), only if binlogs are on
BINPOS=(); [ "$(mariadb -N -e 'SELECT @@log_bin')" = 1 ] && BINPOS=(--master-data=2)

for db in "${DBS[@]}"; do
  # one file per database, NOT compressed (PBS compresses and deduplicates itself)
  mariadb-dump --single-transaction --skip-dump-date --order-by-primary \
    --routines --events --triggers --hex-blob "${BINPOS[@]}" \
    --databases "$db" > "$STAGE/$db.sql"       # set -e stops here if the dump fails
done
# system tables are NOT dumped raw (reloading them on another version is risky):
# accounts and grants as portable SQL instead (tested with mariadb-dump 11.8)
mariadb-dump --system=users > "$STAGE/_users.sql"
(cd "$STAGE" && sha256sum *.sql > SHA256SUMS)   # lets you self-check a restore later

proxmox-backup-client backup "dumps.pxar:$STAGE" \
  --backup-id "$ID-mariadb-dumps" --chunk-size "$CHUNK" \
  --keyfile "$KEY" --crypt-mode encrypt
rm -rf "$STAGE"

Scheduling

# /etc/cron.d/mariadb-pbs
15 2 * * *   root  /usr/local/sbin/mariadb-pbs-dump     >> /var/log/mariadb-pbs.log 2>&1
*/5 * * * *  root  /usr/local/sbin/mariadb-pbs-binlogs  >> /var/log/mariadb-pbs.log 2>&1

Binlogs every 5 minutes, and point-in-time recovery

The daily dump gives one recovery point per day. To go back to just before a human error, you need binlogs. On every cycle, the current binlog is closed (FLUSH BINARY LOGS) and only closed files are sent, in --change-detection-mode=metadata: only new files are read. To size how much data you can afford to lose, and how long a restore may take, use the RPO / RTO calculator.

In our test (in a container, accelerated), each cycle sent 4.7 to 5.5 MiB of binlog, i.e. 0.6 to 0.7 MiB compressed. Point-in-time recovery was validated: base dump plus binlogs pulled back from Nimbus, replayed up to the faulty DROP TABLE, 180,000 rows recovered out of 180,000.

Binlog script

#!/bin/bash
# /usr/local/sbin/mariadb-pbs-binlogs : every 5 minutes, closed binlogs only
set -euo pipefail
. /etc/mariadb-pbs/env
ID="$(hostname --fqdn)"
BL=/var/lib/mysql-binlog     # dedicated directory (log_bin=/var/lib/mysql-binlog/mariadb-bin)

mariadb -e "FLUSH BINARY LOGS"                             # close the current binlog
ACTIVE=$(mariadb -N -e "SHOW MASTER STATUS" | cut -f1)     # the new, still open one

proxmox-backup-client backup "binlogs.pxar:$BL" \
  --backup-id "$ID-mariadb-binlogs" --change-detection-mode=metadata \
  --exclude "$ACTIVE" --exclude "$ACTIVE.idx" \
  --keyfile /root/.config/proxmox-backup/encryption-key.json --crypt-mode encrypt

Restore to just before a mistake (PITR)

# 1. restore the dump of the database and the latest binlog snapshot
proxmox-backup-client restore "host/$ID-mariadb-dumps/<date>"   dumps.pxar   /r/dumps   --keyfile "$KEY"
proxmox-backup-client restore "host/$ID-mariadb-binlogs/<date>" binlogs.pxar /r/binlogs --keyfile "$KEY"

# 2. replay start: the position written by --master-data=2 at the top of the dump
grep -m1 "CHANGE MASTER TO" /r/dumps/app.sql
#   -- CHANGE MASTER TO MASTER_LOG_FILE='mariadb-bin.000001', MASTER_LOG_POS=4394120;

# 3. find the faulty event: note the "# at" of the GTID event just before it
mariadb-binlog /r/binlogs/mariadb-bin.000008 | grep -B30 "DROP TABLE" | grep -E "^# at|GTID"

# 4. binlog list from the index file (never a glob: 11.8 writes .idx files next to them)
FILES=$(sed 's|.*/||' /r/binlogs/mariadb-bin.index | awk '$0>="mariadb-bin.000001" && $0<="mariadb-bin.000008"' | sed 's|^|/r/binlogs/|')

# 5. reload the dump, then replay up to the event before the mistake
mariadb < /r/dumps/app.sql
mariadb-binlog --start-position=4394120 --stop-position=5737637 $FILES | mariadb
  • MariaDB 11.8 writes .idx files next to binlogs: read the list from mariadb-bin.index, never with a shell glob.
  • --stop-datetime only stops to the second: our test lost a whole cycle of writes that way. After a human error, find the faulty event and use --stop-position.
  • A snapshot every 5 minutes adds up quickly. Since the client account can't delete, retention (prune) is configured on the Nimbus side.
  • Our test covered a single database. On a server with several databases, validate the replay on your workload before relying on it.

Physical copy with no temporary space: FLUSH TABLES … FOR EXPORT

A dump is restored by replaying SQL: every INSERT, every index rebuilt. On 30 GB that can take hours. A physical copy goes back in place at disk speed. On our 5 small databases, our test on an internal database gives 43 s to fetch and bring back a 4.05 GiB physical copy, against about 195 s to fetch and reload the 1.7 GiB dump. But mariadb-backup needs temporary space the size of the database, which our server lacked.

FLUSH TABLES … FOR EXPORT freezes the tables, writes a .cfg file next to each .ibd and blocks writes until UNLOCK TABLES. Meanwhile pxar reads the database directory in place, launched from the same session (the mariadb client's system command) so the lock holds. In metadata mode, unchanged files, like the closed partitions of a time series, aren't even re-read. To keep the lock short from the first night, a pre-pass without lock first uploads almost everything into the same group: its snapshot isn't consistent and is annotated as such.

Physical copy script (pre-pass, then locked pass)

#!/bin/bash
# /usr/local/sbin/mariadb-pbs-datadir : physical copy per database, NO temporary space
# 1) pre-pass without lock (not consistent, annotated)  2) locked pass: FLUSH TABLES ... FOR EXPORT,
# pxar of the database directory sent FROM THE SAME SESSION, then UNLOCK TABLES.
set -euo pipefail
. /etc/mariadb-pbs/env
ID="$(hostname --fqdn)"
KEY=/root/.config/proxmox-backup/encryption-key.json
O="--chunk-size 1024 --change-detection-mode=metadata --keyfile $KEY --crypt-mode encrypt"
note() { t=$(proxmox-backup-client snapshot list "host/$1" --output-format json |
    python3 -c 'import json,sys; print(max(x["backup-time"] for x in json.load(sys.stdin)))')
  proxmox-backup-client snapshot notes update "host/$1/$(date -u -d @$t +%Y-%m-%dT%H:%M:%SZ)" "$2"; }

# schema + system tables as portable SQL (needed to rebuild a fresh instance)
S=/var/backups/mariadb-pbs-schema; rm -rf "$S"; install -d -m 700 "$S"
mariadb-dump --no-data --routines --triggers --all-databases > "$S/schema.sql"
mariadb-dump --system=all > "$S/system.sql"
proxmox-backup-client backup "schema.pxar:$S" --backup-id "$ID-mariadb-schema" $O
rm -rf "$S"

for db in $(mariadb -N -e "SELECT schema_name FROM information_schema.schemata
    WHERE schema_name NOT IN ('mysql','information_schema','performance_schema','sys')"); do
  T=$(mariadb -N -e "SELECT GROUP_CONCAT(table_name) FROM information_schema.tables
      WHERE table_schema='$db' AND engine='InnoDB' AND table_type='BASE TABLE'")
  [ "$T" != "NULL" ] || continue
  G="$ID-mariadb-datadir-$db"
  # 1. pre-pass, no lock, low priority: uploads almost everything while the database runs
  PRE_START=$(date +%s)
  nice -n 19 ionice -c3 proxmox-backup-client backup "$db.pxar:/var/lib/mysql/$db" --backup-id "$G" $O
  note "$G" "PRE-PASS without lock: NOT CONSISTENT, do not restore"
  # 2. locked pass: only files changed since the pre-pass are re-read (metadata mode).
  #    Under the lock, every file modified since the pre-pass began is touched: it is then fully
  #    re-read, even if a write during the pre-pass left the same mtime (ext4 timestamp granularity).
  mariadb "$db" <<SQL
FLUSH TABLES $T FOR EXPORT;
system find /var/lib/mysql/$db -type f -newermt @$((PRE_START-2)) -exec touch {} +
system proxmox-backup-client backup $db.pxar:/var/lib/mysql/$db --backup-id $G $O
UNLOCK TABLES;
SQL
  note "$G" "CONSISTENT (FLUSH TABLES FOR EXPORT): restore with IMPORT TABLESPACE"
done

Restore: fresh instance, schema, then IMPORT TABLESPACE (full or partial)

# On a FRESH instance of the same major version (mysql_install_db, then start MariaDB)
. /etc/mariadb-pbs/env; ID="$(hostname --fqdn)"; K="--keyfile /root/.config/proxmox-backup/encryption-key.json"
# pick the snapshots annotated CONSISTENT, never the pre-pass ones
proxmox-backup-client snapshot list "host/$ID-mariadb-datadir-mydb"     # read the notes column

# 1. schema and system tables
proxmox-backup-client restore "host/$ID-mariadb-schema/<time>" schema.pxar /srv/restore/schema $K
mariadb < /srv/restore/schema/schema.sql
mariadb < /srv/restore/schema/system.sql

# 2. tablespace files of the database (.ibd + .cfg)
proxmox-backup-client restore "host/$ID-mariadb-datadir-mydb/<time>" mydb.pxar /srv/restore/mydb $K
D=/var/lib/mysql/mydb

# 3a. non-partitioned table (repeat per table: that's also the partial restore)
mariadb mydb -e "ALTER TABLE customers DISCARD TABLESPACE"
cp /srv/restore/mydb/customers.ibd /srv/restore/mydb/customers.cfg $D/ && chown mysql:mysql $D/customers.*
mariadb mydb -e "ALTER TABLE customers IMPORT TABLESPACE"

# 3b. partitioned table: MariaDB refuses DISCARD on it -> one partition at a time via EXCHANGE
mariadb mydb -e "CREATE TABLE _imp LIKE measurements; ALTER TABLE _imp REMOVE PARTITIONING;
                 ALTER TABLE _imp DISCARD TABLESPACE"
cp "/srv/restore/mydb/measurements#P#p202609.ibd" $D/_imp.ibd
cp "/srv/restore/mydb/measurements#P#p202609.cfg" $D/_imp.cfg; chown mysql:mysql $D/_imp.*
mariadb mydb -e "ALTER TABLE _imp IMPORT TABLESPACE;
                 ALTER TABLE measurements EXCHANGE PARTITION p202609 WITH TABLE _imp; DROP TABLE _imp"

# 4. check the pages of every restored tablespace (catches a torn page), BEFORE importing in production
for f in /srv/restore/mydb/*.ibd; do innochecksum "$f" || echo "PAGE ERROR: $f"; done
# 5. check: row count + logical checksum, to compare with the source
mariadb -N mydb -e "SELECT COUNT(*), BIT_XOR(CRC32(CONCAT_WS('#', id, measured_at, v))) FROM measurements"

What we measured

  • Container prototype (table partitioned by month, 2.45 million rows): 1st upload with a 2.5 s lock; 2nd upload after 50,000 inserts: 18.6 MiB re-read out of 160.8, 0.3 s lock.
  • Prototype restore with IMPORT TABLESPACE and EXCHANGE PARTITION: row count and logical checksum identical to the source.
  • Pre-pass without lock on our server (6 databases): 45 GiB → 9.6 GiB stored (−79%), read in 14 min at low priority. That's about twice the dump: the physical copy holds the indexes and the free space inside the .ibd files.
  • First locked pass on our server (30/09/2026), two days after the pre-pass: lock held 138.5 s on the time-series database (2.51 GiB re-read and sent out of 41.7 GiB, closed partitions reused without re-reading), then 4.4 s, 2.2 s, 2.0 s, 1.7 s and 0.4 s on the five other databases. This first pass covered two days of changes: an ordinary night's lock will be shorter.
  • Writes to the locked tables are blocked during the locked pass: that's why the pre-pass exists, and why the locked upload doesn't run at low priority.
  • InnoDB only. The mysql system database goes as portable SQL (mariadb-dump --system=all), with the schema.
  • It isn't a datadir you can start on as is: there's no system tablespace and no logs. Restoring goes through a fresh instance, the schema, then the import. To start the engine directly on the backup, you need a full mariadb-backup copy.
  • MariaDB refuses DISCARD TABLESPACE on a partitioned table: import partition by partition, through a temporary table and EXCHANGE PARTITION.
  • Pre-pass snapshots aren't restorable: filter on the "CONSISTENT" note. The import requires the same schema and a compatible version.
  • In a mariadb client system line, a ; turns the whole line into SQL, even inside single quotes: our first production attempt failed that way (error 1064). Chain with && or call a script.
  • metadata mode only re-reads a file if its modification time changed. On ext4 that timestamp has a granularity of a few milliseconds: a write during the pre-pass could leave the same time, and the locked pass would reuse a half-written page. Hence the touch, under the lock, of every file modified since the pre-pass began, which forces a full re-read, and the innochecksum check in the restore procedure.

Restore and check it yourself

The SHA256SUMS file produced by the script lets you check a restore bit for bit. Then reload the dump into a throwaway instance: that's the only real test. Restore drills run by our team are part of our managed services, not of the backup offer.

Restore and check

. /etc/mariadb-pbs/env; ID="$(hostname --fqdn)"
KEY=/root/.config/proxmox-backup/encryption-key.json

# list snapshots (paths are host/<backup-id>/<RFC 3339 date>, not an epoch)
proxmox-backup-client snapshot list "host/$ID-mariadb-dumps"

# restore every dump of a snapshot, then check it bit for bit
SNAP="host/$ID-mariadb-dumps/2026-09-28T12:06:51Z"
proxmox-backup-client restore "$SNAP" dumps.pxar /srv/restore --keyfile "$KEY"
cd /srv/restore && sha256sum -c SHA256SUMS

# or pick a single database file interactively
proxmox-backup-client catalog shell "$SNAP" dumps.pxar --keyfile "$KEY"
#   pxar:/ > find --select *.sql      # --select is required, otherwise nothing is restored
#   pxar:/ > restore-selected /srv/restore

Our bench: the measured figures

Our test, on an internal database: RDEM's web-01 server, MariaDB 11.8.6, 5 databases, dumps of about 1.695 GiB, proxmox-backup-client 4.2.6 (official Debian package), NimbusBackup datastore, client-side encryption on, 28/09/2026. Each row shows what the 2nd pass, one hour after the 1st, had to send. Deduplication is computed the same way for every row: 1 − (raw new / raw total).

MethodNew (raw)Total (raw)New (compressed)Deduplication
One dump per database → directory → pxar, --chunk-size 25624.2 MiB1.695 GiB1.6 MiB98.6 %
mariadb-dump --dir → pxar, 256 KiB (restore instance)29.4 MiB1.493 GiB2.2 MiB98.1 %
One dump per database → pxar, default 4 MiB106.4 MiB1.695 GiB8.5 MiB93.9 %
Go client, -backupstream (≈ 5.8 MB chunks)107.8 MiB1.695 GiBnot measured93.8 %
mariadb-dump --dir → pxar, 4 MiB (restore instance)96.0 MiB1.493 GiB9.5 MiB93.7 %
mariadb-backup (physical, 5 databases) → pxar882.1 MiB4.012 GiB56.1 MiB78.5 %
mydumper 1.0.5 (4 threads) → pxar, 256 KiB1.173 GiB1.967 GiB66.8 MiB40.4 %
mydumper 1.0.5 → pxar, 4 MiB1.617 GiB1.967 GiB91.7 MiB17.8 %
Pipe → .img:/dev/stdin (official client)1.691 GiB1.695 GiB108.0 MiB0.2 %
Pipe | zstd -3 → .img:/dev/stdin111.9 MiB111.9 MiB111.9 MiB0 %

First full backup: 6 databases, 29.73 GiB

Our test, on an internal database: same server, the 5 databases plus a 29 GB (on disk) time-series database (tables partitioned by month, mostly appends), on a datastore emptied beforehand, with a new encryption key. On 28/09/2026, in the early evening, at low priority (nice, ionice) on a production server. The bench dumps with this page's script, but without --events: its MariaDB account is read-only and lacks the EVENT privilege.

  • MariaDB dump of the 6 databases: 2,047 s (34 min), of which 1,926 s for the time-series database. That's MariaDB's time, not the upload.
  • Temporary space: that database's SQL dump weighs 28 GiB (30.1 GB), as much as the database on disk. The directory method needs that space locally.
  • Encrypted upload to NimbusBackup: 29.73 GiB in 7 min 00 s with 1 MiB chunks (72 MiB/s), 6 min 29 s with 4 MiB (78 MiB/s), 7 min 49 s with 256 KiB (65 MiB/s).
  • Stored: 29.73 GiB → 4.3 GiB (−86%), about 7x compression on SQL text. To estimate the space needed on your own datastore, see our ZFS RAIDZ capacity and PBS storage calculator.
  • Number of chunks on the datastore for these 29.73 GiB: 121,483 at 256 KiB, 30,362 at 1 MiB, 7,132 at 4 MiB. Smaller chunks deduplicate better, but multiply the files to store, verify and garbage-collect.
  • Restore of the dump files from NimbusBackup (1 MiB archive): 29.73 GiB in 12 min 27 s (40.8 MiB/s), identical SHA256. Reloading into MariaDB isn't counted. It also ran at low priority and wrote to the production disk: on a dedicated machine it would be faster.

Second backup, 7 hours later

Our test, on an internal database: the same 6 databases, dumped again on 29/09/2026 at 2 am, 7 hours after the first backup. Each line shows what PBS had to receive that was new. It's a change indicator for our database, mostly fed by appends, over 7 hours: don't extrapolate it to 24 hours or to another database.

  • 1 MiB chunks: 132.2 MiB of new data out of 29.77 GiB (99.57% already present), i.e. 17.2 MiB actually sent after compression, in 2 min 35 s.
  • 256 KiB chunks: 96.1 MiB new (99.68%), 12.6 MiB sent. 4 MiB chunks: 260.2 MiB new (99.15%), 34.1 MiB sent.
  • The upload still takes 2 to 3 minutes for a few dozen MiB: the client has to re-read and chunk the 29.77 GiB locally to find what changed.
  • Restore of the dump files from NimbusBackup: 12 min 57 s, identical SHA256.

Third backup, 24 hours later

Our test, on an internal database: the same 6 databases, dumped on 30/09/2026 at 2 am, 24 hours after the second backup. It's the day-to-day measurement for our database, a time series mostly fed by appends: a more heavily updated database will give less.

  • 1 MiB chunks: 668.9 MiB of new data out of 29.93 GiB (97.82% already present), i.e. 60.6 MiB actually sent after compression, in 2 min 35 s.
  • 256 KiB chunks: 634.7 MiB new (97.93%), 55.3 MiB sent. 4 MiB chunks: 791.4 MiB new (97.42%), 77.6 MiB sent.
  • 1 MiB deduplicates only 0.1 point less than 256 KiB, for four times fewer chunks (30,513 vs 122,246): that's the size we keep.
  • Restore of the dump files from NimbusBackup: 12 min 09 s, identical SHA256.

First bench (5 databases): first pass and restore

  • First pass (empty group): 1.695 GiB sent in 18 s, 108 MiB stored after compression.
  • Restore of 1.694 GiB from Nimbus in 10.2 s (170.5 MiB/s as reported by the client), with the key alone.
  • Identical SHA256 for all 5 dumps: that's the proof the restore is bit-for-bit faithful.
  • Reload into MariaDB 11.8.6 in about 3 minutes, 552 tables out of 552, same table count as the source (a CHECK TABLE right after an import proves little; the SHA256 is what counts).

PBS deduplicates inline

There is no post-process deduplication. Each chunk is identified by its digest: the client doesn't send chunks the group already has, and the server stores each chunk once, across all groups. Garbage collection only removes orphan chunks. On our test datastore, 39 snapshots used 2.05 GiB for 22.3 GiB of logical data.

Deduplication depends on the write pattern

On synthetic data (1 million rows), scattered updates (one row in a hundred, spread evenly) gave 0% deduplication, with pxar at 256 KiB as with the Go client: every chunk contained a change. With clustered updates (1% of contiguous rows, plus appends), pxar at 256 KiB went back up to 96.4%. A database that mostly appends rows deduplicates very well; one rewritten everywhere, much less.

Other methods and their limits

NimbusBackup's historical, engine-agnostic approach

This page covers the MariaDB / MySQL-specific method, straight to Proxmox Backup Server: NimbusBackup, our managed PBS offer, or your own. For any database, whatever the engine, NimbusBackup also offers its historical approach: offsite database backup.

Compressing before sending (zstd)

Avoid it: PBS already compresses (112 MiB with zstd versus 108 MiB compressed by PBS), and a compressed stream no longer deduplicates. zstd --rsyncable would keep some of it, but it's still a bad idea.

mariadb-backup (physical backup)

78.5% deduplication in our bench. Useful to restore a large database quickly, but it needs root, datadir access and a few GB of local disk.

mariadb-dump --dir

As good as the per-database dump at equal chunk size (98.1% at 256 KiB), but the server writes the files: it needs the FILE privilege and a defined secure_file_priv. Measured on our restore instance, not in production.

mydumper

Parallel and consistent, but its output changes from one pass to the next: 40.4% at 256 KiB, 17.8% at 4 MiB. Here, parallelism costs deduplication.

Go client (tizbac/proxmoxbackupclient_go v1.1.3)

Its -backupstream option stores a stream in a dynamic index, without local disk: 93.8% in our bench, with chunks of about 5.8 MB that can't be tuned. Two limits today: no client-side encryption, and the official client can't read these backups back ("failed to parse archive type"). We contribute to this client.

What about piping directly, without local disk?

The official client accepts a stream since version 4.1.5 (commit 2db23ce, March 2026), as name.img:/dev/stdin. It works, and the restore is bit-for-bit faithful. But an .img archive is cut into fixed 4 MiB blocks: one added row shifts everything after it, and our bench measured only 0.2% deduplication. Every backup is therefore stored almost in full.

Two more limits: only the default chunk size works (any .img archive below 4096 KiB fails with "detected multiple end chunks (chunk size too small)", even from a file), and the Fedora/RHEL COPR is stuck at 4.1.0, without pipes. Our unofficial proxmox-backup-client repository (RPM, Arch, Alpine, arm64) ships a recent version.

Remember set -o pipefail: without it, a dump that fails halfway is backed up truncated, without any error.

Pipe to .img (client ≥ 4.1.5)

# Works from client 4.1.5 onwards; default chunk size only
set -o pipefail
mariadb-dump --single-transaction --skip-dump-date --databases app \
  | proxmox-backup-client backup app.img:/dev/stdin --keyfile "$KEY" --crypt-mode encrypt

Some guides suggest db.pxar.didx:- (example, accessed 28/09/2026). Every client version we tested rejects this syntax. It seems to mix the Go client, which stores a stream in a .didx, with the official client's command:

Actual client output

$ echo x | proxmox-backup-client backup db.pxar.didx:-
Error: parameter verification failed - 'backupspec': value does not match the regex pattern

Versions tested

VersionPipe .img:/dev/stdin.pxar.didx:-.img < 4096 KiB
4.0.20 → 4.1.4❌ « got unexpected file type (expected file or block device) »❌❌
4.1.5 → 4.2.6✅ default chunk size❌❌

An encrypted backup restores both ways between versions 4.2.6 and 4.1.1, with identical SHA256.

On Rocky Linux and RHEL: the certificate trap

On Rocky Linux 10.2, Proxmox's raw static binary looks for certificates in /usr/lib/ssl, which doesn't exist, and fails:

Raw binary error on Rocky 10.2

certificate validation failed - Certificate fingerprint was not confirmed.
Error: client error (Connect)
Caused by:
    0: error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:../ssl/statem/statem_clnt.c:2124:

The package from our unofficial proxmox-backup-client repository (RPM, Arch, Alpine, arm64) adds the two links needed: installed with dnf and GPG checks, it restored our dumps with the key alone, identical SHA256. Otherwise, SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt works around the problem (tested on Rocky 10.2 with the raw binary).

What about a Galera cluster?

We follow the pattern published in our case study backing up a Galera cluster to Proxmox Backup Server (in French): a 4th replica node, stopped cleanly, then backed up, without touching production nodes. The dumps and binlogs described here can be added from that node.

Backup naming and retention

A backup-id can't contain a /, and the client account can't create namespaces. We use the server's fully qualified name followed by the backup type, in separate groups: schedules differ, and PBS deduplicates from one snapshot to the next within the same group. If a namespace was created for the server at onboarding, use it.

Naming convention

ID="$(hostname --fqdn)"
--backup-id "$ID-mariadb-dumps"      # daily dump
--backup-id "$ID-mariadb-binlogs"    # every 5 minutes
--backup-id "$ID-mariadb-datadir"    # mariadb-backup, if you use it
# or, when a namespace was created for the server: --ns "$ID" --backup-id dumps

Retention is configured on the Nimbus side, since the client can't delete: that's also what protects your backups if the server is compromised.

Frequently asked questions

How long does a first backup to NimbusBackup take?

In our test on an internal database (6 databases, 29.73 GiB of dump, client-side encryption, production server at low priority): 7 minutes of upload with 1 MiB chunks, 4.3 GiB stored, and 12 min 27 s to restore the dump files from NimbusBackup, identical SHA256. The MariaDB dump itself took 34 minutes, and reloading the dumps into a database (not measured on this full set) also depends on MariaDB and the server, not on PBS.

Should dumps be compressed before sending them to PBS?

No. PBS compresses each chunk itself, and an already compressed dump no longer deduplicates: in our bench, a zstd stream was sent in full on every pass.

Can mariadb-dump be piped straight into proxmox-backup-client?

Yes, since version 4.1.5, with name.img:/dev/stdin and set -o pipefail. But the .img archive is cut into fixed blocks: our bench measured only 0.2% deduplication. A dump in a directory sent as pxar deduplicated at 98.6%.

Does the db.pxar.didx:- syntax work?

No. Every version tested, from 4.0.20 to 4.2.6, rejects it with "'backupspec': value does not match the regex pattern".

Does PBS deduplicate after the fact?

No, deduplication happens at upload: the client doesn't send chunks already known, and the server stores each chunk once. Garbage collection only removes orphan chunks.

Can RDEM restore my encrypted backups?

No. With client-side encryption, the key stays with you and we can't read the data. Restore drills by our team are part of managed services, with access defined in the contract.

Your MariaDB databases off-site, encrypted, deduplicated

NimbusBackup's PBS storage, with no client-side delete rights by default. You can also compare all plans. For restore drills run by our team, see our managed services.

Start your managed MariaDB project

Let's discuss your database needs. Our DBA team advises you on the optimal architecture for your use case.

RDEM Systems SAS — SIREN 820 338 671 — 5 B rue des Noyers, 95300 Pontoise