# How to run PostgreSQL in a sandbox for tests and agents Install Ubuntu's postgresql package in a microVM, start a server with initdb and postgres, load your schema, and query it with psql. **On Runtime, a seeded database can be copied, running, in one call.** Load the schema and fixtures once, then fork the sandbox: every copy starts with Postgres already up and the data in memory, so ten tests or ten agent attempts each get a private database without ten restores. Every sandbox is a Firecracker microVM, and one that holds an idle database costs $0.03125 an hour for 2 vCPU and 4 GiB ([pricing](/docs/pricing)). Ubuntu 24.04 shipped PostgreSQL 16.15 on 25 September 2026; the PostgreSQL project's own apt repository had 18 as its current stable release. ## Start a database and load a schema Ubuntu's package creates a cluster of its own; the sample drops it and makes one in `/workspace` that belongs to the sandbox user, so nothing else depends on how the system starts services. ```ts check import { writeFile } from "node:fs/promises"; import { Sandbox } from "withruntime"; const bin = "/usr/lib/postgresql/16/bin"; await using sbx = await Sandbox.create({ timeoutSeconds: 1800 }); const slow = { check: true, timeoutMs: 600_000 } as const; await sbx.exec("sudo apt-get update -qq && sudo apt-get install -y -qq postgresql", slow); await sbx.exec("sudo pg_dropcluster --stop 16 main", { check: true }); await sbx.exec(`${bin}/initdb -D /workspace/pgdata -U postgres --auth=trust`, slow); await sbx.spawn(`${bin}/postgres -D /workspace/pgdata -k /tmp -c listen_addresses=127.0.0.1`); await sbx.exec("timeout 60 bash -c 'until pg_isready -q -h 127.0.0.1; do sleep 0.5; done'", { check: true, timeoutMs: 90_000, }); await sbx.files.upload("./schema.sql", "/workspace/schema.sql"); const psql = "psql -h 127.0.0.1 -U postgres -v ON_ERROR_STOP=1"; await sbx.exec(`${psql} -f schema.sql`, { check: true }); const count = await sbx.exec(`${psql} -Atc 'select count(*) from users'`, { check: true }); console.log("users:", count.stdout.trim()); await sbx.exec("pg_dump -h 127.0.0.1 -U postgres -Fc -f /workspace/app.dump postgres", { check: true, }); await writeFile("app.dump", await sbx.files.read("/workspace/app.dump")); ``` ```python check from withruntime import Sandbox BIN = "/usr/lib/postgresql/16/bin" PSQL = "psql -h 127.0.0.1 -U postgres -v ON_ERROR_STOP=1" with Sandbox.create(timeout_seconds=1800) as sbx: sbx.exec("sudo apt-get update -qq && sudo apt-get install -y -qq postgresql", check=True, timeout_ms=600_000) sbx.exec("sudo pg_dropcluster --stop 16 main", check=True) sbx.exec(f"{BIN}/initdb -D /workspace/pgdata -U postgres --auth=trust", check=True) sbx.spawn(f"{BIN}/postgres -D /workspace/pgdata -k /tmp -c listen_addresses=127.0.0.1") sbx.exec("timeout 60 bash -c 'until pg_isready -q -h 127.0.0.1; do sleep 0.5; done'", check=True, timeout_ms=90_000) sbx.files.upload("schema.sql", "/workspace/schema.sql") sbx.exec(f"{PSQL} -f schema.sql", check=True) print(sbx.exec(f"{PSQL} -Atc 'select count(*) from users'", check=True).stdout) sbx.exec("pg_dump -h 127.0.0.1 -U postgres -Fc -f /workspace/app.dump postgres", check=True) sbx.files.download("/workspace/app.dump", "app.dump") ``` - `spawn` keeps the server running after the call returns; a process started with `exec` ends with its command. - `--auth=trust` lets any process in the sandbox connect without a password. Only code you run there, and you through the forward below, can reach `127.0.0.1`: nothing on the internet can connect in to a sandbox. - `-k /tmp` puts the Unix socket where the sandbox user may write it. ## One seeded database per test ```ts check import { Sandbox } from "withruntime"; declare const seeded: Sandbox; // the sandbox above, schema and fixtures loaded const copies = await seeded.fork({ count: 10 }); const results = await Promise.all( copies.map((db, i) => db.exec(`psql -h 127.0.0.1 -U postgres -f tests/case-${i}.sql`, { timeoutMs: 300_000 }), ), ); await Promise.all(copies.map((db) => db.stop())); console.log(results.map((r) => r.exitCode)); ``` A fork copies files, memory and running processes, so Postgres in each copy is the same server mid-flight, not a fresh start and a restore. The source is paused for about a second on a fresh sandbox, longer the more memory it holds. Forks make 1 to 10 copies per call; to start copies later, take a [snapshot](/glossary/sandbox-snapshot) and create sandboxes from it. ## Connect from your laptop or a GUI Forward the port and point `psql`, DBeaver, TablePlus or your app at `localhost`: ```python check import os import psycopg # pip install "psycopg[binary]" from withruntime import Sandbox sbx = Sandbox.connect(os.environ["SANDBOX_ID"]) with sbx.forward_port(5432, local_port=15432) as forward: with psycopg.connect(f"postgresql://postgres@127.0.0.1:{forward.local_port}/postgres") as conn: print(conn.execute("select version()").fetchone()) ``` ```bash no-run runtime sandbox port-forward 15432:5432 psql postgresql://postgres@127.0.0.1:15432/postgres ``` The forward is a WebSocket through Runtime's API, authenticated with your key ([forward ports](/docs/editors#forward-ports)). It works on the free trial. To give a service outside Runtime a connection string instead, a paid account can open a public TCP port ([networking](/docs/networking)). ## Keep the data | You want | Use | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Come back to the same running database | Pause: files, memory and processes are kept (30 days by default on paid accounts, 1 to 365), and the next request wakes it | | Many databases from one prepared state, later | A snapshot, then `sandboxes.create({ snapshot })` | | Data that outlives any sandbox | `initdb` onto a [volume](/docs/javascript#volumes) mounted at `/data`; run `sync` after writes you must keep | | A copy on your own machine | `pg_dump`, then `files.read` as above | A sandbox with a volume attached cannot be snapshotted, and a fork takes a snapshot, so choose per database: fast copies, or a disk that outlives the machine. ## A newer PostgreSQL, or Postgres in every sandbox The PostgreSQL project's apt repository supports Ubuntu 24.04. Its script adds the repository, and `-y` skips its prompt: ```bash no-run sudo apt-get install -y postgresql-common sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y sudo apt-get install -y postgresql-18 # binaries in /usr/lib/postgresql/18/bin ``` To skip the install in every sandbox, build it into a [custom image](/docs/images); building is free, and the trial stores three images free. ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "postgres", recipe: { apt: ["postgresql"], commands: ["pg_dropcluster --stop 16 main"] }, }); ``` ```python check from withruntime import Runtime runtime = Runtime() runtime.images.build(name="postgres", recipe={"apt": ["postgresql"], "commands": ["pg_dropcluster --stop 16 main"]}) ``` A sandbox from the image starts at the `initdb` step of the first sample. For Postgres next to an app and a cache, a Compose file is often simpler; see [Docker Compose integration tests](/integrations/docker-compose). For agents that need a database to try migrations against, see [a coding agent sandbox](/use-cases/coding-agent-sandbox). ## Sources - Ubuntu 24.04 postgresql-16 package, https://packages.ubuntu.com/noble/postgresql-16, read 25 September 2026 - PostgreSQL downloads for Ubuntu, https://www.postgresql.org/download/linux/ubuntu/, read 25 September 2026 - apt.postgresql.org.sh options, https://salsa.debian.org/postgresql/postgresql-common/-/raw/master/pgdg/apt.postgresql.org.sh, read 25 September 2026 Facts on this page were checked on 25 September 2026.