Binary-log archival on MariaDB
By the end of this page you will have a MariaDB backup job that builds an incremental chain and packs the server's binary logs into a tar archive beside each incremental artifact. You will be able to read the chain out of the execution history and the archive out of the manifest.
You will also know precisely where incremental support stops in v1.4.0, which on MariaDB matters more than the parts that work.
Budget about twenty-five minutes.
Binary-log archival is real and this page walks through it in full. Binary-log replay is not
reachable from a configured restore job: the restore planner rejects restore_mode: incremental for
every engine except PostgreSQL. Read
Where incremental support stops before you plan any work
around this feature.
What you need
-
The working directory and
sentinel.yamlfrom Restoring a MariaDB backup. -
A MariaDB container that actually writes binary logs, names them
mariadb-bin.NNNNNN, and keeps them somewhere Sentinel can read. Step 1 covers all three. -
MYSQL_PWDstill exported:export MYSQL_PWD=tutorial
Step 1: Start a server whose binary logs Sentinel can find
Three conditions have to hold, and on MariaDB none of them is the default.
Binary logging has to be switched on. This is the first real divergence from MySQL on this
track. MySQL 8 writes binary logs out of the box; MariaDB does not log at all until --log-bin is
given.
The file names must begin with mariadb-bin. When Sentinel scans the log directory it keeps only
regular files whose name starts with mariadb-bin. or mysql-bin., skipping directories and any
.index file. MariaDB derives its log file names from --log-basename or the host name, so a
server started with a bare --log-bin will typically produce <hostname>-bin.NNNNNN, which the scan
discards. Passing the value explicitly, --log-bin=mariadb-bin, removes the guesswork.
The directory must be readable by Sentinel, on Sentinel's own filesystem. mysql.binlog_path is
validated at configuration load: it is resolved to an absolute path and must exist and be a
directory. A path inside a container is not visible to a Sentinel process outside it, so the log
directory has to be bind-mounted out.
Recreate the container with all three satisfied:
docker rm -f sentinel-mariadb-tutorial
mkdir -p binlogs && chmod 0777 binlogs
docker run --name sentinel-mariadb-tutorial \
-e MYSQL_ROOT_PASSWORD=tutorial \
-e MYSQL_DATABASE=shop \
-p 3306:3306 \
-v "$PWD/binlogs:/var/lib/mysql-binlogs" \
-d mariadb:lts \
--log-bin=/var/lib/mysql-binlogs/mariadb-bin
The chmod 0777 is there because the MariaDB server process inside the container writes as its own
user, which will not match yours. It is acceptable for a throwaway directory in a tutorial and is not
a pattern to carry into production, where the binary logs are already on a filesystem the backup
process can read.
Recreating the container discards the data from the earlier pages. Re-seed it:
mariadb -h 127.0.0.1 -P 3306 -u root shop -e "
CREATE TABLE customers (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(120) NOT NULL, email VARCHAR(180) NOT NULL);
CREATE TABLE orders (id INT AUTO_INCREMENT PRIMARY KEY, customer_id INT, total DECIMAL(10,2), FOREIGN KEY (customer_id) REFERENCES customers(id));
INSERT INTO customers (name, email) VALUES ('Ada Lovelace','ada@example.invalid'),('Grace Hopper','grace@example.invalid'),('Alan Turing','alan@example.invalid');
INSERT INTO orders (customer_id, total) VALUES (1, 42.00), (2, 17.50), (3, 99.99);"
Then confirm all three conditions:
mariadb -h 127.0.0.1 -P 3306 -u root -e "SHOW VARIABLES LIKE 'log_bin%';"
ls binlogs/
What to look for: log_bin reported as ON, log_bin_basename ending in mariadb-bin, and at
least one file named mariadb-bin.000001 in binlogs/ alongside a mariadb-bin.index. If
binlogs/ is empty, everything below will fail with no_binlog_files_found.
Step 2: Enable incremental backup on the job
Edit the shop entry under databases in sentinel.yaml and add two blocks:
databases:
shop:
type: mariadb
host: 127.0.0.1
port: 3306
username: root
password_env: MYSQL_PWD
database: shop
output: shop.sql
incremental_backup:
enabled: true
max_chain_depth: 3
binlog_check: true
mysql:
binlog_path: ./binlogs
enabled: trueturns on chain planning. Without it every backup is a standalone full.max_chain_depth: 3caps how far a chain grows before Sentinel starts a fresh one. The default when omitted is 6. A low value here makes the reset observable inside one tutorial.mysql.binlog_pathpoints at the directory to archive. A relative path is resolved against the working directory. This key is required whenever incremental backup is enabled on a MariaDB or MySQL job.binlog_check: truedeclares that binary logging must be on. Sentinel accepts and stores the flag, but see the caution below: it cannot currently fail.
mysql: even on MariaDBThere is no mariadb: key. Both engines share one configuration block, one prerequisite check, and
one set of error messages, all of which say mysql. A message reading
mysql.binlog_path is required for incremental backup (type=mariadb) is Sentinel telling you the key
name and the job type in the same breath, not a misconfigured job type.
binlog_check can never fire, and MariaDB is where that hurtsThe prerequisite function that would report log_bin_off is called with its "binary logging is
enabled" argument hard-coded to true, so the only thing actually checked is binlog_path. On MySQL
that gap is mostly harmless because logging is on by default. On MariaDB, where it is off by default,
a server that logs nothing at all passes configuration validation and fails later, at the archival
step, with no_binlog_files_found. Treat Step 1's SHOW VARIABLES check as the real check.
Step 3: Watch the validator enforce binlog_path
These are worth provoking once so you recognise them later. All were captured by running them.
Remove the mysql: block entirely and validate:
sentinel config validate --config sentinel.yaml
You should see:
Error: invalid config "sentinel.yaml": backup 'shop': mysql.binlog_path is required for incremental backup (type=mariadb)
Point it at a directory that does not exist and validate again:
Error: invalid config "sentinel.yaml": backup 'shop': mysql.binlog_path must exist and be locally mounted (type=mariadb)
Restore the working version, and validation passes:
2026/08/05 20:58:09 WARN TLS not configured for database event=tls_not_configured database=shop
configuration is valid
The remaining codes the same check can produce are binlog_path_invalid (the path cannot be made
absolute), binlog_path_unreadable (it exists but cannot be stat'ed), and binlog_path_not_directory
(it is a file).
Step 4: Build a chain
Take three backups, changing the data between them so each has something new to capture:
sentinel backup --config sentinel.yaml
mariadb -h 127.0.0.1 -P 3306 -u root shop -e \
"INSERT INTO orders (customer_id, total) VALUES (1, 250.00);"
sentinel backup --config sentinel.yaml
mariadb -h 127.0.0.1 -P 3306 -u root shop -e \
"INSERT INTO orders (customer_id, total) VALUES (2, 12.75);"
sentinel backup --config sentinel.yaml
Each prints Backup complete ! as before. The interesting output is in the history:
sentinel monitor list --config sentinel.yaml
What to look for: the CHAIN and DELTA columns, empty until now, become populated.
- The rows from the earlier pages still show
-inCHAIN. Backups taken before incremental was enabled belong to no chain and are never adopted into one. - The first backup after enabling is
full, at chain index#0. A chain always starts with a full. - The next two are
incremental, at#1and#2in the samechain-<number>, and they carry aDELTAbyte count where the full shows-.
The chain identifier is printed as chain-<id>#<index>.
Step 5: Find the binary-log archive
Look in the backup directory:
ls backups/
Alongside shop.sql and shop.sql.manifest.json there is now shop.sql.binlogs.tar. The name is
the artifact's file name with .binlogs.tar appended, and the archive is written into the same
directory as the artifact. The naming is identical on both engines; only the files inside differ.
tar -tf backups/shop.sql.binlogs.tar
What to look for: one entry per mariadb-bin.NNNNNN file, stored flat with no directory prefix,
in sorted file-name order. The mariadb-bin.index file is deliberately excluded.
The archive is recorded in the manifest, under advanced_restore.incremental_lineage:
cat backups/shop.sql.manifest.json
Three fields there are populated only for MariaDB and MySQL:
| Field | Meaning |
|---|---|
binlog_start_file | the first log file in the archive |
binlog_end_file | the last one |
binlog_artifacts | the path of the tar archive itself |
The full backup at chain index #0 gets no archive; only runs Sentinel classifies as incremental
trigger the packing step. And the archive is a copy of every binary log currently in the directory,
not the slice written since the previous backup: Sentinel does not track a starting position and does
not purge the source. A long-lived server will archive the same early logs into every incremental
artifact until you expire them server-side with PURGE BINARY LOGS or expire_logs_days.
Step 6: Watch the chain reset
max_chain_depth: 3 lets a chain reach index 3, then starts a new one. Take two more backups:
mariadb -h 127.0.0.1 -P 3306 -u root shop -e \
"INSERT INTO orders (customer_id, total) VALUES (3, 5.00);"
sentinel backup --config sentinel.yaml
mariadb -h 127.0.0.1 -P 3306 -u root shop -e \
"INSERT INTO orders (customer_id, total) VALUES (1, 7.25);"
sentinel backup --config sentinel.yaml
sentinel monitor list --config sentinel.yaml
What to look for: the first of the two extends the existing chain to #3. The second starts a
new chain-<number> at #0 with a full backup, because the previous index had reached
max_chain_depth. So a depth of 3 yields chains of four artifacts: one full and three incrementals.
Where incremental support stops in v1.4.0
Three things this page deliberately does not tell you to do, because they do not work.
The chain subcommands cannot be invoked
backup chain-status, backup chain-list, and backup force-full all require --config, but the
flag is registered only on the parent backup command's local flag set, so the subcommands never
receive it:
$ sentinel backup chain-status --job shop --config sentinel.yaml
Error: unknown flag: --config
$ sentinel backup chain-status --job shop
Error: --config is required
There is no ordering of the arguments that satisfies both. Until this is fixed, read chain state from
sentinel monitor list and sentinel monitor show as Steps 4 and 6 do, and reset a chain by
lowering max_chain_depth rather than by forcing a full.
An incremental restore job validates, dry-runs cleanly, and then fails
This is the sharpest edge on the whole track, because nothing warns you until the run itself.
Add a restore job in incremental mode to the restores block:
shop-chain:
type: mariadb
enabled: true
host: 127.0.0.1
port: 3306
username: root
password_env: MYSQL_PWD
database: shop_chain
schedule: "0 5 * * 0"
restore_mode: incremental
incremental_from_backup: backups/shop.sql
backup_source:
type: local
local_path: ./backups
backup_path: shop.sql
incremental_from_backup is required whenever restore_mode: incremental; omit it and validation
fails with incremental_from_backup is required when restore_mode is incremental.
Now validate:
sentinel config validate --config sentinel.yaml
You should see:
configuration is valid
The validator whitelists all four engines for incremental mode. It does not object.
Dry-run it:
sentinel restore dry-run shop-chain --config sentinel.yaml
You should see:
Dry-run: Job "shop-chain"
Type: mariadb
Database: shop_chain
Backup Source Type: local
Backup Path: shop.sql
Restore Mode: incremental
Timeout: 0 seconds
NOTE: This is a dry-run. No data will be restored.
The dry run does not object either, because it never calls the planner.
Ask the planner directly and the picture changes:
sentinel restore validate-chain shop-chain --config sentinel.yaml
You should see:
Error: chain validation failed: status=rejected reason=unsupported_database_type
And running it produces the same verdict, one layer further out:
$ sentinel restore run shop-chain --config sentinel.yaml
Error: restore execution failed: restore planning rejected: unsupported_database_type
The rejection is unconditional. The incremental planner returns unsupported_database_type for any
engine that is not postgres, before it looks at your baseline, your manifest, or your chain. No
value of incremental_from_backup and no shape of chain changes it.
The failure is recorded, which is the one consolation:
$ sentinel restore history shop-chain --config sentinel.yaml
RESTORE | DATABASE | MODE | PLAN | STATUS | DURATION | TIMESTAMP | REASON | FALLBACK
shop-chain | shop_chain | incremental | rejected | failed | 10ms | 2026-08-05T20:57:53Z | unsupported_database_type | none
PLAN is rejected and REASON carries the code, so a scheduled job that starts failing this way
is visible in the history rather than silent.
So the binary logs cannot be replayed
The replay machinery exists, and on MariaDB it carries an extra hazard worth knowing about. Sentinel
picks its client tool by engine, so a MariaDB replay would pipe into mariadb rather than mysql.
But the tool producing the SQL is hard-coded to the name mysqlbinlog for both engines, and MariaDB
11 ships that program as mariadb-binlog. A MariaDB host with only the modern names would therefore
fail with required_tool_missing: mysqlbinlog even if the planner allowed the restore.
It does not allow it, so that failure is unreachable, and so is the replay. To recover data on
MariaDB today, restore an artifact with a plain full job as on the restore page,
and if you need the binary logs applied on top, run mariadb-binlog yourself against the archive
Sentinel produced. The archive is a plain tar of standard binary-log files, so nothing about it is
Sentinel-specific.
Incremental backup is configured through the same incremental_backup block on every engine, but
the mechanism differs: PostgreSQL uses WAL summarisation and is the only engine whose incremental
restore is planned, MariaDB and MySQL use binary logs, and MongoDB uses the oplog and requires a
replica set. See Incremental and PITR.
What just happened
Enabling incremental_backup changed what Sentinel records around each dump and added one step after
it; it did not change how the dump is produced. Before each run Sentinel reads the job's recent
successful executions, finds the newest one carrying a chain ID, and decides whether to start a new
chain or extend the existing one. When the decision is "extend", it packs the binary-log directory
into a tar archive beside the artifact and records the archive in the manifest. The restore planner
reads that manifest back, and, on MariaDB, declines to act on it.
Next
- Point-in-time recovery: the third restore mode, and the two separate walls that stop it on MariaDB.
{/* sources: internal/config/types.go, internal/config/validator.go, internal/config/marshal.go, internal/cli/backup.go, internal/cli/backup_factory.go, internal/cli/monitor.go, internal/cli/restore.go, internal/domain/backup/incremental/prerequisites.go, internal/domain/backup/pipeline.go, internal/domain/restore/planner.go, internal/domain/restore/executor.go, internal/adapters/restore/incremental/mysqlbinlog/archive.go, internal/adapters/restore/incremental/mysqlbinlog/replay.go, internal/ports/manifest.go, infra/docker/docker-compose.yml */}