Skip to content

Ephemeral runners

Every runner Runaway spawns is a one-shot container. It registers with GitHub, picks up exactly one job, runs it, and then exits. On exit the container is destroyed (AutoRemove: true, the API equivalent of docker run --rm), and a fresh container takes its place. Nothing carries over from one job to the next.

The naive way to keep a self-hosted runner alive is restart: unless-stopped in a Compose file. It works for an afternoon, then rots: Docker restarts the same container after each job, so its writeable layer accumulates state run after run. Leftover checkout dirs, half-installed toolchains, growing logs, a filesystem that drifts further from clean every time. Eventually a job fails for a reason that has nothing to do with the job — it inherited the mess from the last one.

AutoRemove: true is the only correct way to get true ephemeral behaviour with Docker. The container is gone the instant the job ends; there is no writeable layer to accumulate anything. Every job starts from the same known-clean image.

You never spawn or destroy a runner by hand. When a runner exits, the reconciliation loop notices the gap between the pool you asked for and the pool that exists, and spawns a replacement. That single path owns every container’s birth and death, keeping the “one job, then gone” guarantee airtight across crashes and restarts.

The container is disposable; your caches are not. Each scale set mounts a persistent /cache volume that outlives every runner generation. Point your package managers and browser binaries at it and warm caches survive across runs without GitHub’s Actions Cache round-trip.