Skip to main content

bdbackup

Backups by design.

bdbackup is a command-line backup tool and Python library for MySQL, MariaDB and files on Linux and macOS. It takes MySQL and MariaDB backups with mysqldump, Percona XtraBackup and MariaDB Backup (full and incremental physical backups, compressed and optionally encrypted), archives files and directories as tar, tar.gz or tar.zst, and rotates the results across storage stages with GFS (grandfather-father-son) retention. Every backup is verified before it is published and recorded with a SHA-256 checksum in an SQLite backup history, from which a guided restore prepares a recovery copy. Jobs live in one TOML file and run from cron.

Features

Documentation

Document Covers
File and directory backups with tar file jobs, template files, exclusion templates, restoring an archive
MySQL and MariaDB logical backups with mysqldump mysqldump jobs, credentials, default options
MySQL and MariaDB physical backups with Percona XtraBackup and MariaDB Backup xtrabackup jobs, full and incremental backups, encryption, preparing a recovery copy, engine pruning
GFS (grandfather-father-son) backup rotation stages, preparing stage directories, safety rules, run and step logs, ledger copies
Custom backup engines (plugins) the engine registry and writing your own engine
Annotated configuration every config key with a comment

Support and safety

Python 3.12+ on POSIX systems (Linux/macOS); Windows is not supported. tar and tar.gz work on every supported Python; tar.zst requires Python 3.14 with Zstandard support. Database binaries are separate system dependencies.

Every built-in backend verifies staging output before publishing it. A missing or unreadable file fails the backup and preserves the previous archive. A process lock prevents concurrent writers to the same file destination or physical root. --no-verify skips only the additional verification pass after publication. Verification checks archive readability or physical checkpoints; it does not replace testing a restore. File backups are not filesystem snapshots: quiesce applications or back up snapshots when files can change during a run.

Safe relative symlinks, hardlinks, empty directories and ordinary directory permissions/timestamps round-trip. Restore rejects escaping paths/links and special files on all supported Python versions. Ownership and privileged permission bits are intentionally not restored. Use a destination that is not being modified by another process during extraction.

Installation

Install the current code from the original GitHub repository:

pip install "git+https://github.com/ayder/bdbackup.git"

From PyPI:

pip install bdbackup

For development:

git clone https://github.com/ayder/bdbackup.git
cd bdbackup
pip install -e ".[dev]"

Commands

bdbackup has three commands; everything else is an option:

bdbackup run      -c CONFIG -j JOB [--full | --incremental] [--no-verify] [--dry-run]
bdbackup run      -c CONFIG --validate [-j JOB]
bdbackup run      -c CONFIG -j GFS_JOB --initialize-structure
bdbackup restore  -c CONFIG [--backup-id N] [--job JOB] [-y] [-d DIR] [--encrypt-key-file KEY]
bdbackup restore  --archive ARCHIVE -d DIR
bdbackup restore  --backup BACKUP --root ROOT -d DIR [--binary BINARY] [--encrypt-key-file KEY]
bdbackup history  -c CONFIG [--job JOB] [--successful | --create-checksum]

-c is --config on every command; -j is the short form of --job on run only (restore and history take --job). bdbackup itself takes only --version, --help and --logging LEVEL, placed before the command: bdbackup --logging DEBUG run -c config.toml -j files-daily. Levels: DEBUG, INFO, WARNING, ERROR.

The CLI uses these exit codes:

Code Meaning
0 Success
1 Backup/verify failure (including a job whose backend rejects its settings in a real run), or validation checks failed/incomplete
2 Usage error, an inactive job, or a configuration error found when the config loads or by --dry-run
3 Lock held; for GFS, another run of the job is in progress or only deferred work remains

Config-driven jobs

Every backup is a job in a TOML config file. Convention: keep all bdbackup settings under ~/.config/bdbackup/ (honours $XDG_CONFIG_HOME / $BDBACKUP_CONFIG_DIR) — config.toml, the files.template path lists, templates/*.py exclusion presets, and engines/*.py database engines.

# ~/.config/bdbackup/config.toml
[files-daily]
type = "file"
template_filename = "~/.config/bdbackup/files.template"
backup_dst = "/backup/files/daily"
chdir = "/srv/www"   # source path: template/exclude entries resolve against it
format = "tar.gz"  # tar.zst requires Python 3.14+
exclude_pattern = ["*.log", "node_modules"]
exclude_templates = ["python-dev"]

[mysql-prod]
type = "xtrabackup"
backup_root = "/backup/mysql/production"
user = "xtrabackup"
retention_days = 7
parallel = 2

[mysqldump-all]
type = "mysqldump"
out_dir = "/backup/mysql/dumps"
user = "backup"
options = ["--single-transaction", "--all-databases"]

Configuration paths expand ~; relative config paths resolve against the config file's directory. Template entries and exclude entries resolve against chdir (or the process working directory if omitted). For a single-database mysqldump job, set database = "mydatabase"; for all databases include --all-databases in options.

Except for the optional [history] settings, each top-level table is one job; its keys (except type, active and restore_root) are passed to the backend constructor, so they use Python-style underscores (exclude_templates). See config.toml.example for a fully annotated config with every option explained.

Run one job:

bdbackup run -c ~/.config/bdbackup/config.toml -j mysql-prod

An xtrabackup job also takes --incremental; see MySQL and MariaDB physical backups.

Dry run

--dry-run reports what a job would do: it runs no backup tool, writes no backup and records no history; a GFS dry run creates only its job's lock file beside the history database. What each job type reports is described with it: file jobs, mysqldump jobs, xtrabackup jobs and GFS jobs.

A dry run exits 1 where the real run would fail before writing anything (an incremental without a full, a file job that selects nothing). --dry-run cannot be combined with --verify or --no-verify.

Pausing a job

Set active = false in a job's table to pause it. Every run -j of that job, including --dry-run and --validate -j, then exits 2 with Job '<name>' is inactive (active = false), and a full --validate names it as not checked. active defaults to true. Configuration rules still apply to an inactive job, and GFS still rotates its backups.

Scheduling with cron

bdbackup does not install schedules. Add crontab lines yourself, run as the OS account the backups belong to. For example, a weekly full on Sunday, incrementals on the other days, a nightly file backup, and GFS after the backups:

0 2 * * 0   bdbackup run -c /opt/dbs/config.toml -j mysql-prod
0 2 * * 1-6 bdbackup run -c /opt/dbs/config.toml -j mysql-prod --incremental
30 2 * * *  bdbackup run -c /opt/dbs/config.toml -j files-daily
0 4 * * *   bdbackup run -c /opt/dbs/config.toml -j gfs-main

Times use the cron daemon's timezone, and separate lines do not wait for each other: leave enough time for backups before GFS. Add --dry-run to the GFS line until its report looks right.

Validate configuration and permissions

Check all configured jobs without creating a backup or running retention:

bdbackup run -c /opt/dbs/config.toml --validate
# Or validate only one job:
bdbackup run -c /opt/dbs/config.toml --validate -j mysql-prod

--validate cannot be combined with --full, --incremental, --verify, --no-verify or --dry-run. Run validation as the OS account that will run your cron jobs. It checks backend settings, required executables, destination access, SQLite/recovery directories, file-template sources, and encryption key requirements. Missing directories are reported with mkdir -p guidance and the required OS-user permissions. A missing directory that the backend can create under a writable parent is reported as creatable; validation does not create it.

For MySQL jobs, the installed mysql/mariadb client authenticates using the job's credentials and reads CURRENT_USER(), server version, datadir and SHOW GRANTS. Passwords are passed through a temporary mode-0600 options file, removed afterward. No CREATE USER or GRANT is executed. Missing privileges produce SQL for an administrator, for example:

GRANT RELOAD, BACKUP_ADMIN, REPLICATION CLIENT, PROCESS, LOCK TABLES
ON *.* TO 'xtrabackup_user'@'localhost';
GRANT SELECT ON `performance_schema`.`log_status`
TO 'xtrabackup_user'@'localhost';

The helper also checks the performance-schema tables used by current Percona XtraBackup and prints CREATE TABLESPACE separately as an optional privilege for importing individual tables. MariaDB Backup gets its own privilege recommendations. See Percona privileges and MariaDB Backup privileges.

Checks cover direct grants; role-derived privileges and partial revokes may need manual review. Physical datadir checks cover root-directory access, not every data file or external tablespace. Custom mysqldump options, routine visibility, GTID settings and exact server/binary version compatibility still need review. For a GFS job, every stage path after the first must exist and be writable, and its .bdbackup-destination marker must be present and writable; a missing marker's line names the --initialize-structure command that creates it. An unreachable database or missing client is a failed/incomplete check, not a pass. Validation does not add SQLite history rows. It exits 0 when its checks pass, 1 for failed or incomplete checks, and 2 for invalid command or configuration syntax.

Backup history and guided restore

Enable SQLite history in your TOML configuration. No extra Python dependency or database server is required:

[history]
database = "state/history.sqlite3"
restore_root = "/restore/bdbackup"

Both paths expand ~; relative paths resolve against the configuration file. restore_root defaults to restores beside that file. Omitting [history] disables history. File jobs automatically exclude this live database and its SQLite journal files; keep it outside backup inputs when possible. New history databases are created with permissions 0600.

run records every backup attempt of a configured job:

bdbackup run -c config.toml -j files-daily
bdbackup history -c config.toml
bdbackup history -c config.toml --job files-daily --successful

Each record contains an ID, job name, backup type, UTC start/completion times, status (running, success, or failed), and the successful artifact's absolute path and size. Physical full and incremental runs record their respective types and the root/binary needed for preparation. Credentials and raw error messages are not stored; failed rows contain only the exception class. Restores, validation, GFS runs and dry runs are not backup attempts and create no backup-attempt rows. Direct Python backend calls are not automatically recorded; applications can wrap them with History.run.

Every successful backup also records its unit and a SHA-256 checksum taken right after the backup, which costs one extra read of the backup. A file's checksum is the SHA-256 of its content. A directory's checksum is the SHA-256 of a manifest with one line per regular file, <sha256> <relative path>, sorted by path; a symlink or special file inside a physical backup fails the backup. The unit is what a rotation tool moves as one piece: the archive or dump file itself, or for xtrabackup the dated directory that holds the full and all of its incrementals. bdbackup history shows it as unit <path>, or unit - for records without one. For units that GFS manages, the line continues with | stage <stage path> | locations <path>, …, or | deleted <time> once GFS has removed the unit. Reproduce a directory checksum on Linux (use shasum -a 256 on macOS):

cd /backup/mysql/production/2026-09-29/Full_<id> && find . -type f -print0 \
  | LC_ALL=C sort -z | xargs -0 sha256sum | sed 's|  \./|  |' | sha256sum

Records written by bdbackup 0.6.2 or earlier have no checksum. Record them explicitly:

bdbackup history -c config.toml --create-checksum
bdbackup history -c config.toml --create-checksum --job mysql-prod

It prints <id>: recorded, <id>: skipped: unavailable for an artifact that is missing or was replaced, or <id>: skipped: unexpected unit for a physical backup outside its dated layout, and then exits 1. A checksum already recorded is never changed. --create-checksum cannot be combined with --successful. The checksum lives in the checksum column of the backup_runs table.

History uses schema version 5. A database at an older schema upgrades on the next write; bdbackup 0.6.2 or earlier refuses a version-5 database.

History is written before work starts, and success only after the backup and its requested verification finish. If history cannot be written, the command fails; an artifact already created is preserved. A forcibly killed process may leave a running row, which is never offered for restore. Existing backups are not imported automatically.

Select a successful backup interactively:

bdbackup restore -c config.toml
bdbackup restore -c config.toml --job files-daily

Each backup job may set restore_root = "/restore/mysql-prod" to override [history] restore_root for its guided-restore suggestions. It must be a nonempty path string; ~ expands and relative paths resolve against the config file. Job-level restore_root requires [history]. Validation checks that each job's root is writable or can be created. Physical jobs usually need a separate root with room for the full and its incrementals, staged near the database datadir.

The current config's job is matched by the history record's job name. If that job has no restore_root, or is no longer configured, the history root is used. An explicit --dst always wins.

Restore lists successful, available artifacts, asks for the backup ID, suggests <restore_root>/<job>-<id>, and asks for confirmation. You can edit the suggested path. Repeated restores suggest a numbered alternative; history-based restore requires a new directory even when --dst is supplied. For automation, specify the exact backup ID and use --yes:

bdbackup restore -c config.toml --backup-id 12 --dst /restore/job-12 --yes

What a restore produces depends on the job type; see file jobs, mysqldump jobs, xtrabackup jobs and custom engines.

Deleted artifacts remain in history as unavailable. File identity, size, and modification time detect replaced archives, so older rows for a reused filename are not offered as older recovery points. Availability uses these file checks. Restore checks the recorded checksum of every file it uses for backups that GFS manages (see GFS backup rotation); for other backups it validates the actual archive or physical dependency chain. History stores references to artifacts and does not preserve an archive that a later backup replaces.

Development

Run tests and linting:

pytest
ruff check bdbackup tests scripts

Optional database integration tests use disposable, network-isolated Docker containers with synthetic data. They remove only the containers and volumes created for that run. Pull the matching images before running:

python scripts/integration_mysql.py           # mysql:8.4
python scripts/integration_physical.py percona # percona-server/xtrabackup:8.4
python scripts/integration_physical.py percona-encrypted # AES256 + zstd recovery
python scripts/integration_physical.py mariadb # mariadb:11.4

Physical recovery returns a prepared data directory; copying it into a server's data directory, assigning ownership to the server user, and starting that server are separate administrator actions. Use the same database version and filesystem case-sensitivity as the source.

pyproject.toml (project.version) is the sole version source. The package's __version__ and CLI read installed distribution metadata generated from it. After changing the version, run uv lock to refresh the generated lockfile and reinstall with pip install -e '.[dev]' to refresh local metadata. Source development requires this editable installation.

Build and validate a release:

python -m build
twine check dist/*
python scripts/smoke_install.py dist

Commit the validated changes and create an annotated tag named v<project.version>. The publishing workflow accepts only that matching tag. It runs the reusable CI workflow on that commit (tests on Python 3.12–3.14, lint, real MySQL/Percona/MariaDB recovery tests, source/wheel build, Twine, and an installed-wheel recovery smoke test), then uploads those exact artifacts. Configure the PyPI trusted publisher for ayder/bdbackup, publish.yml, environment pypi before releasing. Account configuration and actual database recovery evidence are release prerequisites.

License

MIT License. See LICENSE.

Metadata

Release files for bdbackup 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bdbackup 0.7.0
File Size Uploaded
bdbackup-0.7.0.tar.gz 121.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bdbackup 0.7.0
File Interpreter ABI Platform
bdbackup-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 191.4 kB

Release files / bdbackup-0.7.0.tar.gz

Download URL bdbackup-0.7.0.tar.gz
Size 121.5 kB
Tags Source
SHA-256 checksum
How to use checksums
78dde5ba0ee81e74ee9e7b421a1e7192b0ff216266f6bbedef05e1a061e287c4
BLAKE2b-256 checksum
How to use checksums
a7b57f669a604cce015e788af97ad82465ac8a9ca7fc7c3e64261695c6578065
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / bdbackup-0.7.0-py3-none-any.whl

Download URL bdbackup-0.7.0-py3-none-any.whl
Size 69.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ad55a5bff45527c12785ca9fc9f63244c6c42ef97f681635f6a043eaa2118eaf
BLAKE2b-256 checksum
How to use checksums
e75aac074693bf1fc457b896d3d77449d3e06c0de06ba976d5639043801eb691
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page