Skip to content

postkit db deploy clones the entire target database, including all row data #54

Description

@Yasirunet

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

  1. Configure a remote with a non-trivial amount of data (a few GB is enough to make it obvious).
  2. Commit a migration: postkit db commit -m "add a column".
  3. Run postkit db deploy.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions