---
title: "How to Run Postgres in a Sandbox"
description: "Build a snapshot with PostgreSQL already running, then boot a VM and run SQL queries against the live database."
url: "/docs/guides/run-postgres-in-a-sandbox"
---

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


## Install the SDK

```bash pnpm
pnpm add freestyle
```

```bash bun
bun add freestyle
```

```bash npm
npm install freestyle
```

```bash yarn
yarn add freestyle
```

Set your API key before calling the API:

```bash
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.

```ts
import { freestyle } from "freestyle";

const { vm: builder } = await freestyle.vms.create();

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.

```ts
const { vm } = await freestyle.vms.create({
  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`:

```ts
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](https://www.freestyle.sh/docs/vms/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:

```ts
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.
```
