Working Features:
- PostgreSQL full backup using
pg_basebackupwith WAL streaming - PostgreSQL differential backup by collecting archived WAL files from a configured
archive_directory - PostgreSQL restore pipeline from full backup + archived WAL replay (
restore_command,recovery.signal, startup,SELECT 1) - PostgreSQL PITR restore with
--target-time, catalog-based base backup selection, andrecovery_target_time - PostgreSQL automated end-to-end restore validation after full backups:
temporary instance, restore, per-table
COUNT(*), checksum comparison, andvalidation.jsonresult - Basic PostgreSQL WAL validation (sequence gaps, timeline consistency, file sanity checks)
- MySQL full backup using
xtrabackup(Percona XtraBackup) - MySQL differential backup command implemented via
xtrabackup --incrementalagainst the last full backup - Minimal MySQL restore from an XtraBackup directory (
prepare,copy-back,mysqld,mysqladmin ping) - Encrypted credential profiles (MySQL login-path, PostgreSQL .pgpass)
- SQLite-backed backup catalog tracking and versioned per-backup metadata/validation artifacts
- Optional single
.tar.zstarchive creation for full backups whenzstdis available - Local Web UI for dashboard, backup execution, restore validation, job logs, and metadata/validation viewing
Not Yet Implemented:
- Cloud storage integration (CLI placeholders exist, upload implementation does not)
- MySQL restore from compressed
.qpartifacts or single.tar.zstarchives - Production-grade error handling
- Stable user-facing PostgreSQL incremental backup workflow
Known Issues:
- Direct filesystem access is required for MySQL physical backups and PostgreSQL archived WAL access
- Limited testing across different PostgreSQL/MySQL versions
- No automated cleanup of old backups
- WAL validation is basic; PostgreSQL full backups now also use restore validation
- Naive PITR target times are interpreted as UTC; timestamps with offsets are normalized to UTC
This utility is a work in progress and not recommended for production use at this time.
The utility uses native database backup tools instead of SQL dumps:
PostgreSQL:
- Full backup:
pg_basebackupwith tar format and gzip compression - Differential backup: copies archived WAL files from a configured
archive_directory - WAL chain validation (sequence gaps, timeline consistency, file integrity β basic)
- Requires REPLICATION privilege,
wal_level = replica, and access to the configured WAL archive directory
MySQL:
- Full backup:
xtrabackupphysical backup with compression - Differential backup command:
xtrabackup --incremental --incremental-basedir=<last full backup> - Minimal restore command:
xtrabackup --prepare,xtrabackup --copy-back, start isolatedmysqld - Requires Percona XtraBackup installed and access to the MySQL data directory
Backups now use a unified metadata contract and a SQL-backed catalog layer.
metadata.jsonis versioned and stores normalized backup fields such asengine,backup_type,parent_backup_id,base_backup_id, artifact paths, LSN/binlog markers, checksums, manifest/checkpoint pointers, and validation state.validation.jsonis generated alongsidemetadata.jsonand records schema validation, required artifact presence, marker summary, and recovery status.backup_catalog.dbis the source of truth for backup history and chain resolution.- If
backup_catalog.dbis empty and a legacybackup_catalog.jsonexists, catalog records are imported automatically on first use. - Backup chain lookup is now filtered by
database_name, so parent/base backup resolution does not mix different databases that share the same catalog.
Each completed backup is persisted through the same contract layer.
- PostgreSQL metadata includes LSN/WAL markers when available.
- MySQL metadata includes binlog markers when available.
- Artifact checksums are calculated for key backup files.
- Required artifacts are validated per engine and backup type before the backup is marked as completed in the catalog.
- PostgreSQL full backups run an automated test-restore by default. The restored database is compared with the source database using per-table row counts and deterministic row JSON checksums.
- The restore validation report is embedded in
validation.jsonunderrestore_validation; the compact status is mirrored inrecovery_status. - If backup output directory creation succeeded but later stages fail, the backup can still produce formal
metadata.json/validation.jsonrecords with failed validation state.
- Python 3.10 or higher
- PostgreSQL client tools (pg_basebackup)
- MySQL: Percona XtraBackup 8.0
- Optional:
zstdandtarfor single-file.tar.zstarchives - User permissions:
- PostgreSQL: REPLICATION privilege
- PostgreSQL: Access to the configured WAL archive directory
- MySQL: Standard backup privileges + read access to data directory
git clone https://github.com/<your_user>/<repo_name>.git
cd <repo_name>
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -r requirements.txtThe utility supports three configuration methods:
Most secure option - Credentials are encrypted and never exposed in environment variables or command line.
MySQL uses mysql_config_editor to store encrypted credentials in ~/.mylogin.cnf:
# Create a login-path profile
mysql_config_editor set --login-path=xtrabackup \
--host=localhost \
--user=backup_user \
--password
# Enter password when prompted
# Verify the profile (password is obfuscated)
mysql_config_editor print --all
# Test connection
mysql --login-path=xtrabackup -e "SELECT VERSION();"PostgreSQL uses ~/.pgpass file for password storage:
# Create .pgpass file in your home directory (~)
echo "localhost:5432:*:backup_user:your_password" >> ~/.pgpass
# Set correct permissions (required)
chmod 0600 ~/.pgpass
# Test connection (no password prompt)
psql -h localhost -U backup_user -d postgresπ Replace backup_user and your_password with your actual credentials.
More information: PostgreSQL .pgpass documentation
Usage with profile configuration:
# PostgreSQL with .pgpass
python cli/dbtool.py backup --db postgres --database mydb \
--storage local --config profile
# MySQL with login-path
python cli/dbtool.py backup --db mysql --database mydb \
--storage local --config profileπ Use --config profile parameter to read encrypted credentials.
Traditional method using a local .env file. Create it manually in the project root (an .env.example template is not currently committed).
Configure .env with your database credentials:
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=yourpassword
DB_NAME=yourdatabaseUsage:
python cli/dbtool.py backup --db postgres --database mydb \
--storage local --config filePGPASSWORD environment variable is not recommended because other users can see process environment variables.
PostgreSQL 18 / Environment Variables
Pass credentials directly via command line (least secure):
python cli/dbtool.py backup --db postgres --database mydb \
--storage local --config manual \
--host localhost --port 5432 \
--user backup_user --password secretGrant replication privilege:
ALTER USER your_user REPLICATION;Ensure wal_level is set correctly in postgresql.conf:
wal_level = replica
Configure WAL archiving so PostgreSQL copies segments into an archive directory:
archive_mode = on
archive_command = 'cp %p /path/to/archive/%f'
Restart PostgreSQL after configuration changes.
On first PostgreSQL run, the utility asks you to confirm or save that archive_directory in ~/.backup_utility/config.json.
Install Percona XtraBackup:
# Ubuntu/Debian
wget https://repo.percona.com/apt/percona-release_latest.generic_all.deb
sudo dpkg -i percona-release_latest.generic_all.deb
sudo apt-get update
sudo apt-get install percona-xtrabackup-80
# macOS
brew install percona-xtrabackup
# Verify
xtrabackup --versionOperations that require direct filesystem access need elevated privileges:
Operations requiring sudo/elevated permissions:
- MySQL
xtrabackupfull or differential backup (reads MySQL data files) - PostgreSQL differential backup when the configured WAL archive directory is protected by OS permissions
Operations NOT requiring sudo:
- PostgreSQL
pg_basebackupfull backup (uses replication protocol)
macOS/Linux:
# MySQL full backup (requires sudo)
sudo python cli/dbtool.py backup --db mysql --database mydb --storage local --config profile
# PostgreSQL differential backup (may require sudo or postgres user)
sudo python cli/dbtool.py backup --db postgres --database mydb --storage local --config profile
# PostgreSQL full backup (no sudo needed - uses replication protocol)
python cli/dbtool.py backup --db postgres --database mydb --storage local --config profileWindows:
- Run Command Prompt or PowerShell as Administrator
- Or grant read permissions to database data directories for your user account
Alternative to sudo:
- Run as database user:
sudo -u postgres python cli/dbtool.py ...orsudo -u mysql python cli/dbtool.py ... - Grant read permissions to the MySQL data directory / configured PostgreSQL archive directory (one-time setup)
Security Note: When using sudo with encrypted profile configuration, credentials remain secure and are never exposed in process lists.
# Full database backup
full database -path /path/to/backups
# PostgreSQL full backup with explicit restore validation flags
full database -path /path/to/backups \
-validate-restore true \
-restore-validation-port 0 \
-restore-validation-checksums true
# Differential backup
# PostgreSQL: archived WAL copy with basic validation
# MySQL: xtrabackup incremental from the last full backup
differential backup
# PostgreSQL restore from interactive console
restore -path /path/to/full_backup_dir -data-dir /path/to/new_restore_data -wal-path /path/to/archive_directory
# PostgreSQL PITR restore from interactive console
restore -target-time "2026-05-04 12:30:00" -data-dir /path/to/new_restore_data -wal-path /path/to/archive_directory
# Execute SQL query
SQL SELECT * FROM users WHERE id < 100
# Export query results to CSV
SQL SELECT * FROM users -extract -path /tmp/exports
# Show help
help
# Exit
exitThe project also includes a lightweight local control panel built on Python's standard HTTP server. It does not require Flask, FastAPI, or frontend build tooling.
source .venv/bin/activate
python -m webui.serverOpen:
http://127.0.0.1:8765
The Web UI provides:
- backup catalog dashboard from
backup_catalog.db - PostgreSQL/MySQL full backup form
- PostgreSQL test-restore validation form
- background job status and live job logs
metadata.jsonandvalidation.jsonviewer
For PostgreSQL backups, pass the WAL archive directory in the form. The web server does not use the CLI prompt flow for WAL archive configuration.
For PostgreSQL restore, -wal-path should point to the WAL archive directory used by restore_command, not to a single WAL segment file.
Backups are created directly under the path passed to full database -path .... Differential backups are created as sibling directories next to the last full backup. Exact contents depend on the database type:
/backups/
βββ full_mydb_20251105_150000_a1b2/
β βββ metadata.json
β βββ validation.json
β βββ base.tar.gz # PostgreSQL full backup
β βββ pg_wal.tar.gz # PostgreSQL WAL at backup time
β βββ backup_manifest # PostgreSQL 13+, optional
β βββ xtrabackup_checkpoints # MySQL full backup metadata
β βββ ...
βββ full_mydb_20251105_150000_a1b2.tar.zst # Optional single archive
βββ differential_mydb_20251105_160000_c3d4/
βββ base_backup_id.txt
βββ metadata.json
βββ validation.json
βββ 0000000100000000000000A1 # PostgreSQL archived WAL files
βββ xtrabackup_checkpoints # MySQL differential backup metadata
βββ ...
Other on-disk artifacts:
backup_catalog.dbstores backup history across runsbackup_<database>.logstores per-database logs
metadata.json and validation.json are now part of the normal backup contract, not optional diagnostics. Restore and automated test-restore logic consume these files together with backup_catalog.db.
PostgreSQL full backups run an end-to-end validation pipeline by default.
What the validation does:
- captures source table signatures for the selected database
- creates a temporary PostgreSQL data directory
- restores the backup into a temporary PostgreSQL instance on a separate port
- runs SQL verification against the restored instance
- compares source and restored table lists,
COUNT(*), and table checksums - stops and removes the temporary restore instance unless
-restore-validation-keep-data trueis passed - stores the full result in the backup's
validation.json
Useful flags:
full database -path /backups/postgres -validate-restore true
full database -path /backups/postgres -restore-validation-port 55440
full database -path /backups/postgres -restore-validation-checksums false
full database -path /backups/postgres -restore-validation-keep-data trueIf restore validation is enabled and fails, the backup contract is marked as failed even if pg_basebackup created files successfully. This is intentional: a backup that cannot be restored is not treated as a valid backup.
For high-write databases, run validation during a quiet/maintenance window or expect legitimate source-vs-restored mismatches from writes committed after the backup recovery point.
Minimal PostgreSQL restore is now available through a dedicated CLI command. It is intentionally scoped to the current PostgreSQL full backup format produced by this utility: base.tar.gz + pg_wal.tar.gz, plus archived WAL files reachable through restore_command.
python cli/dbtool.py restore --db postgres --database postgres \
--backup-path /path/to/full_backup_dir \
--data-dir /path/to/new_restore_data \
--archive-dir /path/to/archive_directory \
--restore-port 55432For PITR, pass --target-time. Timestamps without timezone are treated as UTC. Timestamps with an offset are normalized to UTC before backup selection and before writing recovery_target_time. If --backup-path is omitted, the utility selects the newest completed PostgreSQL full backup for the database whose backup completion time is at or before the target time:
python cli/dbtool.py restore --db postgres --database postgres \
--target-time "2026-05-04 12:30:00" \
--data-dir /path/to/new_restore_data \
--archive-dir /path/to/archive_directoryWhat this command does:
- extracts the full backup into a new PostgreSQL data directory
- replays WAL through
restore_commandfrom the supplied archive directory - when
--target-timeis set, writes UTC-normalizedrecovery_target_time,recovery_target_action = 'promote', andrecovery_target_timeline = 'latest' - creates
recovery.signal - starts PostgreSQL with
pg_ctl - verifies the restored instance with
SELECT 1;
If --archive-dir is omitted, the utility tries to use the saved PostgreSQL WAL archive path from ~/.backup_utility/config.json.
Minimal MySQL restore is available for an already-expanded XtraBackup directory:
python cli/dbtool.py restore --db mysql \
--backup-path /path/to/mysql_backup \
--data-dir /tmp/mysql_restore \
--port 3308The backup directory must contain xtrabackup_info, xtrabackup_checkpoints, ibdata1, and mysql/. If those artifacts are stored as .zst files, the restore flow recursively decompresses them with zstd -d --keep before validation and prepare. It then runs xtrabackup --prepare, xtrabackup --copy-back, adjusts ownership when possible, starts an isolated mysqld on 127.0.0.1:<port>, and verifies it with mysqladmin ping.
Current Limitations:
- No partial/table-level backups (full database only)
- PostgreSQL differential backup depends on a preconfigured WAL archive directory
- No retention policy management
- No background scheduling or orchestration; most operations are still sequential
- Limited error recovery
PostgreSQL:
- Differential backup may require running as
postgresuser or equivalent permissions - Differential backup depends on filesystem access to the configured WAL archive directory
- Restore currently targets the standard full backup layout (
base.tar.gz+pg_wal.tar.gz) and does not support tablespace tar archives - PITR depends on a complete WAL archive reachable through
restore_commandand currently uses PostgreSQL 12+recovery.signalstyle recovery
MySQL:
- Full backups and differential backups are implemented
- Differential backup is based on
xtrabackup --incrementalagainst the last full backup - Minimal restore expects an XtraBackup directory containing
xtrabackup_info,xtrabackup_checkpoints,ibdata1, andmysql/ - Restore recursively decompresses
.zstartifacts beforextrabackup --prepare - Restore from compressed
.qpartifacts or single.tar.zstarchives is not implemented
This project is currently paused. Current follow-up areas if development resumes:
- Hardening the MySQL differential / incremental-from-full workflow
- Hardening PITR restore validation and WAL coverage diagnostics
- Improving error handling and validation
- Hardening PostgreSQL restore validation for very large/high-write databases
- Testing across more database versions
- Improving documentation
- Deciding whether to expose the experimental PostgreSQL incremental/WAL pipeline code currently living under
services/backup/incremential/andservices/wal/
PostgreSQL differential backup fails with permission denied:
The differential flow needs read access to the configured PostgreSQL archive directory. Options:
- Run as the postgres user:
sudo -u postgres python cli/dbtool.py ... - Run with sudo:
sudo python cli/dbtool.py ... - Grant read permissions to the configured archive directory (security consideration required)
MySQL xtrabackup fails with permission denied:
xtrabackup: Can't change dir to '/usr/local/mysql/data/' (OS errno 13 - Permission denied)
xtrabackup needs read access to MySQL data directory. Options:
- Run with sudo:
sudo python cli/dbtool.py ... - Run as mysql user:
sudo -u mysql python cli/dbtool.py ... - Grant read permissions to data directory (security consideration required)
pg_basebackup: must be superuser or replication role:
ALTER USER your_user REPLICATION;wal_level error:
Edit postgresql.conf:
wal_level = replica
Then restart PostgreSQL.
xtrabackup command not found:
Install Percona XtraBackup as shown in the MySQL Setup section.
This project is experimental. Contributions are welcome, particularly:
- Testing on different PostgreSQL/MySQL versions
- Hardening MySQL differential / incremental-from-full backups
- Testing PITR restore on different PostgreSQL versions and timezone settings
- Automated restore functionality
- Backup verification tools
- Error handling improvements
- Documentation
Please note that APIs and project structure may change if development resumes.
MIT License - see LICENSE file
This software is provided as-is, without warranty. It is not production-ready and should only be used in development/testing environments. Always verify your backups and test restore procedures before relying on this tool.