Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dedaluslabs.ai/llms.txt

Use this file to discover all available pages before exploring further.

A pool of self-hosted GitHub Actions runners, each backed by a Dedalus Machine. The first job warms ~/.cargo, ~/.rustup, ~/.npm, ~/.cache/sccache. Every job after that wakes the same machine and reuses everything. You pay only for the seconds a runner is actually running a job.
npm install dedalus-labs

1. Provision a runner pool

Run this once per runner you want in the pool. Each machine registers itself as an Actions runner on first boot.
import Dedalus from "dedalus-labs";
const dedalus = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY! });

async function provisionRunner(label: string, registrationToken: string) {
  const m = await dedalus.machines.create({ vcpu: 4, memory_mib: 8192, storage_gib: 40 });

  await runAndWait(m.machine_id, ["/bin/bash", "-c", `
    set -e
    apt-get update && apt-get install -y curl git build-essential
    useradd -m runner && cd /home/runner
    curl -L https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64.tar.gz | tar xz
    sudo -u runner ./config.sh --unattended --replace \
      --url https://github.com/your-org \
      --token ${registrationToken} \
      --labels dedalus,${label}
    cat >/etc/systemd/system/gh-runner.service <<EOF
    [Service]
    User=runner
    WorkingDirectory=/home/runner
    ExecStart=/home/runner/run.sh
    Restart=always
    [Install]
    WantedBy=multi-user.target
    EOF
    systemctl enable --now gh-runner
  `], 600_000);

  return m.machine_id;
}

2. Wake on workflow_job webhook, sleep when idle

GitHub fires a workflow_job webhook with action queued when a job is dispatched. Wake the matching machine; let auto-sleep handle the rest.
import http from "node:http";

http.createServer(async (req, res) => {
  const body = await readJson(req);
  if (body.action === "queued" && body.workflow_job.labels.includes("dedalus")) {
    const id = await pickFreeMachine();
    const m = await dedalus.machines.retrieve(id);
    await dedalus.machines.wake(id, { "If-Match": m.revision });   // sub-second; runner auto-picks up the job
  }
  res.end();
}).listen(8080);
That’s the entire control loop. The runner’s gh-runner.service is on by default; once the VM wakes, systemd starts the runner process and it polls the GitHub queue. After the job finishes the runner goes idle. Five minutes later the platform auto-sleeps the VM (idle_sleep_after_seconds, default 300s).

Why this beats GitHub-hosted runners

  • Hot toolchain caches. ~/.cargo (3 GB of crates), ~/.rustup, ~/.cache/sccache, ~/.npm, node_modules for monorepo workspaces — all stay on disk between jobs. GitHub-hosted runners cold-pull every job. A Rust job that takes 12 minutes cold takes 90 seconds warm.
  • Pay only for awake-seconds. A pool of 10 runners that handle a 4-hour-per-day workload pays for ~40 vCPU-hours, not 240.
  • 4 vCPU / 8 GiB / 40 GiB out of the box; provision bigger if you need it. Standard GitHub runners cap at 2 vCPU / 7 GiB.
  • Real Linux, your kernel — install kernel modules, run Docker-in-VM with full nesting, run privileged containers, whatever your build needs.

Notes

  • Pre-warming. If you want zero cold-start on the first job of the day, run a dummy wake on cron a few minutes before peak. (See Cron on a Sleeping VM.)
  • Pool size. Provision N machines; a queued job wakes the first idle one. Concurrency above N queues — your webhook handler should fall back to creating a new machine if the pool is saturated.
  • Caches survive sleep and wake. They don’t survive delete. If you provision a new machine you lose its cache; budget for that on first build.
  • Auto-sleep timing. Default 300s of idle is fine for back-to-back PRs. Push it higher (idle_sleep_after_seconds: 1800) for spiky CI patterns.
Last modified on May 2, 2026