Instant copy-on-write PostgreSQL database branching -- a self-hosted alternative to Neon.
pgbranch lets you create full, writable copies of a PostgreSQL database in seconds rather than minutes. Each branch is a real Postgres instance backed by a copy-on-write (CoW) snapshot, so it consumes only the storage needed for the data that diverges from the parent. Branches can be nested, tagged, given a TTL for automatic cleanup, and managed through either a CLI or a REST API.
Why pgbranch exists:
- No vendor lock-in. Unlike Neon, pgbranch runs anywhere you can run a container -- your laptop, a bare-metal server, or a Kubernetes cluster.
- Self-hosted. Your data never leaves your infrastructure. No cloud account, no usage-based billing, no external dependencies.
- Standard Postgres. Each branch runs an unmodified PostgreSQL 16 instance. Any tool that works with Postgres works with pgbranch.
- Developer workflows. Create a branch per pull request, per migration test, or per debugging session. Destroy it when you are done.
pgbranch is a single stateful container that manages a ZFS pool (or LVM thin pool), a SQLite metadata store, and a fleet of pg_ctl-managed Postgres instances. There is no distributed state, no WAL shipping, and no custom storage engine.
┌─────────────────────────────────────────────┐
│ pgbranch container (privileged) │
│ │
CLI / curl ──────────┤► REST API (:8080) │
│ │ │
│ ├─► SQLite metadata store │
│ │ (trunks, branches, port allocation) │
│ │ │
│ ├─► Snapshot engine (ZFS or LVM) │
│ │ zfs snapshot / zfs clone │
│ │ lvcreate --snapshot │
│ │ │
│ ├─► pg_ctl manager │
│ │ start / stop per-instance Postgres │
│ │ │
│ └─► Replication manager │
│ logical replication from remote DB │
│ │
│ Trunks (5432, ...) ──► trunk Postgres instances
│ Branches (5500-5599) ──► branch Postgres instances
└─────────────────────────────────────────────┘
Trunks and branches:
pgbranch uses a tree metaphor. Trunks are independent source databases that branches are created from. A default trunk called "main" is created automatically when the server starts. You can create additional trunks for different projects, teams, or environments.
How branching works:
- A trunk is a real PostgreSQL instance running on its own ZFS/LVM dataset. Trunks auto-start with the daemon and serve as the source of truth for branches. Optionally, any trunk can replicate data from a remote production database via logical replication.
- The server runs a
CHECKPOINTon the source (a trunk or another branch) and takes a point-in-time snapshot of the dataset. - A writable clone is created from that snapshot. With ZFS this is
zfs clone; with LVM this is a thin snapshot. - A new Postgres instance is started on an allocated port using
pg_ctl. - The branch is immediately ready for connections with the same data as its parent.
Because the clone is copy-on-write, it initially shares all data blocks with its parent. Only modified pages consume additional storage.
Key components:
| Component | Description |
|---|---|
pgbranchd |
Server daemon -- REST API, snapshot engine, Postgres manager, TTL reaper |
pgbranch |
CLI client -- talks to the pgbranchd API |
| Trunks | Managed PostgreSQL instances, each on its own dataset. Default trunk is "main". |
| Snapshot engine | Pluggable backend (Engine interface) with ZFS and LVM implementations |
| Replication manager | Logical replication from a remote PostgreSQL database into any trunk |
| SQLite store | Embedded metadata database tracking trunks, branches, ports, tags, and TTLs |
| TTL reaper | Background goroutine that periodically destroys expired branches |
| Replication monitor | Background goroutine that polls replication lag and status every 30s |
| Reconciler | Startup routine that detects and corrects inconsistent branch/trunk states |
pgbranch supports two copy-on-write storage backends. On startup, the server auto-detects the best available backend in this order: ZFS > LVM.
ZFS is the preferred backend. It provides true block-level CoW snapshots and clones with excellent space efficiency, built-in LZ4 compression, and tunable record sizes optimized for PostgreSQL (16K). Snapshots and clones are instantaneous regardless of dataset size.
pgbranch creates a file-backed zpool inside the container using a sparse file, so no dedicated block device is required for development.
LVM thin provisioning is the fallback for environments where ZFS kernel modules are not available (for example, Docker Desktop's linuxkit kernel on macOS without Colima, or certain cloud VMs). It provides block-level CoW via the device-mapper thin target.
LVM thin snapshots are nearly as fast as ZFS snapshots but lack built-in compression and have slightly more operational complexity. The LVM backend creates a loopback-backed thin pool with ext4-formatted thin logical volumes.
Docker Desktop's linuxkit kernel does not include ZFS or LVM thin-pool modules. Use Colima to run a full Ubuntu VM with ZFS support.
# 1. Set up Colima with ZFS
./scripts/setup-colima.sh
# 2. Point Docker at the Colima instance
export DOCKER_HOST=unix://${HOME}/.colima/pgbranch/docker.sock
# 3. Build and start pgbranch
make docker-build
make docker-up
# 4. Create your first branch
pgbranch create my-feature --from main
# 5. Connect to it
pgbranch connect my-featureOn Linux, the host kernel typically has ZFS or device-mapper thin modules available.
# 1. Build and start pgbranch
make docker-build
make docker-up
# 2. Verify the server is healthy
curl -s http://localhost:8080/api/v1/health | jq .
# 3. Create a branch
pgbranch create my-feature --from main --wait
# 4. Connect
pgbranch connect my-feature --psqlpgbranch statusExpected output:
Controller: healthy (v0.1.0, uptime 2m)
Trunks: 1 running / 1 total
Branches: 0 running, 0 stopped, 0 total
Storage: 0 B used
The "main" trunk auto-starts with the daemon. You can check its status and connect to it directly:
pgbranch trunk info
pgbranch trunk connectInstall shell completions so tab-completion works in every new session:
pgbranch completion installThis auto-detects your shell (bash, zsh, or fish) and installs the completion script. To override the detection:
pgbranch completion install --shell zshTo remove completions:
pgbranch completion uninstallThe pgbranch CLI communicates with the pgbranchd server over HTTP. By default it connects to http://localhost:8080. Override this with the --api-url flag or the PGBRANCH_API_URL environment variable.
| Flag | Env Var | Default | Description |
|---|---|---|---|
--api-url |
PGBRANCH_API_URL |
http://localhost:8080 |
Controller API URL |
--api-token |
PGBRANCH_API_TOKEN |
(empty) | API bearer token |
-o, --output |
table |
Output format: table, json, yaml |
|
-v, --verbose |
false |
Enable verbose output | |
--no-color |
false |
Disable color output |
Create a new database branch. If no name is given, one is auto-generated.
# Branch from the main trunk with an auto-generated name
pgbranch create
# Branch from the main trunk with a specific name
pgbranch create my-feature
# Branch from a specific trunk
pgbranch create my-feature --from staging
# Branch from another branch
pgbranch create my-experiment --from my-feature
# Auto-expire after 2 hours
pgbranch create ci-test-429 --ttl 2h
# Add tags
pgbranch create pr-123 --tag ci --tag pr-123
# Wait for ready and immediately open psql
pgbranch create my-feature --wait --connect| Flag | Short | Default | Description |
|---|---|---|---|
--from |
-f |
main |
Parent branch or trunk to snapshot from |
--ttl |
(none) | Auto-destroy after this duration (e.g. 2h, 7d) |
|
--tag |
-t |
(none) | Tags to add (repeatable) |
--cpu |
(none) | CPU limit (e.g. 500m) |
|
--memory |
(none) | Memory limit (e.g. 512Mi) |
|
--wait |
-w |
true |
Wait for the branch to become ready |
--connect |
-c |
false |
Open a psql session after creation |
List all branches. Alias: pgbranch ls.
# List all branches
pgbranch list
# Filter by status
pgbranch list --status running
# Filter by parent
pgbranch list --parent my-feature
# Filter by tag
pgbranch list --tag ci
# Show as a tree (shows all trunks as roots)
pgbranch list --treeExample table output:
NAME STATUS PARENT AGE SIZE
my-feature running main 2h 12.3 MB
pr-123 running my-feature 45m 8.1 MB
ci-test-429 stopped staging 1d 4.2 MB
Example tree output:
main
├── my-feature (running, 12.3 MB)
│ └── pr-123 (running, 8.1 MB)
staging
└── ci-test-429 (stopped, 4.2 MB)
| Flag | Short | Default | Description |
|---|---|---|---|
--status |
-s |
(all) | Filter by status |
--parent |
-p |
(all) | Filter by parent branch name |
--tag |
-t |
(all) | Filter by tag |
--tree |
false |
Display as a tree view |
Show detailed information about a single branch.
pgbranch info my-featureName: my-feature
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Status: running
Parent: main
Created: 2025-06-15T10:30:00Z (2h ago)
PG Version: 16
Size: 12.3 MB
Tags: ci, pr-123
Connection: postgresql://postgres@localhost:5501/postgres
Connect to a branch database. By default, opens psql if it is available on your PATH, otherwise prints the connection URI.
# Auto-detect: open psql if available, otherwise print URI
pgbranch connect my-feature
# Force psql
pgbranch connect my-feature --psql
# Print only the connection URI (useful for scripting)
pgbranch connect my-feature --uri| Flag | Default | Description |
|---|---|---|
--psql |
false |
Open an interactive psql session |
--uri |
false |
Print connection URI only |
Destroy a branch and reclaim its storage. Running branches are automatically stopped. Aliases: pgbranch rm, pgbranch delete.
# Destroy a branch (prompts for confirmation, auto-stops if running)
pgbranch destroy my-feature
# Skip the confirmation prompt
pgbranch destroy my-feature --yes
# Destroy a branch and all of its descendants
pgbranch destroy my-feature --recursive --yes| Flag | Short | Default | Description |
|---|---|---|---|
--recursive |
-r |
false |
Also destroy all descendant branches |
--yes |
-y |
false |
Skip the confirmation prompt |
Start a stopped branch. Waits for Postgres to become ready.
pgbranch start my-featureStarting branch "my-feature"... ready
Connection: postgresql://postgres@localhost:5501/postgres
Stop a running branch. The data is preserved; the Postgres process is shut down and the port is released.
pgbranch stop my-featureStopping branch "my-feature"... done
Add or remove tags on a branch.
# Add tags
pgbranch tag my-feature ci nightly
# Remove tags
pgbranch tag my-feature ci --remove| Flag | Short | Default | Description |
|---|---|---|---|
--remove |
-r |
false |
Remove the specified tags instead of adding them |
Show overall system status including controller health, trunk counts, branch counts, and storage usage.
pgbranch statusController: healthy (v0.1.0, uptime 4h32m)
Trunks: 2 running / 3 total
Branches: 3 running, 1 stopped, 4 total
Storage: 156.2 MB used
Trunks are independent source databases that branches are created from. The default trunk "main" is always present.
Create a new trunk database with its own PostgreSQL instance.
# Create a trunk (auto-allocates a port)
pgbranch trunk create staging
# Create with a specific port
pgbranch trunk create staging --port 5433| Flag | Default | Description |
|---|---|---|
--port |
(auto-allocated) | Port for the trunk's PostgreSQL instance |
List all trunks.
pgbranch trunk listNAME STATUS PORT DATABASE WAL LEVEL
main running 5432 postgres replica
staging running 5433 staging logical
Show detailed information about a trunk. Defaults to "main" if no name is given.
pgbranch trunk info
pgbranch trunk info stagingTrunk Database: staging
Status: running
Port: 5433
Database: staging
PG Version: 16
WAL Level: logical
Started: 2025-06-15T10:00:00Z (4h ago)
Connection: postgresql://postgres@localhost:5433/staging
Replication: active
Remote: postgresql://***@prod-db:5432/myapp
Publication: pgbranch_pub
Subscription: pgbranch_sub
Slot: pgbranch_staging
Start a stopped trunk.
pgbranch trunk start stagingStop a running trunk. Running branches are not affected.
pgbranch trunk stop stagingRestart a trunk (e.g. after a WAL level change).
pgbranch trunk restart stagingConnect to a trunk database. Same flags as pgbranch connect.
pgbranch trunk connect staging
pgbranch trunk connect staging --uriDestroy a trunk and all its branches. The "main" trunk cannot be destroyed.
pgbranch trunk destroy staging --yesAny trunk can replicate data from a remote PostgreSQL database via logical replication. This lets branches inherit real production data.
Set up logical replication from a remote database into a trunk.
pgbranch trunk replicate setup \
--trunk staging \
--remote-dsn "postgresql://user:pass@prod-db:5432/myapp" \
--publication pgbranch_pub \
--tables "public.*"The setup process:
- Connects to the remote DB and verifies
wal_level=logical - Creates (or verifies) a publication on the remote
- Ensures the trunk has
wal_level=logical(restarts if needed) - Syncs the schema from remote to local
- Creates a replication slot on the remote (idempotent -- reuses existing slots)
- Creates a subscription on the trunk using the pre-created slot
| Flag | Default | Description |
|---|---|---|
--trunk |
main |
Trunk to replicate into |
--remote-dsn |
(required) | Connection string for the remote database |
--publication |
pgbranch_pub |
Name of the publication on the remote database |
--tables |
(all tables) | Table filter pattern (e.g. public.*) |
--slot-name |
pgbranch_<trunk> |
Replication slot name on the remote |
Show the current replication status, including lag and last received LSN.
pgbranch trunk replicate status --trunk stagingStatus: active
Remote: postgresql://***@prod-db:5432/myapp
Publication: pgbranch_pub
Subscription: pgbranch_sub
Slot: pgbranch_staging
Last LSN: 0/1A3B4C5D
Lag: 1.2 KB
Print server-validated step-by-step instructions for setting up replication manually (useful when automated setup cannot reach the remote DB directly).
pgbranch trunk replicate guide --remote-dsn "postgresql://user:pass@prod-db:5432/myapp"Remove the replication subscription from a trunk. Also drops the remote replication slot.
pgbranch trunk replicate teardown --trunk staging --yes| Flag | Default | Description |
|---|---|---|
--trunk |
main |
Trunk to tear down replication for |
--yes |
false |
Skip the confirmation prompt |
pgbranch manages replication slots on the remote database. Setup creates slots automatically, but you can also manage them explicitly.
List all logical replication slots on the remote database.
pgbranch trunk replicate slot list --trunk stagingSLOT ACTIVE RESTART LSN CONFIRMED LSN RETAINED WAL
pgbranch_staging true 0/1A3B4C5D 0/1A3B4C5D 1.2 KB
pgbranch_sub false 0/1A300000 0/1A300000 4.5 MB
Create a replication slot on the remote.
pgbranch trunk replicate slot create \
--trunk staging \
--remote-dsn "postgresql://user:pass@prod-db:5432/myapp"| Flag | Default | Description |
|---|---|---|
--trunk |
main |
Trunk this slot is for |
--remote-dsn |
(required) | Remote database connection string |
--slot-name |
pgbranch_<trunk> |
Slot name (auto-generated if empty) |
Show detailed status of the trunk's replication slot on the remote.
pgbranch trunk replicate slot status --trunk stagingSlot: pgbranch_staging
Active: true
Restart LSN: 0/1A3B4C5D
Confirmed LSN: 0/1A3B4C5D
Retained WAL: 1.2 KB
Drop a replication slot on the remote. Useful for cleaning up stale slots from a previous setup.
pgbranch trunk replicate slot drop --trunk staging --yesAll endpoints are prefixed with /api/v1. Responses use Content-Type: application/json. Errors follow RFC 7807 (Problem Details).
If PGBRANCH_API_TOKEN is set, all endpoints except /api/v1/health require a Authorization: Bearer <token> header.
Returns the health status of the server. Does not require authentication.
Response 200 OK:
{
"status": "healthy",
"version": "0.1.0",
"trunks_running": 2,
"trunks_total": 3,
"branches_running": 3,
"branches_total": 4,
"storage_used_bytes": 163840000,
"uptime_seconds": 16320
}Create a new trunk.
Request body:
{
"name": "staging",
"port": 5433
}port is optional and auto-allocated if omitted.
Response 201 Created:
{
"name": "staging",
"status": "creating",
"port": 5433,
"database": "staging"
}List all trunks.
Response 200 OK:
{
"trunks": [
{
"name": "main",
"status": "running",
"port": 5432,
"database": "postgres",
"wal_level": "replica"
}
],
"total": 1
}Get detailed information about a trunk.
Destroy a trunk and all its branches. The "main" trunk cannot be deleted.
Get connection details for a trunk.
Start a stopped trunk.
Stop a running trunk.
Restart a trunk.
Get the current replication status for a trunk.
Response 200 OK:
{
"status": "active",
"remote_dsn": "postgresql://***@prod-db:5432/myapp",
"pub_name": "pgbranch_pub",
"sub_name": "pgbranch_sub",
"slot_name": "pgbranch_staging",
"last_lsn": "0/1A3B4C5D",
"lag_bytes": 1234
}Set up logical replication from a remote database. Creates the slot on the remote automatically.
Request body:
{
"remote_dsn": "postgresql://user:pass@prod-db:5432/myapp",
"publication": "pgbranch_pub",
"tables": "public.*",
"slot_name": "pgbranch_staging"
}All fields except remote_dsn are optional. slot_name defaults to pgbranch_<trunk>.
Response 202 Accepted:
{
"status": "configuring"
}Get server-validated step-by-step instructions for manual replication setup.
Remove the replication subscription and drop the remote slot.
List all logical replication slots on the remote database.
Response 200 OK:
{
"slots": [
{
"slot_name": "pgbranch_staging",
"active": true,
"restart_lsn": "0/1A3B4C5D",
"confirmed_lsn": "0/1A3B4C5D",
"retained_bytes": 1234
}
],
"total": 1
}Create a replication slot on the remote (idempotent).
Get the status of the trunk's replication slot on the remote.
Drop the replication slot on the remote.
Create a new branch.
Request body:
{
"name": "my-feature",
"parent": "main",
"ttl": "2h",
"tags": ["ci", "pr-123"]
}All fields are optional. If name is omitted, one is generated (e.g. branch-a1b2c3d4). If parent is omitted, it defaults to main. The parent can be a trunk name or a branch name.
Response 201 Created:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "my-feature",
"parent": "main",
"status": "creating",
"created_at": "2025-06-15T10:30:00Z",
"connection": {
"host": "localhost",
"port": 5501,
"user": "postgres",
"database": "postgres",
"uri": "postgresql://postgres@localhost:5501/postgres"
}
}Branch creation is asynchronous. The branch transitions through creating -> starting -> running. Poll GET /api/v1/branches/{name} to check the status.
Error responses:
| Status | Type | Cause |
|---|---|---|
400 |
invalid_request |
Malformed request body or invalid TTL |
400 |
invalid_name |
Branch name does not match naming rules |
404 |
parent_not_found |
Parent trunk or branch does not exist |
409 |
name_conflict |
A branch or trunk with that name already exists |
422 |
max_branches_exceeded |
System branch limit reached |
422 |
max_depth_exceeded |
Branch tree depth limit reached |
422 |
parent_not_ready |
Parent branch is in an invalid state |
503 |
insufficient_resources |
No ports available |
List all branches. Supports query-parameter filtering.
Query parameters:
| Parameter | Description |
|---|---|
status |
Filter by status (e.g. running, stopped, error) |
parent |
Filter by parent trunk or branch name |
tag |
Filter by tag |
Response 200 OK:
{
"branches": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "my-feature",
"parent": "main",
"status": "running",
"created_at": "2025-06-15T10:30:00Z",
"size_bytes": 12902400,
"tags": ["ci", "pr-123"],
"connection": {
"host": "localhost",
"port": 5501,
"user": "postgres",
"database": "postgres",
"uri": "postgresql://postgres@localhost:5501/postgres"
}
}
],
"total": 1
}Get detailed information about a single branch, including its children.
Destroy a branch and reclaim its storage. Running branches are automatically stopped (no force flag needed).
Query parameters:
| Parameter | Default | Description |
|---|---|---|
recursive |
false |
Also destroy all descendant branches |
Response 202 Accepted:
{
"name": "my-feature",
"status": "deleting",
"children_deleted": ["pr-123-hotfix"]
}Start a stopped (or errored) branch.
Stop a running branch.
Get connection details for a running branch.
Response 200 OK:
{
"host": "localhost",
"port": 5501,
"user": "postgres",
"password": "trust",
"database": "postgres",
"uri": "postgresql://postgres:trust@localhost:5501/postgres",
"psql": "psql -h localhost -p 5501 -U postgres postgres"
}The pgbranchd server is configured entirely through environment variables. All variables are optional and have sensible defaults.
| Variable | Default | Description |
|---|---|---|
PGBRANCH_API_PORT |
8080 |
Port for the REST API server |
PGBRANCH_API_TOKEN |
(empty) | Bearer token for API authentication. Empty disables auth. |
PGBRANCH_LOG_LEVEL |
info |
Log level: debug, info, warn, error |
PGBRANCH_DATA_DIR |
/var/lib/pgbranch |
Base data directory for metadata and pool backing |
PGBRANCH_METADATA_PATH |
/var/lib/pgbranch/metadata.db |
Path to the SQLite metadata database |
| Variable | Default | Description |
|---|---|---|
PGBRANCH_STORAGE_BACKEND |
zfs |
Storage backend: zfs, lvm, or auto |
PGBRANCH_POOL_NAME |
pgbranch |
Name of the ZFS pool or LVM volume group prefix |
PGBRANCH_POOL_SIZE |
20G |
Size of the sparse backing file |
PGBRANCH_POOL_IMAGE_PATH |
/var/lib/pgbranch/zpool.img |
Path to the sparse file backing the storage pool |
| Variable | Default | Description |
|---|---|---|
PGBRANCH_PG_BIN_DIR |
/usr/lib/postgresql/16/bin |
Path to PostgreSQL binaries (pg_ctl, initdb, etc.) |
PGBRANCH_DEFAULT_PG_USER |
postgres |
Default PostgreSQL user for branch databases |
PGBRANCH_DEFAULT_PG_DB |
postgres |
Default database name for branch connections |
PGBRANCH_PORT_RANGE_START |
5500 |
Start of the port range allocated to branch Postgres instances |
PGBRANCH_PORT_RANGE_END |
5599 |
End of the port range (inclusive). Limits max concurrent branches. |
| Variable | Default | Description |
|---|---|---|
PGBRANCH_DEFAULT_TRUNK_PORT |
5432 |
Port for the default "main" trunk Postgres instance |
PGBRANCH_TRUNK_AUTO_START |
true |
Auto-start all trunks when the daemon starts |
PGBRANCH_DEFAULT_WAL_LEVEL |
replica |
Default WAL level for trunks. Set to logical for replication. |
For backward compatibility, the old variable names still work:
| Old Variable | Maps To |
|---|---|
PGBRANCH_MAIN_PORT |
PGBRANCH_DEFAULT_TRUNK_PORT |
PGBRANCH_MAIN_AUTO_START |
PGBRANCH_TRUNK_AUTO_START |
PGBRANCH_MAIN_WAL_LEVEL |
PGBRANCH_DEFAULT_WAL_LEVEL |
| Variable | Default | Description |
|---|---|---|
PGBRANCH_MAX_BRANCHES |
50 |
Maximum number of branches allowed |
PGBRANCH_MAX_DEPTH |
10 |
Maximum branch tree depth (nested branching) |
PGBRANCH_REAPER_INTERVAL |
30s |
How often the TTL reaper checks for expired branches (Go duration) |
PGBRANCH_RESET_STORAGE |
false |
Destroy and reinitialize the storage pool on startup |
pgbranch includes a Helm chart in charts/pgbranch/.
helm install pgbranch ./charts/pgbranch \
--namespace pgbranch \
--create-namespacehelm upgrade pgbranch ./charts/pgbranch \
--namespace pgbranch| Key | Default | Description |
|---|---|---|
replicaCount |
1 |
Must be 1. pgbranch is a single-node stateful service. |
image.repository |
pgbranch |
Container image repository |
image.tag |
(Chart appVersion) |
Container image tag |
securityContext.privileged |
true |
Required for ZFS/LVM kernel operations |
config.storageBackend |
zfs |
Storage backend: zfs or lvm |
config.poolName |
pgbranch |
ZFS pool name |
config.poolSize |
50G |
Sparse backing file size |
config.portRangeStart |
5500 |
Start of Postgres port range |
config.portRangeEnd |
5599 |
End of Postgres port range |
config.maxBranches |
50 |
Maximum number of branches |
config.maxDepth |
10 |
Maximum branch tree depth |
config.logLevel |
info |
Log level |
config.pgBinDir |
/usr/lib/postgresql/16/bin |
Path to PostgreSQL binaries |
config.reaperInterval |
30s |
TTL reaper check interval |
config.defaultTrunkPort |
5432 |
Default trunk Postgres port |
config.trunkAutoStart |
true |
Auto-start trunks with the daemon |
config.defaultWalLevel |
replica |
Default WAL level for trunks |
secrets.apiToken |
(empty) | API bearer token (empty disables auth) |
secrets.replicationRemoteDsn |
(empty) | Remote database DSN for logical replication |
service.api.type |
ClusterIP |
Service type for the REST API |
service.api.port |
8080 |
API service port |
service.postgres.type |
NodePort |
Service type for Postgres branch access |
persistence.enabled |
true |
Enable persistent storage for the backing pool |
persistence.size |
50Gi |
PVC size |
persistence.storageClassName |
(cluster default) | Storage class for the PVC |
resources.requests.cpu |
500m |
CPU request |
resources.requests.memory |
512Mi |
Memory request |
resources.limits.cpu |
4 |
CPU limit |
resources.limits.memory |
4Gi |
Memory limit |
helm install pgbranch ./charts/pgbranch \
--namespace pgbranch \
--create-namespace \
--set secrets.apiToken="my-secret-token" \
--set config.poolSize="100G" \
--set persistence.size="100Gi" \
--set config.maxBranches=100- Go 1.23+
- Docker (with Docker Compose)
- On macOS: Colima (for ZFS kernel module support)
# Build both server and CLI binaries to ./bin/
make build
# Build only the server
make build-server
# Build only the CLI
make build-cliBinaries are output to the bin/ directory with version information injected via ldflags.
# Run all unit tests
make test
# Run tests with verbose output
make test-verbose
# Run tests with a coverage report
make test-cover# Run golangci-lint
make lint
# Format source files
make fmt
# Run go vet
make vet# Build the Docker image
make docker-build
# Start the local environment (API on :8080, trunks on :5432+, branches on :5500-5599)
make docker-up
# Follow container logs
make docker-logs
# Open a shell in the running container
make docker-shell
# Stop the environment
make docker-down
# Stop and remove all volumes (destroys all data)
make docker-down-volumesDocker Desktop's linuxkit kernel does not include the ZFS or LVM thin-pool kernel modules required for copy-on-write snapshots. The setup-colima.sh script creates a Colima VM with a full Ubuntu kernel and installs ZFS.
# Start Colima with ZFS support (4 CPUs, 6 GB RAM, 60 GB disk)
make colima-start
# Check status
./scripts/setup-colima.sh status
# Stop
make colima-stop
# Delete the VM entirely
make colima-deleteYou can customize Colima resources with environment variables:
PGBRANCH_COLIMA_CPUS=8 PGBRANCH_COLIMA_MEMORY=12 PGBRANCH_COLIMA_DISK=100 \
./scripts/setup-colima.sh start# Create a k3d cluster with port forwarding
make k3d-create
# Build and load the image into the cluster
make k3d-load
# Deploy via Helm
make helm-install
# Tear down
make helm-uninstall
make k3d-deleteThe ZFS engine (internal/engine/zfs.go) manages a file-backed zpool. On initialization it:
- Creates a sparse file at the configured path (default:
/var/lib/pgbranch/zpool.img). - Creates a zpool on that file (
zpool create pgbranch /var/lib/pgbranch/zpool.img). - Applies PostgreSQL-optimized ZFS properties:
compression=lz4-- transparent compression reduces disk I/Oatime=off-- disables access time trackingrecordsize=16K-- matches the PostgreSQL page sizelogbias=latency-- optimized for OLTP workloadsredundant_metadata=most-- balanced metadata protectionxattr=sa-- stores extended attributes in inodes
Branch operations map to ZFS primitives:
| pgbranch Operation | ZFS Command |
|---|---|
| Create snapshot | zfs snapshot pgbranch/trunks/main@branch-snap |
| Create clone | zfs clone pgbranch/trunks/main@branch-snap pgbranch/branches/my-feature |
| Destroy branch | zfs destroy -r pgbranch/branches/my-feature |
| Get mount point | zfs get mountpoint pgbranch/branches/my-feature |
When to use ZFS: Any environment where the ZFS kernel module can be loaded. This includes most Linux distributions, FreeBSD, and macOS via Colima. ZFS is the recommended backend for both development and production.
The LVM engine (internal/engine/lvm.go) uses device-mapper thin provisioning. On initialization it:
- Creates a sparse backing file.
- Attaches it as a loopback device (
losetup). - Creates a physical volume, volume group, and thin pool LV (95% of VG space, 5% for metadata).
- Creates thin logical volumes formatted with ext4 for each dataset.
Branch operations map to LVM commands:
| pgbranch Operation | LVM Command |
|---|---|
| Create snapshot | lvcreate --snapshot -n <snap> <vg>/<source> |
| Create clone | lvcreate --snapshot -n <clone> <vg>/<snap> + mount |
| Destroy branch | umount + lvremove -f <vg>/<lv> |
| Get mount point | Computed from MountBase + name |
When to use LVM: Environments where ZFS is not available. The LVM thin device-mapper target is available on virtually all Linux kernels, including Docker Desktop's linuxkit, AWS EKS nodes, and minimal cloud images. LVM thin provisioning provides genuine copy-on-write snapshots but does not include built-in compression.
When PGBRANCH_STORAGE_BACKEND is set to auto (the default in Docker), the server probes in this order:
- ZFS: Checks for
/dev/zfs, then triesmodprobe zfs. - LVM: Checks for
lvcreateon PATH and verifies thethindevice-mapper target viadmsetup targets.
If neither backend is available, the server exits with an error.
pgbranch/
├── cmd/
│ ├── pgbranch/ # CLI client
│ │ └── main.go
│ └── pgbranchd/ # Server daemon
│ └── main.go
├── internal/
│ ├── api/ # REST API (chi router, handlers, middleware)
│ │ ├── handlers.go # Endpoint handlers (trunks, branches, replication)
│ │ ├── middleware.go # Logging and bearer-token auth middleware
│ │ └── server.go # HTTP server, routing, TTL reaper, reconciler
│ ├── client/ # Go HTTP client for the pgbranch API
│ │ └── client.go
│ ├── engine/ # Copy-on-write snapshot engine interface + backends
│ │ ├── engine.go # Engine interface definition
│ │ ├── zfs.go # ZFS backend implementation
│ │ └── lvm.go # LVM thin provisioning backend implementation
│ ├── model/ # Data types, config, validation
│ │ └── model.go
│ ├── postgres/ # pg_ctl process manager
│ │ └── manager.go
│ ├── replication/ # Logical replication + remote slot manager (pgx/v5)
│ │ └── replication.go
│ └── store/ # SQLite metadata store (trunks + branches)
│ └── store.go
├── charts/
│ └── pgbranch/ # Helm chart
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
├── scripts/
│ ├── entrypoint.sh # Container entrypoint
│ ├── init-zfs.sh # ZFS pool initialization script
│ └── setup-colima.sh # macOS Colima setup for ZFS support
├── docs/ # Design documents and research
├── Dockerfile # Multi-stage build (Go builder + Ubuntu 22.04 runtime)
├── docker-compose.yaml # Local development environment
├── Makefile # Build, test, Docker, Helm, and Colima targets
├── go.mod
└── go.sum
Apache 2.0