Freestyle Docs

Freestyle / Guides

How to Run Postgres in a Sandbox

Bake PostgreSQL into a VM snapshot, then boot a VM and run SQL queries.

Install PostgreSQL once into a snapshot, then launch VMs that boot with the database ready to serve.

Install the SDK

pnpm add freestyle@latest
bun add freestyle@latest
npm install freestyle@latest
yarn add freestyle@latest

Set your API key before calling the API:

export FREESTYLE_API_KEY="your-api-key"

Build a snapshot with a running database

The base image is minimal, so install PostgreSQL explicitly with apt-get. Do this once on a builder VM, snapshot it, then throw the builder away.

A Freestyle snapshot is a full memory and disk capture, so it preserves a running service and its in-memory state, not just the installed files. So instead of stopping at “installed”, start the server and wait until it is accepting connections before snapshotting. The snapshot then bakes in a live, ready database, and every VM you create from it boots with Postgres already serving.

The PostgreSQL package ships a systemd unit (postgresql). systemctl start is a one-shot command — it tells systemd to launch the unit and returns immediately — so plain vm.exec is the right tool. systemd then supervises the daemon and restarts it if it ever crashes.

import { Freestyle } from "freestyle";

const freestyle = new Freestyle();

const { vm: builder } = await freestyle.vms.create({
  // Required: a VM reaches nothing it has not been allowed to.
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] }, slug: "postgres-builder" });

await builder.exec(
  "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq postgresql",
);

// Start the database and wait until it is accepting connections.
await builder.exec("systemctl start postgresql");

let ready = false;
while (!ready) {
  await new Promise((r) => setTimeout(r, 1000));
  const probe = await builder.exec('su postgres -c "pg_isready" || true');
  ready = probe.stdout?.includes("accepting connections") ?? false;
}

// Capture the running, ready database into the snapshot.
const { snapshotId } = await builder.snapshot();
await builder.delete();

Query the database

Create a fresh VM from the snapshot. Because the snapshot captured a running, ready server, the new VM comes up with Postgres already active and accepting connections — there is nothing to start and nothing to wait for. Just connect and query.

const { vm } = await freestyle.vms.create({
  // Required: a VM reaches nothing it has not been allowed to.
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
  slug: "postgres-sandbox",
  snapshotId,
  idleTimeoutSeconds: null,
});

The postgres OS user owns the cluster, so run client commands as that user with su postgres. Each query is a one-shot command, so use synchronous vm.exec:

await vm.exec(
  `su postgres -c "psql -c 'CREATE TABLE fruits (id serial PRIMARY KEY, name text);'"`,
);

await vm.exec(
  `su postgres -c "psql -c \\"INSERT INTO fruits (name) VALUES ('apple'), ('banana');\\""`,
);

const rows = await vm.exec(
  `su postgres -c "psql -c 'SELECT id, name FROM fruits ORDER BY id;'"`,
);
console.log(rows.stdout);
//  id |  name
// ----+--------
//   1 | apple
//   2 | banana
// (2 rows)

Stream the Server’s Logs

vm.exec() buffers a command and only returns once it finishes, so it can’t show a long-running service’s output as it happens. To watch the logs live, open a PTY on the VM — a real terminal streamed over a WebSocket (server-side only, Node 22+) — and follow the unit’s journal. onData delivers the bytes as they arrive:

const session = await vm.pty.open({
  cols: 120,
  rows: 30,
  onData: (bytes) => process.stdout.write(bytes), // live log lines
});

// Follow the service; new lines stream in until you detach.
session.write("journalctl -u postgresql -f\n");

// session.detach() drops your handle — the service keeps running in the VM.
esc