MariaDB replication with db-adm¶
This guide sets up asynchronous primary → replica replication using GTIDs, seeds the replica from a consistent dump, and monitors it. Each step shows the db-adm mariadb command and the SQL it runs, so you can also do it by hand.
Examples use db1 (primary, 10.0.0.11) and db2 (replica, 10.0.0.12), with config profiles [primary] and [replica1].
1. How it works¶
- The primary writes every change to its binary log (binlog). Each transaction gets a GTID
domain-server_id-sequence, e.g.0-1-1042. - The replica's IO thread connects to the primary as a replication user and copies binlog events into its relay log.
- The replica's SQL thread applies the relay log and records the last applied GTID in
gtid_slave_pos. - With GTIDs the replica asks for "everything after
gtid_slave_pos", so you never have to track binlog file names and offsets. That makes reseeding and failover much easier.
The replica only needs a consistent starting point: a copy of the data plus the exact GTID that copy corresponds to.
2. Server configuration¶
Both servers need a unique server_id and binary logging. Put this in /etc/my.cnf.d/replication.cnf (RHEL/Fedora) or /etc/mysql/mariadb.conf.d/60-replication.cnf (Debian/Ubuntu) and restart.
Primary:
[mariadb]
server_id = 1
log_bin = mariadb-bin
log_basename = db1 # stable binlog/relay-log names if the hostname changes
binlog_format = ROW
gtid_domain_id = 0
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
bind_address = 0.0.0.0 # or the replication interface
binlog_expire_logs_seconds = 604800 # 7 days; must exceed your longest replica outage
Replica:
[mariadb]
server_id = 2
log_bin = mariadb-bin # lets the replica be promoted later
log_basename = db2
binlog_format = ROW
log_slave_updates = ON # replicated changes also go to its binlog
read_only = ON # stops app writes; replication and admins still write
relay_log_recovery = ON
Check the primary:
db-adm mariadb --profile primary repl primary-setup --check-only
SETTING VALUE CHECK NOTE
log_bin ON ok
server_id 1 warn default value; make sure every replica uses a different id
binlog_format ROW ok
bind_address 0.0.0.0 ok
sync_binlog 1 ok
innodb_flush_log_at_trx_commit 1 ok
gtid_domain_id 0 info
It exits 2 if replication can't work (log_bin off, server_id 0, skip_networking), and prints the config snippet to add.
3. Replication account¶
Create it on the primary, restricted to the replicas' network:
db-adm mariadb --profile primary repl primary-setup \
--repl-user repl --repl-host '10.0.0.%' \
--generate --store-pass homelab/mariadb/repl [--require-ssl]
SQL equivalent:
CREATE USER 'repl'@'10.0.0.%' IDENTIFIED BY '…';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.%';
The tool hashes the password client-side, stores it in pass before creating the account, and prints the current gtid_binlog_pos.
Firewall: allow TCP 3306 from the replica to the primary.
4. Seed the replica¶
The replica must start with the same data the primary had at a known GTID.
Option A: logical dump (most homelab-sized databases)¶
On the primary:
db-adm mariadb --profile primary backup --all-in-one --master-data --dir /var/tmp/seed
This runs mariadb-dump --all-databases --single-transaction --master-data=2 --gtid --routines --triggers --events --hex-blob. --single-transaction gives a consistent InnoDB snapshot without locking writers. --master-data=2 --gtid records the position as a comment:
-- SET GLOBAL gtid_slave_pos='0-1-1042';
(MariaDB 10.x/11.x writes that line at the end of the dump; --gtid-pos-from-dump scans the whole file.)
Copy the dump to the replica and load it:
scp /var/tmp/seed/all-databases/db1-all-databases-*.sql.zst db2:/var/tmp/
ssh db2 'zstdcat /var/tmp/db1-all-databases-*.sql.zst | sudo mariadb'
The dump includes the mysql schema, so accounts (including repl) come across too.
Option B: physical copy (large databases)¶
For hundreds of GB, use mariadb-backup --backup --slave-info on the primary, then --prepare and --copy-back on the replica. The GTID is in xtrabackup_binlog_info; pass it with --gtid-pos.
5. Point the replica at the primary¶
On the replica:
db-adm mariadb --profile replica1 repl replica-setup \
--primary-host 10.0.0.11 --repl-user repl \
--repl-pass-entry homelab/mariadb/repl \
--gtid-pos-from-dump /var/tmp/db1-all-databases-2026.10.09.021500Z.sql.zst \
--read-only
Before changing anything, it logs in to the primary as repl, from where the tool runs. That checks the network, the account's host pattern and the password, and refuses to continue if both servers have the same server_id or the primary has log_bin off. It then runs:
SET GLOBAL gtid_slave_pos = '0-1-1042';
CHANGE MASTER TO
MASTER_HOST = '10.0.0.11', MASTER_PORT = 3306,
MASTER_USER = 'repl', MASTER_PASSWORD = '…',
MASTER_CONNECT_RETRY = 10,
MASTER_USE_GTID = slave_pos;
SET GLOBAL read_only = ON;
START SLAVE;
It then waits up to 10 seconds for both threads to report Yes:
gtid_slave_pos from /var/tmp/db1-all-databases-….sql.zst: 0-1-1042
preflight: reached primary 10.0.0.11:3306 (server_id 1, 11.4.9-MariaDB-log, gtid_binlog_pos 0-1-1057)
replica configured: primary 10.0.0.11:3306 as repl
replication running: IO=Yes SQL=Yes lag=0s position 0-1-1057
Other options:
--master-sslsetsMASTER_SSL=1(pair with--require-sslon the account).--gtid current-posis for a server that may have binlogged its own writes, e.g. a former primary rejoining as a replica.--log-file/--log-posis classic file/position replication (implies--gtid no).--connection NAMEis for multi-source: one replica, several primaries.--dry-runprints the SQL with the password masked.--forcereconfigures an existing setup (it runsSTOP SLAVEfirst).
Brand-new primary with no data yet? Skip seeding. With an empty gtid_slave_pos the replica asks for the binlogs from the beginning, and the tool warns about that.
6. Verify¶
db-adm mariadb --profile replica1 repl status -v
REPL OK - default: IO=Yes SQL=Yes lag=0s | 'lag_default'=0s;60;300
CONNECTION STATE PRIMARY IO SQL LAG POSITION
default OK 10.0.0.11:3306 Yes Yes 0 0-1-1057
Smoke test: write on the primary, read on the replica.
7. Monitoring¶
repl status follows the Nagios plugin conventions: the first line is the summary with perfdata, and the exit code is 0 OK, 1 WARNING, 2 CRITICAL or 3 UNKNOWN.
| Condition | State |
|---|---|
IO and SQL threads Yes, lag < --warn-lag |
OK |
lag ≥ --warn-lag (default 60s) |
WARNING |
lag ≥ --crit-lag (default 300s) |
CRITICAL |
IO thread No/Connecting, SQL thread stopped, lag NULL |
CRITICAL, with Last_IO_Error/Last_SQL_Error |
| replication not configured | CRITICAL |
| can't connect / no privilege | UNKNOWN |
Use a least-privilege login for checks:
db-adm mariadb --profile primary user create monitor@localhost --role monitor --generate --store-pass homelab/mariadb/monitor
Because it's created on the primary, it replicates to the replica.
Icinga/Nagios command:
object CheckCommand "mariadb_repl" {
command = [ "/usr/local/bin/db-adm", "mariadb", "--profile", "$profile$", "repl", "status",
"--warn-lag", "$warn_lag$", "--crit-lag", "$crit_lag$" ]
}
No monitoring system? Have a timer mail you instead. This replaces the old cron + mail script:
# /etc/systemd/system/mariadb-repl-check.service
[Service]
Type=oneshot
User=dbbackup
ExecStart=/usr/local/bin/db-adm mariadb --profile replica1 repl status --notify [email protected]
SuccessExitStatus=1 2 3
--notify (or notify_email in the config) sends mail through the local sendmail/mail only when the state isn't OK.
8. When it breaks¶
Symptom in repl status |
Cause | Fix |
|---|---|---|
IO=Connecting … Can't connect |
Primary down, firewall, bind_address |
Fix connectivity; the IO thread retries every 10s on its own |
IO=No … Access denied |
Wrong password or host pattern | repl replica-setup --force … with the right --repl-pass-entry, or fix the account on the primary |
IO=No … 1236 … could not find GTID / binlog purged |
Replica was offline longer than binlog retention | Reseed (step 4) and raise binlog_expire_logs_seconds |
SQL=No … Duplicate entry / 1032 Can't find record |
Data drift: someone wrote on the replica | Find and fix the drifted rows, then repl start. Keep read_only=ON. Last resort for one bad event: STOP SLAVE; SET GLOBAL sql_slave_skip_counter=1; START SLAVE; then check consistency (e.g. pt-table-checksum) |
server_id clash at setup |
Cloned VM or default config | Unique server_id in my.cnf, restart |
| Lag keeps climbing | Big transactions, slow replica disk, single-threaded apply | slave_parallel_threads=4, slave_parallel_mode=optimistic; check slowlog summary on the replica |
Start over cleanly on the replica:
db-adm mariadb --profile replica1 repl reset --yes # STOP SLAVE; RESET SLAVE ALL
9. Promoting the replica (planned switchover)¶
- On the primary:
SET GLOBAL read_only = ON;and wait for apps to drain. - On the replica: wait for
repl statuslag 0 andgtid_slave_pos= primary'sgtid_binlog_pos. - On the replica:
db-adm mariadb repl reset --yesthenSET GLOBAL read_only = OFF; - Point applications at the replica.
- Optional, old primary as the new replica:
db-adm mariadb repl replica-setup --primary-host <new primary> --gtid current-pos …
10. Galera instead?¶
Galera is synchronous multi-primary, which is a different trade-off: no lag and no failover step, but every write pays a cluster round-trip and you need 3+ nodes for quorum. For Galera nodes use db-adm mariadb galera status --expect-size 3 instead of repl status.