zb migrate CLI
Tested with: zb CLI 0.1 · pg_dump 17 · mysqldump 8.4 · valkey-cli 8.1 · mongodump 100.10 · sqlite3 3.46 · unit-tested command lines for docker exec and wrangler d1 export
zb migrate --from ./app.sqlite --engine libsql --create # new instance from a filezb migrate --from local --engine postgres --db myapp --to inst_abc # local server via pg_dumpzb migrate --from local --engine postgres --container my-pg --db myapp --to inst_abc # pg_dump inside a containerzb migrate --from d1:prod-db --create # Cloudflare D1 via wranglerzb migrate status mig_123 ; zb migrate cutover mig_123zb migrate --from inst_abc --to inst_def # Databasezy to DatabasezyOptions
Section titled “Options”| Option | Meaning |
|---|---|
--from <source> | A connection string, a file path, local, d1:<database> (Cloudflare D1), or a Databasezy instance id |
--to <instance> | Target instance id. Omit with --create |
--create | Create the target instance first (--size, --name, --project apply) |
--engine <id> | Required for .sql files and local; inferred from connection strings and d1: (always libsql) |
--db <name> | Database name for --from local (also PGDATABASE / MYSQL_DATABASE). With --container it defaults to the container’s POSTGRES_DB / MYSQL_DATABASE |
--container <name|id> | With --from local: run the dump tool inside this running Docker container with docker exec |
--mode copy|copy_and_sync | Default copy. copy_and_sync keeps the target current until cutover (PostgreSQL, MySQL connection strings) |
--reverse-sync | With copy_and_sync on PostgreSQL: after cutover, replicate the new instance back to the source for rollback |
--include, --exclude | Table or collection patterns (public.*, public.users, events_*); comma separated |
--drop-target | Drop existing objects in the target before restoring |
--dry-run | Print the offline preflight checklist (with machine codes) and the exact plan, then exit |
--no-wait | Return once the migration is accepted instead of following progress |
--json | Machine-readable output (global) |
--org <id> | Organization (global; default from zb login) |
The provider preset is chosen from the hostname; --dry-run shows every rewrite it makes (pooler to direct host,
TLS mode, scheme changes). Set ZB_EGRESS_CIDRS to see the allow-list item in a dry run.
Subcommands
Section titled “Subcommands”zb migrate status <mig_id> # status, progress, preflight warnings, verification, sync lagzb migrate cutover <mig_id> [--no-wait] [--timeout 600]zb migrate cancel <mig_id> # stop the job; sync objects (publication, slot, channel) are removedstatus adds a sync line for copy_and_sync migrations:
status ready_for_cutoverprogress ready_for_cutover lag 0.4 s, ready for cutover (`zb migrate cutover mig_123`)sync lag 0.4 s, ready for cutover (`zb migrate cutover mig_123`)A migration is ready for cutover after two consecutive lag samples under 5 seconds. cutover requests the cutover
and waits until the migration is completed, failed or cancelled (or --timeout seconds pass), then prints the
measured downtime. If the target cannot catch up or verification finds a mismatch, the cutover is abandoned and sync
keeps running; the status message says why. See what happens at cutover.
Local-tool mode
Section titled “Local-tool mode”--from local and file sources run the engine’s own tools on your machine (pg_dump, mysqldump,
valkey-cli/redis-cli, mongodump, sqlite3) and stream the output to a pre-signed upload URL. No inbound
connection and no port forwarding is needed. The CLI checks for the tool and prints the install command if it is
missing:
error: `pg_dump` is not installed (needed to dump a local postgres server). install: brew install libpq && brew link --force libpqThe install line is the one for your OS. If docker ps shows a running container whose image matches the engine,
the message names it instead of picking one for you:
error: `pg_dump` is not installed (needed to dump a local postgres server). install: sudo apt install postgresql-client or dump inside a running Docker container with the tool in its image; found one: my-pg (postgres:17) rerun with `--container my-pg`Docker containers
Section titled “Docker containers”--container <name|id> runs the engine’s tool inside a running container, so nothing but docker is needed on your
machine. The dump goes to stdout, into a temporary file, and is uploaded like any other dump.
zb migrate --from local --engine postgres --container my-pg --db app --to inst_abczb migrate --from local --engine mysql --container my-mysql --db shop --createzb migrate --from local --engine valkey --container my-valkey --createzb migrate --from local --engine mongodb --container my-mongo --db shop --create| Engine | Runs inside the container | Upload format |
|---|---|---|
postgres, timescaledb | pg_dump --format=custom --no-owner --no-acl | pg_custom |
mysql, mariadb | mysqldump or mariadb-dump with --single-transaction --routines --triggers --hex-blob (plus --set-gtid-purged=OFF on MySQL) | sql |
valkey, redis | valkey-cli or redis-cli --rdb to a temporary file in the container, then cat | rdb |
mongodb, ferretdb | mongodump --archive (the image must ship mongodump) | bson_tar |
The command is docker exec [-e ZB_DB_USER] [-e ZB_DB_PASSWORD] <container> sh -c '<script>' zb-dump [<db>]. Passwords
never appear on a command line: the script reads the image’s own variables (POSTGRES_USER/POSTGRES_PASSWORD,
MYSQL_ROOT_PASSWORD or MYSQL_USER/MYSQL_PASSWORD, the MARIADB_* equivalents, REDIS_PASSWORD,
MONGO_INITDB_ROOT_USERNAME/MONGO_INITDB_ROOT_PASSWORD) and hands them to the tool through its environment
(PGPASSWORD, MYSQL_PWD, REDISCLI_AUTH) or, for mongodump, a temporary 0600 config file. To use other
credentials, export ZB_DB_USER and ZB_DB_PASSWORD; zb forwards them by name (-e ZB_DB_PASSWORD), so docker
reads the value from your environment. Without --db, Postgres dumps POSTGRES_DB (default: the user name), MySQL
dumps MYSQL_DATABASE, and MongoDB dumps every database.
The CLI stops with exit code 6 and says what to do when docker is not installed, the container does not exist or
is not running (docker start <name>), or the image has no dump tool (or no sh). SQLite and ClickHouse containers
are not supported with --container: copy the file out with docker cp and upload it.
Cloudflare D1
Section titled “Cloudflare D1”--from d1:<database> runs wrangler d1 export <database> --remote --output <tmp>/d1-<database>.sql and uploads the
SQL into a libSQL instance. The engine is always libsql. Wrangler must be logged in (wrangler login).
zb migrate --from d1:prod-db --create --dry-runzb migrate --from d1:prod-db --createzb migrate --from d1:prod-db --to inst_abcWithout wrangler the CLI stops with exit code 6 and prints what to run instead:
error: `wrangler` is not installed (needed to export Cloudflare D1 database `prod-db`). install: npm install -g wrangler (then `wrangler login`) or export it yourself and upload the file: npx wrangler d1 export prod-db --remote --output=prod-db.sql zb migrate --from ./prod-db.sql --engine libsql --to <instance>An export you already have goes through the file path: zb migrate --from ./prod-db.sql --engine libsql --create.
Exit codes
Section titled “Exit codes”0 on success, 1 on an error (preflight blockers, a failed migration), 2 when the request is rejected as invalid,
3 when verification finds a mismatch, 4 when you answer no at a prompt, 5 when the API is unreachable and 6 when
a local prerequisite is missing (the dump tool, docker, wrangler, a running container, or the dump tool inside the
container’s image). The message says which.