> ## 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.

# Ephemeral CI Runners

> GitHub Actions runners that sleep between jobs with toolchain caches hot on disk.

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.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
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.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
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.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
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();
    await dedalus.machines.wake(id);   // 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 (`autosleep`, default `"5m"`).

## 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](/cookbook/cron-on-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 (`autosleep: "30m"`) for spiky CI patterns.
