Skip to content

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

shell
zb migrate --from postgres://user:[email protected]/db --to inst_abc # provider auto-detected
zb migrate --from postgres://user:[email protected]/app --to inst_abc --mode copy_and_sync
zb migrate --from ./app.sqlite --engine libsql --create # new instance from a file
zb migrate --from local --engine postgres --db myapp --to inst_abc # local server via pg_dump
zb migrate --from local --engine postgres --container my-pg --db myapp --to inst_abc # pg_dump inside a container
zb migrate --from d1:prod-db --create # Cloudflare D1 via wrangler
zb migrate status mig_123 ; zb migrate cutover mig_123
zb migrate --from inst_abc --to inst_def # Databasezy to Databasezy
OptionMeaning
--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
--createCreate 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_syncDefault copy. copy_and_sync keeps the target current until cutover (PostgreSQL, MySQL connection strings)
--reverse-syncWith copy_and_sync on PostgreSQL: after cutover, replicate the new instance back to the source for rollback
--include, --excludeTable or collection patterns (public.*, public.users, events_*); comma separated
--drop-targetDrop existing objects in the target before restoring
--dry-runPrint the offline preflight checklist (with machine codes) and the exact plan, then exit
--no-waitReturn once the migration is accepted instead of following progress
--jsonMachine-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.

shell
zb migrate status <mig_id> # status, progress, preflight warnings, verification, sync lag
zb migrate cutover <mig_id> [--no-wait] [--timeout 600]
zb migrate cancel <mig_id> # stop the job; sync objects (publication, slot, channel) are removed

status adds a sync line for copy_and_sync migrations:

status ready_for_cutover
progress 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.

--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 libpq

The 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`

--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.

shell
zb migrate --from local --engine postgres --container my-pg --db app --to inst_abc
zb migrate --from local --engine mysql --container my-mysql --db shop --create
zb migrate --from local --engine valkey --container my-valkey --create
zb migrate --from local --engine mongodb --container my-mongo --db shop --create
EngineRuns inside the containerUpload format
postgres, timescaledbpg_dump --format=custom --no-owner --no-aclpg_custom
mysql, mariadbmysqldump or mariadb-dump with --single-transaction --routines --triggers --hex-blob (plus --set-gtid-purged=OFF on MySQL)sql
valkey, redisvalkey-cli or redis-cli --rdb to a temporary file in the container, then catrdb
mongodb, ferretdbmongodump --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.

--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).

shell
zb migrate --from d1:prod-db --create --dry-run
zb migrate --from d1:prod-db --create
zb migrate --from d1:prod-db --to inst_abc

Without 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.

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.