Skip to content

Self-hosted PostgreSQL database branching. pgbranch provides instant copy-on-write branches of a PostgreSQL database, similar to Neon but fully self-hosted with no vendor lock-in.

License

Notifications You must be signed in to change notification settings

jlmorton/pgbranch

Repository files navigation

pgbranch

Instant copy-on-write PostgreSQL database branching -- a self-hosted alternative to Neon.


Overview

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.


Architecture

                        ┌─────────────────────────────────────────────┐
                        │          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:

  1. 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.
  2. The server runs a CHECKPOINT on the source (a trunk or another branch) and takes a point-in-time snapshot of the dataset.
  3. A writable clone is created from that snapshot. With ZFS this is zfs clone; with LVM this is a thin snapshot.
  4. A new Postgres instance is started on an allocated port using pg_ctl.
  5. 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

Why ZFS / Why LVM

pgbranch supports two copy-on-write storage backends. On startup, the server auto-detects the best available backend in this order: ZFS > LVM.

ZFS

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

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.


Quick Start

macOS (with Colima)

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

Linux (with Docker)

On 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 --psql

Verify Installation

pgbranch status

Expected 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 connect

Shell Completion

Install shell completions so tab-completion works in every new session:

pgbranch completion install

This auto-detects your shell (bash, zsh, or fish) and installs the completion script. To override the detection:

pgbranch completion install --shell zsh

To remove completions:

pgbranch completion uninstall

CLI Reference

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

Global Flags

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

pgbranch create [name]

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

pgbranch list

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

Example 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

pgbranch info <name>

Show detailed information about a single branch.

pgbranch info my-feature
Name:        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

pgbranch connect <name>

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

pgbranch destroy <name>

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

pgbranch start <name>

Start a stopped branch. Waits for Postgres to become ready.

pgbranch start my-feature
Starting branch "my-feature"... ready
  Connection: postgresql://postgres@localhost:5501/postgres

pgbranch stop <name>

Stop a running branch. The data is preserved; the Postgres process is shut down and the port is released.

pgbranch stop my-feature
Stopping branch "my-feature"... done

pgbranch tag <name> <tags...>

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

pgbranch status

Show overall system status including controller health, trunk counts, branch counts, and storage usage.

pgbranch status
Controller:  healthy (v0.1.0, uptime 4h32m)
Trunks:      2 running / 3 total
Branches:    3 running, 1 stopped, 4 total
Storage:     156.2 MB used

Trunk Management

Trunks are independent source databases that branches are created from. The default trunk "main" is always present.

pgbranch trunk create <name>

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

pgbranch trunk list

List all trunks.

pgbranch trunk list
NAME       STATUS   PORT   DATABASE   WAL LEVEL
main       running  5432   postgres   replica
staging    running  5433   staging    logical

pgbranch trunk info [name]

Show detailed information about a trunk. Defaults to "main" if no name is given.

pgbranch trunk info
pgbranch trunk info staging
Trunk 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

pgbranch trunk start [name]

Start a stopped trunk.

pgbranch trunk start staging

pgbranch trunk stop [name]

Stop a running trunk. Running branches are not affected.

pgbranch trunk stop staging

pgbranch trunk restart [name]

Restart a trunk (e.g. after a WAL level change).

pgbranch trunk restart staging

pgbranch trunk connect [name]

Connect to a trunk database. Same flags as pgbranch connect.

pgbranch trunk connect staging
pgbranch trunk connect staging --uri

pgbranch trunk destroy <name>

Destroy a trunk and all its branches. The "main" trunk cannot be destroyed.

pgbranch trunk destroy staging --yes

Replication

Any trunk can replicate data from a remote PostgreSQL database via logical replication. This lets branches inherit real production data.

pgbranch trunk replicate setup

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:

  1. Connects to the remote DB and verifies wal_level=logical
  2. Creates (or verifies) a publication on the remote
  3. Ensures the trunk has wal_level=logical (restarts if needed)
  4. Syncs the schema from remote to local
  5. Creates a replication slot on the remote (idempotent -- reuses existing slots)
  6. 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

pgbranch trunk replicate status

Show the current replication status, including lag and last received LSN.

pgbranch trunk replicate status --trunk staging
Status:        active
Remote:        postgresql://***@prod-db:5432/myapp
Publication:   pgbranch_pub
Subscription:  pgbranch_sub
Slot:          pgbranch_staging
Last LSN:      0/1A3B4C5D
Lag:           1.2 KB

pgbranch trunk replicate guide

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"

pgbranch trunk replicate teardown

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

Replication Slot Management

pgbranch manages replication slots on the remote database. Setup creates slots automatically, but you can also manage them explicitly.

pgbranch trunk replicate slot list

List all logical replication slots on the remote database.

pgbranch trunk replicate slot list --trunk staging
SLOT              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

pgbranch trunk replicate slot create

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)

pgbranch trunk replicate slot status

Show detailed status of the trunk's replication slot on the remote.

pgbranch trunk replicate slot status --trunk staging
Slot:            pgbranch_staging
Active:          true
Restart LSN:     0/1A3B4C5D
Confirmed LSN:   0/1A3B4C5D
Retained WAL:    1.2 KB

pgbranch trunk replicate slot drop

Drop a replication slot on the remote. Useful for cleaning up stale slots from a previous setup.

pgbranch trunk replicate slot drop --trunk staging --yes

REST API Reference

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


GET /api/v1/health

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
}

Trunk Endpoints

POST /api/v1/trunks

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"
}

GET /api/v1/trunks

List all trunks.

Response 200 OK:

{
  "trunks": [
    {
      "name": "main",
      "status": "running",
      "port": 5432,
      "database": "postgres",
      "wal_level": "replica"
    }
  ],
  "total": 1
}

GET /api/v1/trunks/{name}

Get detailed information about a trunk.

DELETE /api/v1/trunks/{name}

Destroy a trunk and all its branches. The "main" trunk cannot be deleted.

GET /api/v1/trunks/{name}/connection

Get connection details for a trunk.

POST /api/v1/trunks/{name}/start

Start a stopped trunk.

POST /api/v1/trunks/{name}/stop

Stop a running trunk.

POST /api/v1/trunks/{name}/restart

Restart a trunk.


Trunk Replication Endpoints

GET /api/v1/trunks/{name}/replication

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
}

POST /api/v1/trunks/{name}/replication/setup

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"
}

POST /api/v1/trunks/{name}/replication/guide

Get server-validated step-by-step instructions for manual replication setup.

POST /api/v1/trunks/{name}/replication/teardown

Remove the replication subscription and drop the remote slot.

GET /api/v1/trunks/{name}/replication/slots

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
}

POST /api/v1/trunks/{name}/replication/slot

Create a replication slot on the remote (idempotent).

GET /api/v1/trunks/{name}/replication/slot

Get the status of the trunk's replication slot on the remote.

DELETE /api/v1/trunks/{name}/replication/slot

Drop the replication slot on the remote.


Branch Endpoints

POST /api/v1/branches

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

GET /api/v1/branches

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 /api/v1/branches/{name}

Get detailed information about a single branch, including its children.

DELETE /api/v1/branches/{name}

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"]
}

POST /api/v1/branches/{name}/start

Start a stopped (or errored) branch.

POST /api/v1/branches/{name}/stop

Stop a running branch.

GET /api/v1/branches/{name}/connection

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"
}

Configuration

The pgbranchd server is configured entirely through environment variables. All variables are optional and have sensible defaults.

Server Configuration

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

Storage Configuration

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

PostgreSQL Configuration

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.

Trunk Configuration

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

Limits and Maintenance

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

Kubernetes Deployment

pgbranch includes a Helm chart in charts/pgbranch/.

Install

helm install pgbranch ./charts/pgbranch \
  --namespace pgbranch \
  --create-namespace

Upgrade

helm upgrade pgbranch ./charts/pgbranch \
  --namespace pgbranch

Key values.yaml Options

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

Example: Production with Authentication

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

Local Development

Prerequisites

  • Go 1.23+
  • Docker (with Docker Compose)
  • On macOS: Colima (for ZFS kernel module support)

Building from Source

# Build both server and CLI binaries to ./bin/
make build

# Build only the server
make build-server

# Build only the CLI
make build-cli

Binaries are output to the bin/ directory with version information injected via ldflags.

Running Tests

# Run all unit tests
make test

# Run tests with verbose output
make test-verbose

# Run tests with a coverage report
make test-cover

Linting and Formatting

# Run golangci-lint
make lint

# Format source files
make fmt

# Run go vet
make vet

Docker Development Environment

# 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-volumes

macOS Colima Setup

Docker 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-delete

You can customize Colima resources with environment variables:

PGBRANCH_COLIMA_CPUS=8 PGBRANCH_COLIMA_MEMORY=12 PGBRANCH_COLIMA_DISK=100 \
  ./scripts/setup-colima.sh start

Local Kubernetes Development (k3d)

# 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-delete

Storage Backends

ZFS Backend

The ZFS engine (internal/engine/zfs.go) manages a file-backed zpool. On initialization it:

  1. Creates a sparse file at the configured path (default: /var/lib/pgbranch/zpool.img).
  2. Creates a zpool on that file (zpool create pgbranch /var/lib/pgbranch/zpool.img).
  3. Applies PostgreSQL-optimized ZFS properties:
    • compression=lz4 -- transparent compression reduces disk I/O
    • atime=off -- disables access time tracking
    • recordsize=16K -- matches the PostgreSQL page size
    • logbias=latency -- optimized for OLTP workloads
    • redundant_metadata=most -- balanced metadata protection
    • xattr=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.

LVM Thin Provisioning Backend

The LVM engine (internal/engine/lvm.go) uses device-mapper thin provisioning. On initialization it:

  1. Creates a sparse backing file.
  2. Attaches it as a loopback device (losetup).
  3. Creates a physical volume, volume group, and thin pool LV (95% of VG space, 5% for metadata).
  4. 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.

Auto-Detection

When PGBRANCH_STORAGE_BACKEND is set to auto (the default in Docker), the server probes in this order:

  1. ZFS: Checks for /dev/zfs, then tries modprobe zfs.
  2. LVM: Checks for lvcreate on PATH and verifies the thin device-mapper target via dmsetup targets.

If neither backend is available, the server exits with an error.


Project Structure

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

License

Apache 2.0

About

Self-hosted PostgreSQL database branching. pgbranch provides instant copy-on-write branches of a PostgreSQL database, similar to Neon but fully self-hosted with no vendor lock-in.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •