Install PostgreSQL once into a snapshot, then launch VMs that boot with the database ready to serve.
Install the SDK
pnpm add freestyle@latestbun add freestyle@latestnpm install freestyle@latestyarn 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.