Skip to main content
One Dedalus Machine per open PR. The PR’s branch is checked out, dependencies installed, services running. When nobody touches the preview URL for 5 minutes the VM sleeps. When the next reviewer opens the link, it wakes in under a second with the database, seed data, and log history intact.
npm install dedalus-labs

1. On PR opened: provision

import Dedalus from "dedalus-labs";
const dedalus = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY! });

async function provisionPreview(prNumber: number, branch: string) {
  const m = await dedalus.machines.create({
    vcpu: 2,
    memory_mib: 4096,
    storage_gib: 20,
  });

  await runAndWait(m.machine_id, ["/bin/bash", "-c", `
    set -e
    apt-get update && apt-get install -y git nodejs postgresql
    git clone --depth 1 -b ${branch} https://github.com/your-org/your-app /root/app
    cd /root/app && npm install
    pg_ctlcluster 16 main start && createdb app && npm run migrate && npm run seed
    nohup npm run dev -- --host 0.0.0.0 --port 3000 >/var/log/app.log 2>&1 &
  `]);

  const port = await dedalus.machines.previews.create(m.machine_id, {
    port: 3000,
    protocol: "https",
    visibility: "org",   // or "public" for external reviewers
  });

  return { machineId: m.machine_id, url: port.url };
}

2. On webhook hit (or middleware): wake

The simplest pattern is a small router that proxies https://pr-123.previews.your-app.com to the machine’s preview URL. Before forwarding, wake the machine.
import http from "node:http";

http.createServer(async (req, res) => {
  const prNumber = parseInt(req.headers["x-pr-number"] as string);
  const machineId = await lookupMachine(prNumber);   // your DB

  await dedalus.machines.wake(machineId);
  // ...then proxy req → the preview URL stored at provision time.
}).listen(8080);
wake is idempotent and returns once the machine is running. If it was already running, it returns immediately. If it was sleeping, it returns when the snapshot has restored — typically under a second.

3. On PR merged or closed: destroy

async function teardownPreview(prNumber: number) {
  const machineId = await lookupMachine(prNumber);
  await dedalus.machines.delete(machineId);
}

Why a microVM beats a container here

  • Postgres, Redis, real systemd, anything else that doesn’t survive container restart, just runs. Migrations + seed data persist across sleep/wake.
  • Sleep means zero compute cost while the PR is dormant. A 3-week-old PR costs only the storage GiB-month — not 24/7 uptime.
  • Wake under a second. Reviewers don’t notice they’re hitting a slept VM.
  • Idle auto-sleep is built in (autosleep, default "5m"). If your wake-on-traffic proxy forgets to put the VM back to sleep, the platform does it for you after five idle minutes.

Notes

  • Set visibility: "public" if external collaborators need to reach the preview without a Dedalus account. Use "org" to gate it on org membership, or "private" for the creator only.
  • Wake latency depends on storage size. A 20 GiB machine wakes faster than a 200 GiB one.
  • A wake-on-cold-start race exists if many requests hit a sleeping VM simultaneously. The first wake call returns once the VM is up; subsequent calls coalesce. Your proxy should serialize per-machine.
Last modified on May 12, 2026