Freestyle Docs

Freestyle / Guides

How to Run Hasura in a Sandbox

Run Hasura and Postgres in a VM, then open the Console on a public domain.

Hasura gives you an instant GraphQL API over Postgres, and it ships as a Docker image. So this guide starts from a snapshot that already has Docker installed — build one with How to Run Docker in a Sandbox — then boots a VM, brings up Hasura and its database with docker compose, and opens the Console on a public domain.

Install the SDK

pnpm add freestyle
bun add freestyle
npm install freestyle
yarn add freestyle

Set your API key before calling the API:

export FREESTYLE_API_KEY="your-api-key"

Boot a VM and Start Hasura

Boot a VM from your Docker snapshot — Docker is already running, so you can write a Compose file and bring the stack up right away. dockerSnapshotId is the snapshot id from How to Run Docker in a Sandbox.

The stack has two services: a postgres database and the graphql-engine itself. Two things in the Compose file matter:

  • The database service must be named postgres — Compose uses the service name as its hostname on the default network, and the connection URLs point at @postgres:5432. Rename the service and Hasura can’t resolve its metadata database.
  • YAML-quote the boolean-ish and asterisk values ("true", "*"). Unquoted, YAML parses true as a boolean and * as an alias reference, and Compose rejects the file.

Containers run under the Docker daemon with restart: always, so the stack comes back on its own after a reboot — there is no systemd unit to write.

import { freestyle } from "freestyle";

const { vm, vmId } = await freestyle.vms.create({
  slug: "hasura-server",
  snapshotId: dockerSnapshotId,
  idleTimeoutSeconds: null,
});

await vm.exec("mkdir -p /root/hasura");
await vm.fs.writeTextFile(
  "/root/hasura/docker-compose.yml",
  `services:
  postgres:
    image: postgres:15
    restart: always
    environment:
      POSTGRES_PASSWORD: postgrespassword
    volumes:
      - db_data:/var/lib/postgresql/data
  graphql-engine:
    image: hasura/graphql-engine:v2.46.0
    ports:
      - "8080:8080"
    depends_on:
      - postgres
    restart: always
    environment:
      HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
      PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
      HASURA_GRAPHQL_DEV_MODE: "true"
      HASURA_GRAPHQL_ADMIN_SECRET: a-strong-secret
      HASURA_GRAPHQL_CORS_DOMAIN: "*"
volumes:
  db_data:
`,
);

await vm.exec("cd /root/hasura && docker compose up -d");

Hasura retries the metadata database connection on its own, so there is no healthcheck or wait-for-Postgres dance — just poll the unauthenticated /healthz endpoint until it answers 200 (ready in about nine seconds):

let code = "000";
while (code !== "200") {
  await new Promise((r) => setTimeout(r, 1500));
  code = (
    await vm.exec("curl -s -o /dev/null -w '%{http_code}' localhost:8080/healthz")
  ).stdout.trim();
}

console.log((await vm.exec("curl -s localhost:8080/v1/version")).stdout);
// {"server_type":"ce","version":"v2.46.0"}

Snapshot for Fast Boots

The image pull on first boot is the only slow part, and Hasura bakes no host-specific config — HASURA_GRAPHQL_CORS_DOMAIN is * and it answers on any Host. So once it’s up, snapshot the VM with the stack already running, and every VM you boot from that snapshot comes up serving on 8080 with no pull and no docker compose up:

const { snapshotId } = await vm.snapshot();
// later — boot an already-serving Hasura in seconds:
// const { vm, vmId } = await freestyle.vms.create({ snapshotId });

A Freestyle snapshot captures the running containers (restart: always brings them back on boot) and the db_data volume, so the snapshot is a complete, ready-to-serve Hasura.

Stream the Logs

vm.exec() buffers a command and only returns once it finishes, so it can’t show the running containers’ 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 stack with docker compose logs -f. 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 both services; new lines stream in until you detach.
session.write("cd /root/hasura && docker compose logs -f\n");

// session.detach() drops your handle — the containers keep running.

Open It on a Domain

Hasura publishes port 8080, so route a domain to it. Pick your own unique *.style.dev subdomain; it needs no DNS or verification.

const domain = `hasura-${crypto.randomUUID().slice(0, 8)}.style.dev`;
await freestyle.domains.mappings.create({ domain, vmId, vmPort: 8080 });

// The *.style.dev proxy may briefly serve a "Reloading" warmup page (also 200)
// right after mapping, so poll a couple of times until /healthz returns Hasura's "OK".
let body = "";
for (let i = 0; i < 5 && body !== "OK"; i++) {
  await new Promise((r) => setTimeout(r, 1500));
  body = (await (await fetch(`https://${domain}/healthz`)).text()).trim();
}
console.log(body); // OK

console.log(`https://${domain}/console`);

Open the printed /console URL in a browser and the Hasura Console loads and works over the domain — connect a database, define tables, and explore the API. The /healthz and /v1/version endpoints are unauthenticated, which makes them ideal health checks. The GraphQL endpoint itself lives at /v1/graphql and requires the x-hasura-admin-secret: a-strong-secret header you set in the Compose file.

One edge: a raw scripted GraphQL POST from a non-browser client over the public domain may be challenged by the edge proxy. The browser Console — and any app sending normal browser headers — goes straight through, so this only affects bare scripts. From inside the VM you can always hit localhost:8080/v1/graphql directly.

esc