Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/deeplethe/forkd/llms.txt

Use this file to discover all available pages before exploring further.

The traditional “one Postgres container per test” approach costs roughly 2 seconds per test run: ~300 ms for the container to start, ~1.5 s for initdb, and ~200 ms for the postmaster to begin accepting connections. With forkd, initdb runs once when the parent snapshot is built. Every fork inherits the post-init, postmaster-running state via copy-on-write memory. The result is ~10 ms per child instead of ~2 s — roughly a 200× speedup at the fixture level, with full KVM isolation between each test’s database.

The cost breakdown

StepCold-start cost (per test)forkd cost (per fork)
Container / VM start~300 ms
initdb~1 500 ms— (runs once at build)
Postmaster ready-to-accept~200 ms~10 ms
Total~2 000 ms~10 ms

Build the snapshot

1

Build the parent rootfs

cd recipes/postgres-fixture
sudo bash build.sh
Starts from postgres:16, runs initdb, seeds the forkd_test database, and launches the postmaster. The entire post-init state is baked into parent.ext4. Rootfs: ~500 MB.Custom credentials can be baked at build time:
PG_USER=app PG_PASSWORD=hunter2 PG_DATABASE=mydb \
    sudo -E bash recipes/postgres-fixture/build.sh
2

Set up host networking

sudo bash scripts/host-tap.sh
sudo bash scripts/netns-setup.sh 20
3

Snapshot the warm parent

sudo -E forkd snapshot --tag pgfix \
    --kernel ./vmlinux-6.1.141 \
    --rootfs recipes/postgres-fixture/parent.ext4 \
    --tap forkd-tap0 \
    --boot-wait-secs 15    # let initdb + postmaster settle
4

Fork isolated databases

sudo -E forkd fork --tag pgfix -n 20 --per-child-netns --memory-limit-mib 512

What the build produces

Every fork of the pgfix snapshot starts with:
  • Postgres 16 postmaster already running and accepting connections
  • Database forkd_test (or your custom $PG_DATABASE) already initialized
  • User forkd / password forkd with trust auth on 0.0.0.0/0 (safe — Postgres is only reachable inside the child’s private netns)
  • Any SQL files placed in /docker-entrypoint-initdb.d/ during the build run automatically — the recommended way to pre-seed a schema

The fork-per-test demo

The demo.py script fans out 10 isolated Postgres children, runs a CREATE TABLE / INSERT / SELECT sequence in each, and proves the databases are independent by verifying each child sees only its own rows.
# demo.py — spawn 10 isolated postgres children and query each

DAEMON = os.environ.get("FORKD_URL", "http://127.0.0.1:8889")
N = int(os.environ.get("N", "10"))
PG_USER = os.environ.get("PG_USER", "forkd")
PG_DB = os.environ.get("PG_DATABASE", "forkd_test")


def psql_in_netns(netns: str, host: str, sql: str) -> str:
    """Run psql inside a per-child netns and return stdout."""
    cmd = [
        "ip", "netns", "exec", netns,
        "psql", "-h", host, "-p", "5432",
        "-U", PG_USER, "-d", PG_DB,
        "-tAc", sql,
    ]
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
    if r.returncode != 0:
        raise RuntimeError(f"psql failed: {r.stderr.strip()}")
    return r.stdout.strip()


def main() -> int:
    t0 = time.perf_counter()
    sbs = post(
        "/v1/sandboxes",
        {"snapshot_tag": "pgfix", "n": N, "per_child_netns": True, "memory_limit_mib": 512},
    )
    t_spawn = time.perf_counter()
    print(f"spawned {len(sbs)} postgres children in {(t_spawn - t0) * 1000:.0f} ms")

    # Each child gets a unique row id; afterwards we'll prove the
    # databases are independent by checking each child sees only its own row.
    for i, sb in enumerate(sbs):
        host = sb["guest_addr"].split(":")[0]
        t_q0 = time.perf_counter()
        psql_in_netns(
            sb["netns"], host,
            f"CREATE TABLE marker (id int); INSERT INTO marker VALUES ({i});",
        )
        count = psql_in_netns(sb["netns"], host, "SELECT count(*) FROM marker;")
        t_q1 = time.perf_counter()
        print(f"  child {i} ({sb['netns']}): rows={count} in {(t_q1 - t_q0) * 1000:.0f} ms")

    for sb in sbs:
        delete(f"/v1/sandboxes/{sb['id']}")
    print(f"torn down; total wall-clock {(time.perf_counter() - t0) * 1000:.0f} ms")
    return 0
Run it with:
FORKD_TOKEN=$(sudo cat /etc/forkd/token) python3 recipes/postgres-fixture/demo.py

Python SDK usage

from forkd import Sandbox
import psycopg

with Sandbox(tag="pgfix") as sb:
    # Each `with` block is a fresh child VM with its own postgres
    conn = psycopg.connect(
        host=sb.guest_addr.split(":")[0],
        port=5432,
        user="forkd",
        dbname="forkd_test",
    )
    with conn.cursor() as cur:
        cur.execute("CREATE TABLE users (id int PRIMARY KEY, name text)")
        cur.execute("INSERT INTO users VALUES (1, 'alice')")
        cur.execute("SELECT name FROM users WHERE id = 1")
        assert cur.fetchone()[0] == "alice"
    conn.close()
# Sandbox.__exit__ kills the child VM; postgres state goes with it.

Performance numbers

Measured on Linux 6.14 / 20 vCPU / 30 GiB / KVM with a 300 MB rootfs. “Ready-to-query” includes the round-trip needed to psql -c "SELECT 1" from outside the netns.
NWall-clock (fork + ready-to-query)Per-child
1~50 ms50 ms
10~80 ms8 ms
50~250 ms5 ms
100~500 ms5 ms

Isolation guarantees

Each child is a separate Firecracker microVM with its own KVM address space. From Postgres’s perspective:
  • /dev/shm is per-child VM — shared memory buffers don’t collide between tests
  • WAL is per-child — transaction logs diverge independently
  • Postmaster PID is the same in all children at fork time (inherited from the parent snapshot), but PIDs are per-PID-namespace, so there is no conflict
  • Schema / data writes diverge per child via mmap CoW — one test writing rows never affects a sibling

Pre-loading a schema

To ship a schema into every child automatically, place migration SQL in /docker-entrypoint-initdb.d/ before taking the snapshot:
sudo mount -o loop recipes/postgres-fixture/parent.ext4 /mnt/pg-parent
sudo cp my-schema.sql /mnt/pg-parent/docker-entrypoint-initdb.d/
sudo umount /mnt/pg-parent
# Then re-snapshot
sudo -E forkd snapshot --tag pgfix ...
Files in /docker-entrypoint-initdb.d/ run during the first-boot hook before the snapshot is taken.

Use cases

CI integration tests

Hundreds of DB-touching integration tests in parallel — each test gets its own Postgres with the same schema, no cleanup required between runs.

Schema migration testing

Boot the parent at the previous migration version, fork N children, apply a migration candidate in each, compare results. No shared state, no ordering dependencies.

Parallel CI workers

Replace per-worker Docker Postgres containers. Skip per-worker container cold-start and initdb; pay only the ~5–10 ms fork cost instead.

Fuzz testing

Each fuzz run gets a clean database at the parent’s seeded state — no teardown, no re-init, no accumulated corruption from prior runs.
This recipe is designed for ephemeral test fixtures. Postgres state is lost when the child VM is killed. For persistent storage across forks, attach a separate volume after the fork — that pattern is not yet included in this recipe.

Build docs developers (and LLMs) love