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 freestylebun add freestylenpm install freestyleyarn 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 parsestrueas 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.