Summary
Running postkit db deploy pulls a full copy of the target (usually production) database — schema and every row — down to a throwaway local container before it will deploy anything. On any database with real data this dominates the runtime of the command, and users have reported deploys taking far longer than expected and transferring their production dataset.
postkit db start does not have this problem: it clones structure only.
Steps to reproduce
- Configure a remote with a non-trivial amount of data (a few GB is enough to make it obvious).
- Commit a migration:
postkit db commit -m "add a column".
- Run
postkit db deploy.
- Observe the "Cloning target database to local..." step.
Expected behavior
db deploy runs a dry-run of committed DDL migrations against a local clone. Verifying that DDL applies cleanly needs the target's structure. Deploying a one-line ALTER TABLE should not require transferring the production dataset.
Actual behavior
The dry-run clone is a full pg_dump of the target with no --schema-only, piped into psql against a local temp database. Every row in the remote crosses the network, gets restored locally, and is then dropped again when cleanupLocal() runs at the end of the command.
cloneDatabase and cloneDatabaseViaContainer both take a schemaOnly parameter that defaults to false:
cli/src/modules/db/services/database.ts:119
cli/src/modules/db/services/container.ts:126
db start passes true (cli/src/modules/db/commands/start.ts:227, :229). db deploy passes nothing (cli/src/modules/db/commands/deploy.ts:230, :232), so it gets the full-data default.
There is no way to opt out. The deploy command only registers --remote, --url and -f (cli/src/modules/db/index.ts:127-137).
Knock-on effects
Seeds run against a database that already has production data. runSteps() applies seeds to the clone (cli/src/modules/db/commands/deploy.ts:92). Because the clone now carries real rows, seed inserts collide with existing data — duplicate keys and similar failures — and a dry-run failure here aborts the deploy with a confusing error that has nothing to do with the migration being deployed.
A full copy of production lands on the developer's disk (or in the temp Docker container's volume) for the duration of the command.
Cost and exposure. On managed Postgres this is billed egress on every deploy, and it pulls production data onto developer machines that may not be meant to hold it.
Contributing factor: the dump is funneled line-by-line through Node
Independently of the data volume, the whole pg_dump → psql stream is routed through a JS line transform so sanitizeCloneLine can rewrite it (cli/src/common/shell.ts:115). Every chunk is split into lines and each line is pushed individually.
Measured on this machine:
|
200 MB through the pipe |
| raw passthrough |
29 ms |
through createLineTransform |
1976 ms (~101 MB/s) |
Node becomes the throughput ceiling for the clone, ahead of both the network and Postgres.
This is made significantly worse by neutralizeUnsupportedPreambleSettings (cli/src/modules/db/services/database.ts:108), which constructs a new RegExp on every line of the dump:
per-line new RegExp: 1804 ms per 3,000,000 lines
hoisted RegExp: 73 ms (25x faster)
prefix-guarded: 17 ms (100x faster)
Both sanitizer regexes can only ever match statements in the dump preamble/DDL — neither can match a COPY data row — yet every data row pays for two regex evaluations plus a compile. With the full-data clone above, the two problems multiply.
Minor related observations
- First-run Docker image pull is invisible.
docker run -d postgres:<N>-alpine (cli/src/modules/db/services/container.ts:50) silently pulls a 100–400 MB image on first use while the spinner reads "Starting postgres:16-alpine container…". With no progress output this looks like a hang.
- Duplicate round trip in
db start. getRemotePgMajorVersion is called at cli/src/modules/db/commands/start.ts:118 only to feed a logger.debug line, then resolveLocalDb calls it again immediately afterwards.
Not a problem
Stream backpressure was checked and is handled correctly — Transform honours push() returning false, so there is no unbounded buffering in the pipe.
Environment
- Affects
postkit db deploy on all platforms, both the localDbUrl-configured path and the auto-container path.
- Present since the dry-run clone was introduced; not a recent regression.
Summary
Running
postkit db deploypulls a full copy of the target (usually production) database — schema and every row — down to a throwaway local container before it will deploy anything. On any database with real data this dominates the runtime of the command, and users have reported deploys taking far longer than expected and transferring their production dataset.postkit db startdoes not have this problem: it clones structure only.Steps to reproduce
postkit db commit -m "add a column".postkit db deploy.Expected behavior
db deployruns a dry-run of committed DDL migrations against a local clone. Verifying that DDL applies cleanly needs the target's structure. Deploying a one-lineALTER TABLEshould not require transferring the production dataset.Actual behavior
The dry-run clone is a full
pg_dumpof the target with no--schema-only, piped intopsqlagainst a local temp database. Every row in the remote crosses the network, gets restored locally, and is then dropped again whencleanupLocal()runs at the end of the command.cloneDatabaseandcloneDatabaseViaContainerboth take aschemaOnlyparameter that defaults tofalse:cli/src/modules/db/services/database.ts:119cli/src/modules/db/services/container.ts:126db startpassestrue(cli/src/modules/db/commands/start.ts:227,:229).db deploypasses nothing (cli/src/modules/db/commands/deploy.ts:230,:232), so it gets the full-data default.There is no way to opt out. The
deploycommand only registers--remote,--urland-f(cli/src/modules/db/index.ts:127-137).Knock-on effects
Seeds run against a database that already has production data.
runSteps()applies seeds to the clone (cli/src/modules/db/commands/deploy.ts:92). Because the clone now carries real rows, seed inserts collide with existing data — duplicate keys and similar failures — and a dry-run failure here aborts the deploy with a confusing error that has nothing to do with the migration being deployed.A full copy of production lands on the developer's disk (or in the temp Docker container's volume) for the duration of the command.
Cost and exposure. On managed Postgres this is billed egress on every deploy, and it pulls production data onto developer machines that may not be meant to hold it.
Contributing factor: the dump is funneled line-by-line through Node
Independently of the data volume, the whole
pg_dump → psqlstream is routed through a JS line transform sosanitizeCloneLinecan rewrite it (cli/src/common/shell.ts:115). Every chunk is split into lines and each line is pushed individually.Measured on this machine:
createLineTransformNode becomes the throughput ceiling for the clone, ahead of both the network and Postgres.
This is made significantly worse by
neutralizeUnsupportedPreambleSettings(cli/src/modules/db/services/database.ts:108), which constructs a newRegExpon every line of the dump:Both sanitizer regexes can only ever match statements in the dump preamble/DDL — neither can match a
COPYdata row — yet every data row pays for two regex evaluations plus a compile. With the full-data clone above, the two problems multiply.Minor related observations
docker run -d postgres:<N>-alpine(cli/src/modules/db/services/container.ts:50) silently pulls a 100–400 MB image on first use while the spinner reads "Starting postgres:16-alpine container…". With no progress output this looks like a hang.db start.getRemotePgMajorVersionis called atcli/src/modules/db/commands/start.ts:118only to feed alogger.debugline, thenresolveLocalDbcalls it again immediately afterwards.Not a problem
Stream backpressure was checked and is handled correctly —
Transformhonourspush()returningfalse, so there is no unbounded buffering in the pipe.Environment
postkit db deployon all platforms, both thelocalDbUrl-configured path and the auto-container path.