dbtote
A cross-platform CLI tool for backing up and restoring databases, with compression, encryption at rest, and pluggable storage backends.
Full design: docs/SPEC.md.
Status
Phase 5: MySQL, PostgreSQL, SQLite, and MongoDB full backup/restore, local and S3 storage, gzip compression, age encryption, OS-keyring credentials, YAML config file with targets, test-connection, list (backed by a local SQLite metadata index), retention policy enforcement, Slack success/failure notifications, and CI/CD (gated releases, Docker image, keyless artifact signing, PR coverage comments).
Install
Download a prebuilt binary from the latest release, or pull the Docker image:
docker pull ghcr.io/nweber23/dbtote:latest
Or install with Go (builds from source, so dbtote --version won't show a real version — use a prebuilt binary or the Docker image above if you need that):
go install github.com/nweber23/dbtote/cmd/dbtote@latest
Or build from a local clone:
git clone https://github.com/nweber23/dbtote.git
cd dbtote
go build -o bin/dbtote ./cmd/dbtote
Requires the mysqldump/mysql client binaries on PATH for MySQL targets, pg_dump/pg_restore for PostgreSQL targets, and mongodump/mongorestore (from MongoDB Database Tools) for MongoDB targets. SQLite needs no external binary — it's backed up via SQLite's own VACUUM INTO, run through the pure-Go modernc.org/sqlite driver.
Quick start
-
Store the database password once:
dbtote secret set --target prod-mysql
-
Run a backup:
dbtote backup --target prod-mysql --host db.internal --user backup_svc --database app_production --output /var/backups/dbtote
-
Encrypt backups at rest with age:
age-keygen -o identity.txt # prints the matching public key
dbtote backup --target prod-mysql --host db.internal --user backup_svc --database app_production \
--output /var/backups/dbtote --encrypt --recipient age1...
-
Restore:
dbtote restore --target prod-mysql --from /var/backups/dbtote/prod-mysql_full_....sql.gz.age \
--host db.internal --user backup_svc --database app_production --decrypt-key identity.txt --yes
Config file
Instead of passing --host/--user/--database/etc. on every command, define targets once in a YAML config file. CLI flags always override the config for that run.
-
Write a starter config:
dbtote config init
Prompts for a local storage path and writes $XDG_CONFIG_HOME/dbtote/config.yaml (or --config <path>).
-
Add a target under targets:, e.g.:
targets:
prod-mysql:
engine: mysql
host: db1.internal
port: 3306
user: backup_svc
database: app_production
storage: local-main
-
Validate the config — checks storage references, age recipient keys, cron expressions, and that every target's credential resolves (password_env or OS keyring), without connecting to any database:
dbtote config validate
-
Check connectivity and credentials for a specific target:
dbtote test-connection --target prod-mysql
-
List known backups:
dbtote list
dbtote list --target prod-mysql --since 24h
dbtote list --json
With a config file in place, dbtote backup --target prod-mysql and dbtote restore --target prod-mysql --from <file> --yes resolve engine, host, port, user, database, storage, and encryption recipients from the target's config entry — pass any of --host/--user/--database/etc. explicitly to override just that field for one run.
PostgreSQL
Postgres is supported the same way MySQL is — set engine: postgres on a target. Requires the pg_dump and pg_restore client binaries on PATH.
SQLite and MongoDB
SQLite targets use path: instead of host/port/user/database, and need no credential — there's no password_env or keyring entry to set up:
targets:
local-sqlite:
engine: sqlite
path: /var/backups/app.db
storage: local-main
MongoDB targets use uri_env: — the name of an environment variable holding the full connection URI (including auth), never the URI itself in the config file:
targets:
prod-mongo:
engine: mongodb
uri_env: MONGO_URI
database: app_production
storage: s3-main
dbtote backup --target local-sqlite
MONGO_URI=mongodb://user:pass@mongo.internal:27017 dbtote backup --target prod-mongo
S3 storage, retention, and notifications
Add an S3 (or S3-compatible) backend under storage: — credentials resolve via the standard AWS chain (env vars, shared config, instance/workload identity), never a dbtote-specific setting:
storage:
s3-main:
type: s3
bucket: prod-dbtote-backups
region: us-east-1
prefix: dbtote/
Set a retention policy under defaults: (applies to every target) and/or per-target — keep_last and keep_days are a union, a backup survives if it satisfies either:
defaults:
retention:
keep_last: 7
keep_days: 30
See what a policy would delete without deleting anything, then enforce it:
dbtote retention preview --target prod-mysql
dbtote retention apply --target prod-mysql
Get a Slack message on backup success and/or failure — webhook_url_env names an env var holding the webhook URL, never the URL itself in the config file:
notify:
slack:
webhook_url_env: SLACK_WEBHOOK_URL
on: [success, failure]
dbtote backup sends notifications by default when notify.slack is configured; pass --notify=false to skip one run without touching the config.
Releasing
Pushing a v* tag triggers .github/workflows/release.yml: it gates on the full CI suite, then runs goreleaser to cross-compile binaries for linux/darwin/windows (amd64 + arm64, minus windows/arm64), checksum them, and publish a GitHub Release with an auto-generated changelog. Every release artifact is signed keylessly with cosign (Sigstore's OIDC-based flow — no signing key to manage), and a multi-arch Docker image is published to ghcr.io/nweber23/dbtote:
docker run --rm ghcr.io/nweber23/dbtote:latest backup --target prod-mysql --host db.internal --user backup_svc --database app_production --output /backups
Homebrew isn't set up yet — it needs a separate nweber23/homebrew-dbtote tap repository and a HOMEBREW_TAP_GITHUB_TOKEN secret, neither of which exist yet (link here once they do).
Development
make build # compile ./cmd/dbtote
make test # unit tests
make test-integration # requires Docker; spins up real MySQL via testcontainers
make lint # golangci-lint