Reference
Every command, flag and environment variable of the three binaries, checked
against their --help. The REST API has its own page: REST API.
pgb: the CLI. Local mode drives Docker directly; server mode talks to abranchd.branchd: the daemon (REST API, Postgres router, reconcile loop).pgoverlay-github: the GitHub webhook service; its environment is in GitHub App.
All three print their version with pgb version (or pgb --version),
branchd -version and pgoverlay-github -version; branchd and
pgoverlay-github also log it at startup. Release builds are stamped by the
release workflow; make build stamps from git describe, and
make build VERSION=v1.2.3 overrides it.
pgb
Local mode and server mode
| Local mode | Server mode | |
|---|---|---|
| Selected by | no --server |
--server URL or PGOVERLAY_SERVER |
| Runs the engine | in the pgb process, against Docker |
in branchd |
| State | the registry in PGOVERLAY_HOME (default ~/.pgoverlay) |
branchd's |
| Auth | whoever can use the Docker socket | PGOVERLAY_TOKEN as a bearer token |
| TTL reaping | none on its own: run pgb gc |
branchd's reconcile loop |
Never run local-mode commands against a PGOVERLAY_HOME that a running
branchd owns: the SQLite registry has one writer.
Commands
| Command | What it does |
|---|---|
pgb source add NAME |
Register a source and seed it. Flags below. |
pgb source refresh NAME |
Re-seed into a new generation. Existing branches keep their snapshot; new branches see the new one. --password-env, --no-password. |
pgb source rm NAME |
Remove a source; refused while it has live branches. |
pgb source ls |
List sources. |
pgb source set-mask NAME FILE... |
Replace the source's masking SQL with the files, applied in argument order inside every new or reset branch. |
pgb source get-mask NAME |
Print the masking scripts in order. |
pgb source clear-mask NAME |
Remove the masking SQL. |
pgb branch create NAME --from SOURCE |
Create a branch from a source. --ttl 24h sets an expiry. |
pgb branch create NAME --from-branch PARENT |
Create a branch from another branch. |
pgb branch ls |
List branches. --usage adds a SIZE column (one helper container per branch). Local mode notes branches past their TTL. |
pgb branch reset NAME |
Discard the branch's writes and re-clone it from its base (new container and port). Also retries a failed branch. |
pgb branch recover NAME |
Restart a failed branch on its existing data, keeping its writes. |
pgb branch destroy NAME |
Destroy one branch (container and layer). Retries a branch stuck in destroying. |
pgb connect NAME |
Print connection URLs for a ready branch (see below). |
pgb diff NAME |
Schema diff and row-count changes against the branch's base. --all lists unchanged tables; --data samples up to --sample (default 20, at most 500) new rows per grown table. |
pgb history NAME |
The branch's audit trail: time, transition, actor, reason. |
pgb doctor |
Print the reconcile plan without changing anything. |
pgb gc |
Apply the reconcile plan. |
pgb token create NAME --role ROLE |
Mint an API token (viewer, the default, operator or admin); printed once. Admin only. |
pgb token ls / pgb token revoke NAME |
List (names and roles only) or revoke tokens. |
pgb version |
Print the version. |
pgb doctor and pgb gc take --stuck-timeout (default 10m) in local
mode: the age past which a creating or resetting row counts as abandoned.
pgb source add flags
| Flag | Default | Meaning |
|---|---|---|
--host |
required | source host as reachable from containers; an empty or blank value is rejected |
--port |
5432 |
|
--user |
postgres |
seed user: REPLICATION privilege for basebackup, any user that can read the data for dump |
--database |
postgres |
database recorded for connection strings, and dumped with --via dump |
--pg-version |
17 |
the source's major, 14 to 18. For basebackup it must equal the source's major (a mismatch fails the seed with the right version in the message); for dump it must be at least the source's major |
--via |
basebackup |
basebackup (physical) or dump (logical, for managed Postgres) |
--dump-schema |
whole database | schema to dump, repeatable (--via dump only) |
--image |
postgres:<pg-version> |
image for the seed helpers and every branch; must carry the source's extensions, locales and libc, e.g. postgis/postgis:17-3.5 or pgvector/pgvector:pg17 |
--network |
Docker network the source is reachable on | |
--password-env |
PGPASSWORD |
environment variable holding the source password |
--no-password |
connect without a password (trust, peer or certificate auth); exclusive with --password-env |
pgb connect
Local mode prints the branch's own URL, without a password in the default
inherit mode (set PGPASSWORD to the source's password). Server mode prints
two URLs: the branch's own Postgres, which only works from the branchd host
(Docker publishes branch ports on its 127.0.0.1) or inside the cluster
(Kubernetes pod IPs), and the router URL (database db@branch), which is the
one to use from anywhere else. The router address comes from --proxy-host /
--proxy-port, else from what branchd advertises (--advertise-proxy-addr),
else the --server host and port 6432. With --rotate-branch-credentials
both URLs include the branch's password. connect refuses a branch that is
not ready, and one whose rotated password cannot be decrypted.
Exit codes
pgb doctor exits 0 when there is no drift, 1 when it found drift, and
2 when it could not compute the plan (branchd unreachable, authentication
failed, invalid flags), so it can gate CI. Every other command exits 0 on
success and 1 on any error.
Environment
| Variable | Used by | Meaning |
|---|---|---|
PGOVERLAY_SERVER |
server mode | branchd base URL (http:// or https://, no query or fragment); same as --server |
PGOVERLAY_TOKEN |
server mode | API bearer token; pgb warns when it is unset |
PGOVERLAY_CA_CERT |
server mode | PEM file of a CA to trust for an https:// server (private or self-signed) |
PGOVERLAY_TLS_SKIP_VERIFY |
server mode | 1 disables certificate verification. Insecure, last resort; ignored with a warning when PGOVERLAY_CA_CERT is set |
PGOVERLAY_HOME |
local mode | state directory (default ~/.pgoverlay) |
PGOVERLAY_SECRET_KEY, PGOVERLAY_SECRET_KEY_FILE, PGOVERLAY_SECRET_KEY_PREVIOUS |
local mode | the at-rest key, read the same way branchd reads it, to decrypt rotated passwords; pgb never generates a key |
PGOVERLAY_SEED_SSLMODE |
local mode | sslmode of seed connections (default prefer) |
PGOVERLAY_LAZYRW, PGOVERLAY_WAL_RECYCLE |
local mode | on or off, as branchd's --lazyrw and --wal-recycle: how the branches pgb starts copy files up (default on for both) |
PGOVERLAY_SEED_SETTLE |
local mode | how source add and source refresh settle a new seed: freeze (default), recover or off; see --seed-settle |
PGOVERLAY_VOLUME_ROOT |
local mode | directory on the Docker host to create volumes under, as branchd's --volume-root; use the same value as a branchd sharing the state directory |
PGPASSWORD |
source add, source refresh |
the source password, unless --password-env names another variable |
DOCKER_HOST, DOCKER_CONTEXT, DOCKER_CONFIG |
local mode | the Docker endpoint, resolved like the docker CLI does, including a context's TLS material. ssh:// endpoints are not supported: run branchd on the Docker host and use --server, or forward the socket (ssh -NL /tmp/pgoverlay-docker.sock:/var/run/docker.sock HOST and DOCKER_HOST=unix:///tmp/pgoverlay-docker.sock). Branch ports are published on the Docker host's 127.0.0.1, so direct connection strings only work on that host |
The server-mode client retries 503 responses for every method, and 502,
504 and connection resets for idempotent ones, with jittered backoff over
about eight seconds, so a leader failover or a rolling restart is usually
invisible.
branchd
PGOVERLAY_TOKEN is required and must be at least 16 characters
(openssl rand -hex 16). On SIGINT/SIGTERM branchd stops accepting
connections and new mutations, lets in-flight requests finish for
--shutdown-timeout, cancels and rolls back what is left, and only then
releases its leader Lease. Branch containers keep running: they are durable
state. A second signal exits immediately.
Flags
| Flag | Default | Environment | Meaning |
|---|---|---|---|
--api-addr |
:7070 |
REST API listen address (all interfaces by default) | |
--api-tls-cert, --api-tls-key |
PEM certificate and key; TLS for the API when both are set | ||
--pg-addr |
:6432 |
Postgres router listen address (all interfaces by default) | |
--pg-tls-cert, --pg-tls-key |
PEM certificate and key; the router answers SSLRequest with S when set, N otherwise |
||
--advertise-proxy-addr |
--pg-addr's port |
host:port clients use to reach the router, returned as proxy_host/proxy_port in branch responses and used by pgb connect |
|
--reconcile-interval |
1m |
reconcile tick: TTL reaping, drift repair, garbage collection. --reap-interval is a deprecated alias |
|
--stuck-timeout |
10m |
see below | |
--shutdown-timeout |
1m |
PGOVERLAY_SHUTDOWN_TIMEOUT |
how long in-flight requests get on shutdown before they are cancelled and rolled back; keep it below the pod's terminationGracePeriodSeconds minus about 20 s |
--rotate-branch-credentials |
off | give every branch its own generated password (returned as password) instead of inheriting the source's |
|
--secret-key-file |
PGOVERLAY_SECRET_KEY_FILE |
file holding the at-rest key (32 bytes, hex or base64); see the at-rest key | |
--max-branches |
0 (unlimited) |
PGOVERLAY_MAX_BRANCHES |
cap on live branches; creates past it return 403 |
--default-ttl |
0 (none) |
PGOVERLAY_DEFAULT_TTL |
TTL for branches created without one |
--max-ttl |
0 (none) |
PGOVERLAY_MAX_TTL |
upper bound on any requested TTL; longer ones are capped |
--max-layer-depth |
100 |
PGOVERLAY_MAX_LAYER_DEPTH |
overlay backend: cap on a branch's frozen layer chain; branching from a branch at the cap returns 403 (see Troubleshooting) |
--lazyrw |
on |
PGOVERLAY_LAZYRW |
overlay backend: on preloads the lazyrw shim into branch Postgres, so a table file is copied into the branch on its first write, not when it is read; a branch whose kernel (below 4.19) or image cannot use it copies on open and says so (Troubleshooting); off always copies on open. Branches pick a change up when they next start; see below |
--wal-recycle |
on |
PGOVERLAY_WAL_RECYCLE |
overlay backend, experimental: off starts branch Postgres with wal_recycle=off, so a checkpoint removes a WAL segment that came from the seed instead of copying it up to rename it. A settled seed keeps a single segment, which the branch's first WAL write has already copied up, so measured on ext4 off saves no copy-up; it only keeps fewer recycled segments (16-32 MiB less after 45 MiB of WAL) at the cost of zero-filling every new one |
--seed-settle |
freeze |
PGOVERLAY_SEED_SETTLE |
how a new seed (source add or refresh) is prepared before branches start from it: freeze, recover or off; see below |
--disk-root |
see Observability | path whose filesystem the pgoverlay_disk_bytes_* gauges measure |
|
--volume-root |
PGOVERLAY_VOLUME_ROOT |
docker runtime, overlay backend: create every pgoverlay volume as a local bind volume over <dir>/<volume> on the Docker host instead of in Docker's own volume store; see the volume root |
|
--xfs-cowextsize |
16k |
overlay backend: when copy-up clones on XFS, the copy-on-write extent size hint set on the volume root (--volume-root, or --kube-data-root); 0 leaves the filesystem default (128 KiB) |
|
--runtime |
docker |
docker or kube |
|
--cow |
overlay |
copy-on-write backend: overlay, zfs (experimental) or csi (forced by --kube-storage csi) |
|
--zfs-dataset |
dataset prefix pgoverlay owns, e.g. tank/pgoverlay (required with --cow zfs) |
||
--kube-storage |
hostpath |
hostpath (one storage node) or csi (PVC clones) |
|
--kube-node |
storage node (required with --runtime kube --kube-storage hostpath) |
||
--kube-data-root |
/var/lib/pgoverlay |
data root on the storage node (hostpath only) | |
--kube-namespace |
POD_NAMESPACE, else pgoverlay |
namespace for branch and helper pods | |
--kube-helper-image |
alpine pinned by digest | image for file-level helper pods, e.g. a mirror | |
--kubeconfig |
in-cluster, then KUBECONFIG / ~/.kube/config |
||
--csi-storage-class |
StorageClass for pgoverlay PVCs; required with --kube-storage csi; must support PVC cloning, or snapshots with --csi-snapshot-class |
||
--csi-snapshot-class |
clone through VolumeSnapshot + restore instead of direct PVC clones | ||
--csi-volume-size |
10Gi |
size of every pgoverlay PVC | |
--leader-elect |
off | HA: contend for the pgoverlay-branchd Lease; kube runtime only (High availability) |
|
-version |
print the version and exit |
Stuck timeout
--stuck-timeout (default 10m) is the age past which reconcile treats work
as abandoned:
- a
creatingorresettingbranch, or aseedingsource, that has made no progress for this long is failed. Running operations heartbeat their rows everymin(stuck-timeout / 4, 30s), so a slow but live operation (a long masking script, a large first recovery, a seed whose settle VACUUMs a large database) is never failed by reconcile; the timeout bounds time without progress, not total time; - a branch stuck in
destroyingfor this long is retried; - volumes younger than this are never garbage-collected, and finished helper containers older than this are removed.
Separately, a branch operation started through the REST API is cancelled and
rolled back (504) if it runs longer than --stuck-timeout in total. The
504 names the elapsed time and the limit. When a create or reset is
legitimately that slow (a long masking script is the usual case), raise
--stuck-timeout (Helm value stuckTimeout) above its run time; there is no
environment variable for it. Operations pgb runs in local mode have no such
bound, and neither do source add and source refresh through the API,
settle included: they only heartbeat.
Copy on first write (lazyrw)
On the overlay backend (Docker and Kubernetes hostpath) every branch's Postgres runs with the lazyrw shim preloaded: it opens table and transaction-status files read-only and reopens a file read-write on its first write, so a read copies nothing into the branch and a write copies the file it touches once (how it works).
--lazyrw=on(default;PGOVERLAY_LAZYRW, Helmcow.lazyrw: true): each branch checks at start that its kernel re-targets read-only files after a copy-up (Linux 4.19 and later) and that the build for its image's libc and architecture loads into itspostgres, and uses the shim only then; on PG 18 and later it also pinsio_method=worker. Otherwise the branch copies on open, as withoff, and logs a warning.--lazyrw=off: branches copy every relation file Postgres opens, reads included.- A change reaches a branch when it next starts: a reset, a recover, or a
restart by reconcile. The setting is passed in the branch's environment
(
PGOVERLAY_LAZYRW), so no reinstall is needed.pgbin local mode readsPGOVERLAY_LAZYRWtoo. - What each branch runs in is in
/pgoverlay/rw/cow-modeinside it (lazyrw,eageroroff, then a detail line), in branchd's log when the branch becomes ready, and in thepgoverlay_branch_cow_mode{mode}gauge (Observability). Branches created by a release before v1.0.0 count aseageruntil they are reset (Upgrading). - The zfs and csi backends ignore it: their clones copy blocks.
--wal-recycle=off (experimental, PGOVERLAY_WAL_RECYCLE) starts overlay
branches with wal_recycle=off: a checkpoint then removes a WAL segment that
came from the seed instead of renaming it, which on OverlayFS copies it up
first.
Seed settle
A pg_basebackup copy is an online backup: every branch started from it
would replay the WAL streamed during the backup, and its pages carry the
source's unset hint bits, dead tuples and unfrozen transaction ids, so reads
in a branch write (hint bits, pruning, anti-wraparound autovacuum) and, on the
overlay backend, copy the touched files into the branch. --seed-settle does
that work once, in the seed:
freeze(default): start the seed once in a helper on the branch image, which completes the backup's recovery; runVACUUM (FREEZE, ANALYZE)on every database, thenVACUUM (FREEZE)ofpg_statisticandpg_statistic_ext_datain each of them again (the ANALYZE writes their rows after they were vacuumed, and the first query planned in a branch would otherwise set hint bits on them and copy the catalog up);CHECKPOINT; switch to a fresh WAL segment; stop it cleanly.recover: the same without the VACUUMs. Branches start without WAL replay, but reads may still set hint bits.off: leave the seed aspg_basebackupwrote it.
After the clean stop (freeze and recover) the WAL segment holding the
shutdown checkpoint keeps its first pages and the zero-filled rest becomes a
hole, byte for byte the same file, and the unused segments after it are
removed. A branch appends its WAL to that segment, so on the overlay backend
its first WAL write copies a few KiB instead of the whole 16 MiB segment
(measured on ext4: about 1 MiB allocated instead of 16 MiB, and a first
write of 12-27 ms instead of 46-119 ms). Branch usage counts apparent bytes
(du -sb), so it still shows the segment at 16 MiB.
--via dump seeds end with a clean shutdown anyway; with freeze their
helper runs the VACUUMs first, and unless off it trims the WAL the same way.
The source is never touched. The settle server listens on a private socket
only and ignores the parts of the source's configuration that cannot start in
a throwaway container; see Troubleshooting for
what can still fail it.
Seeding writes the cluster as the image's own postgres user, looked up in
the image first (id -u postgres): 999:999 in the Debian images, 70:70 in the
Alpine ones. An image without a postgres user cannot be seeded.
Copy-up mode
On the overlay backend branchd probes, once at startup and in the
background, what an OverlayFS copy-up costs on the filesystem that holds the
volumes: it copies a 64 MiB file up through an overlay across two temporary
volumes, in a helper with the privileges a branch container already has
(CAP_SYS_ADMIN). The result is logged (copy-up probe: mode=clone fs=xfs
...) and exported as pgoverlay_cow_copyup_mode:
clone: the filesystem reflinks (XFS withreflink=1, btrfs). Copy-up clones extents in milliseconds, and the copy shares every block with the source until one is rewritten, which is block-level copy-on-write. Branch usage is counted as the bytes the branch owns alone (a small static tool,pgoverlay-du, reads the extent map), becauseduwould count a cloned 1 GiB segment with one changed page as 1 GiB.copy: copy-up copies data (ext4, XFS without reflink, most others); usage isdu -sb.unknown: not probed yet, or the probe failed; usage isdu -sb.
The mode changes what a branch's first write to a file costs, not whether
reads copy (they do not, with --lazyrw=on): a copy of up to 1 GiB in
copy mode, an extent clone and then the rewritten blocks in clone mode.
pgb in local mode does not probe, so pgb branch ls --usage there is
always du -sb, which over-counts on a filesystem that clones.
On XFS, branchd also sets a copy-on-write extent size hint
(--xfs-cowextsize, 16 KiB) on the volume root, so the first write to a
cloned block copies 16 KiB rather than 128 KiB. Volumes created afterwards
inherit it. Docker's own volume directories are never touched, so the hint
needs --volume-root on the docker runtime.
The volume root
By default the docker runtime keeps pgoverlay's volumes in Docker's volume
store (/var/lib/docker/volumes), on whatever filesystem that is: usually
ext4, where copy-up copies. --volume-root DIR (or PGOVERLAY_VOLUME_ROOT)
creates every volume instead as a local-driver bind volume over DIR/<volume>
(type=none,o=bind,device=DIR/<volume>): source generations, writable
layers, frozen layers and diff throwaways alike. Point it at a directory on an
XFS (reflink=1) or btrfs disk and branches get block-level copy-on-write
without moving Docker. Volumes are still named volumes, mounted, listed and
labelled by name; docker volume ls shows them.
DIRis a path on the Docker host, which is not this machine whenDOCKER_HOSTpoints elsewhere (it is inside the VM for Docker Desktop, Colima and OrbStack). It must exist; branchd checks it at startup and never creates it, so a disk that failed to mount cannot turn into a directory on the root filesystem.- pgoverlay creates and deletes the directories itself, in helper containers
that mount
DIR, and records each volume's labels inDIR/<volume>/.pgoverlay-labels.json. Removing a volume deletes its directory. A directory whose volume is gone (a removal cut short, or adocker volume rm) is found and deleted by reconcile. - Set the same value for
pgbin local mode (PGOVERLAY_VOLUME_ROOT); both share the registry and the volumes. - Changing or dropping it later is safe: existing volumes stay where they were created, keep working, and are removed from there.
- On SELinux-enforcing hosts (RHEL, Fedora) containers may only use
directories labelled for them:
chcon -Rt container_file_t DIR, or a matchingsemanage fcontextrule. - Keep
DIRon one filesystem: an extent clone cannot cross filesystems, so copy-up between two of them copies. Across subvolumes of one btrfs filesystem copy-up still clones (measured on Linux 7.0), but keep the volume directories themselves plain directories, as pgoverlay creates them: in the #49 evaluation, a branch whose overlay lower and upper sat in different btrfs subvolumes did not start (the image entrypoint'sfindreportedFile system loop detected). - Every volume create and remove runs one short helper container (about a second on a busy host), in addition to what the operation already does.
The at-rest key
With --rotate-branch-credentials, branch passwords are stored encrypted
(AES-256-GCM) under a dedicated key, independent of PGOVERLAY_TOKEN. branchd
takes it from PGOVERLAY_SECRET_KEY (hex or base64), else
--secret-key-file / PGOVERLAY_SECRET_KEY_FILE, else
<state dir>/secret.key, which it generates (mode 0600) on first start and
logs the key id it uses. To rotate the key, start once with the new key and
the old one in PGOVERLAY_SECRET_KEY_PREVIOUS (comma-separated for several);
a generated secret.key left in the state directory is picked up as a
previous key automatically. Back the key up with the registry. Details in
Security.
Environment
| Variable | Meaning |
|---|---|
PGOVERLAY_TOKEN |
required admin bearer token, at least 16 characters |
PGOVERLAY_HOME |
state directory: registry and secret.key (default ~/.pgoverlay; created 0700) |
PGOVERLAY_SECRET_KEY |
the at-rest key itself; wins over the key file |
PGOVERLAY_SECRET_KEY_PREVIOUS |
retired keys, comma-separated, decrypt-only |
PGOVERLAY_SEED_SSLMODE |
sslmode of seed connections: disable, allow, prefer (default), require, verify-ca or verify-full |
PGOVERLAY_MAX_BRANCHES, PGOVERLAY_DEFAULT_TTL, PGOVERLAY_MAX_TTL, PGOVERLAY_MAX_LAYER_DEPTH, PGOVERLAY_SHUTDOWN_TIMEOUT, PGOVERLAY_SECRET_KEY_FILE, PGOVERLAY_LAZYRW, PGOVERLAY_WAL_RECYCLE, PGOVERLAY_SEED_SETTLE, PGOVERLAY_VOLUME_ROOT |
defaults for the flags above |
DOCKER_HOST, DOCKER_CONTEXT, DOCKER_CONFIG |
Docker endpoint (docker runtime), as for pgb |
POD_NAMESPACE, POD_NAME, PGOVERLAY_POD_NAME, PGOVERLAY_POD_UID |
set by the Helm chart: the namespace, the leader-election identity (and leader label), and the pod that owns helper pods |
Helm chart values
The chart's values.yaml
documents every value; Kubernetes explains the ones that
matter. The chart maps these to branchd flags: reconcileInterval,
stuckTimeout, shutdownTimeout (seconds; the pod's grace period is this
plus 30), rotateBranchCredentials, cow.lazyrw (--lazyrw, true or
false), seedSettle, helperImage, dataRoot, node,
storage.*, proxy.tls.certSecret, replicaCount and
leaderElection.enabled. It does not yet expose --max-branches,
--default-ttl, --max-ttl, --max-layer-depth, --advertise-proxy-addr,
--api-tls-*, --xfs-cowextsize, --wal-recycle or a way to inject
PGOVERLAY_SECRET_KEY; the at-rest key is generated on the state volume.
dataRoot is --kube-data-root: put it on XFS (reflink=1) or btrfs for
clone copy-up (--volume-root is docker-only).