Lambdas can run for 8 hours using MicroVMs. This runs GitHub Action jobs on top of it.
Install the AWS CLI, GitHub CLI, jq, Docker, and Node.js 24. Authenticate both
CLIs, create a classic GitHub PAT with the repo scope, then run:
export AWS_REGION=us-east-1
export GITHUB_REPOSITORY=OWNER/PRIVATE_REPOSITORY
scripts/setup-quickstart.shThe script uses your existing local AWS credentials to create the AWS resources, runner image, roles, and a dedicated IAM user. It rotates that user's static access key directly into GitHub Actions secrets and prompts for the classic PAT. It does not use an infrastructure framework or write the AWS secret access key to disk.
The stored IAM user is restricted to image building and runner lifecycle operations. It cannot create or modify IAM identities, roles, policies, OIDC providers, buckets, or log groups. Use it only with private repositories and trusted workflow changes. See advanced credentials to replace both long-lived credentials with GitHub OIDC and a GitHub App.
To preview cleanup of the Quickstart resources:
export GITHUB_REPOSITORY=OWNER/PRIVATE_REPOSITORY
scripts/teardown-quickstart.shRe-run with --yes to delete the generated repository secrets/variables, IAM
user, IAM roles, MicroVM image, artifact bucket, and CloudWatch log groups.
Copy examples/basic.yml into the private repository's
.github/workflows/ directory.
[!TIP] The Quickstart script configures every variable and secret referenced by this workflow.
For a job that runs inside a Node 24 container and talks to a Redis service container, see examples/container-services.yml.
name: Lambda MicroVM runner
on:
workflow_dispatch:
permissions:
contents: read
jobs:
start-runner:
runs-on: ubuntu-latest
outputs:
label: ${{ steps.start.outputs.label }}
microvm-id: ${{ steps.start.outputs.microvm-id }}
region: ${{ steps.start.outputs.region }}
steps:
- uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.MICROVM_AWS_REGION }}
- uses: neebs12/lambda-microvm-github-runner@v1
id: start
with:
mode: start
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
image-id: ${{ vars.MICROVM_RUNNER_IMAGE_ARN }}
image-version: ${{ vars.MICROVM_RUNNER_IMAGE_VERSION }}
execution-role-arn: ${{ vars.MICROVM_EXECUTION_ROLE_ARN }}
cloudwatch-log-group: ${{ vars.MICROVM_RUNTIME_LOG_GROUP }}
max-lifetime-seconds: "3600"
job:
needs: start-runner
runs-on: ${{ needs.start-runner.outputs.label }}
steps:
- uses: actions/checkout@v6
- run: uname -a
- run: docker info
- run: docker buildx version
- run: docker compose version
stop-runner:
if: ${{ always() }}
needs: [start-runner, job]
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ needs.start-runner.outputs.region }}
- uses: neebs12/lambda-microvm-github-runner@v1
with:
mode: stop
microvm-id: ${{ needs.start-runner.outputs.microvm-id }}The start job emits a unique label for one target job. The runner is JIT-only and single-use. Its supervisor self-terminates after that job; the explicit stop job and platform maximum duration are independent cleanup backstops.
By default, every job gets a new MicroVM. To opt into warm reuse, give start a
human-readable server pool name and the Quickstart-created
MICROVM_WARM_STATE_TABLE. start creates or resumes a pool member, and stop
suspends it for another job instead of terminating it.
The MicroVM is reused; the GitHub runner registration is not. Every target job still gets a fresh, single-use JIT runner.
One member of the docker-builds pool looks like this:
RUN 1
|
start
|
v
+-----------------------+
| MicroVM A |
| fresh runner #1 |
| build populates cache |
+-----------------------+
|
stop
|
v
SUSPEND
memory + disk preserved
|
v
RUN 2
|
start + resume
|
v
+-----------------------+
| same MicroVM A |
| fresh runner #2 |
| build can reuse cache |
+-----------------------+
|
stop
|
v
SUSPEND
This is useful when repeated, compatible workloads can reuse local state:
- Docker can reuse its native image and build-layer cache without exporting a cache archive between jobs.
- Package managers and build tools can reuse downloads, intermediate outputs, and installed toolchains left on the machine.
- A named pool can serve multiple workflow runs. Each MicroVM is leased to only one job at a time.
- Suspended members retain memory and disk state without remaining active between jobs. Lambda's maximum lifetime still provides a final cleanup boundary.
There are only two warm-specific additions to the normal workflow: set the pool
name and state table on start, then pass the opaque server output to stop.
The output is a lease handle, not the human-readable pool name.
jobs:
start-runner:
runs-on: ubuntu-latest
outputs:
label: ${{ steps.start.outputs.label }}
server: ${{ steps.start.outputs.server }}
warm-hit: ${{ steps.start.outputs.warm-hit }}
region: ${{ steps.start.outputs.region }}
steps:
- uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.MICROVM_AWS_REGION }}
- uses: neebs12/lambda-microvm-github-runner@v1
id: start
with:
mode: start
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
image-id: ${{ vars.MICROVM_RUNNER_IMAGE_ARN }}
image-version: ${{ vars.MICROVM_RUNNER_IMAGE_VERSION }}
execution-role-arn: ${{ vars.MICROVM_EXECUTION_ROLE_ARN }}
# Human-readable pool name
server: docker-builds
state-table: ${{ vars.MICROVM_WARM_STATE_TABLE }}
build:
needs: start-runner
runs-on: ${{ needs.start-runner.outputs.label }}
steps:
- uses: actions/checkout@v6
- run: docker build --tag app:ci .
stop-runner:
if: ${{ always() && needs.start-runner.outputs.server != '' }}
needs: [start-runner, build]
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ needs.start-runner.outputs.region }}
- uses: neebs12/lambda-microvm-github-runner@v1
with:
mode: stop
# Opaque lease handle returned by start
server: ${{ needs.start-runner.outputs.server }}warm-hit reports whether start resumed an existing member. An available
member always wins. When every member is busy, server-capacity optionally
limits whether that request may create another member; omitting it leaves pool
growth unbounded by the Action.
[!WARNING] A warm cache is not an isolation boundary. Jobs can read or alter state left by other jobs, so share a pool only between equally trusted workflows in the same private repository. Warm caches are temporary, and cache reuse does not guarantee that every workload becomes faster.
See the copy-ready warm-cache workflow and the warm-cache design and testing guide for lifecycle, security, failure-recovery, and capacity details.
The Action implements and tests:
- strict mode-dependent Action input parsing;
- collision-resistant runner identity and deterministic launch client tokens;
- masked gzip/base64 JIT payloads with a 4,096-byte limit;
- bounded full-jitter retry and quota-aware polling;
- repository JIT creation and exact-runner readiness polling;
- idempotent Lambda MicroVM launch, readiness, cleanup, and termination;
- typed GitHub and AWS adapters with mocked-boundary integration tests.
The production AL2023 runner image is implemented and validated locally and
through the AWS image build hooks. Docker prefers overlay2, falls back to the
copy-on-write fuse-overlayfs, and retains vfs as the final compatibility
fallback. The complete private-repository workflow is validated for success, job
failure, cancellation, startup timeout, service containers, and the
maximum-duration backstop.
Node.js 24 is required.
npm ci
npm run checkdist/index.js is committed because GitHub Actions executes the bundled
artifact directly.
Version 1 is ARM64, JIT-only, repository-scoped, and intended for private repositories with trusted workflow changes. It has no webhook, queue, dispatcher, shell ingress, persistent GitHub runner registration, or boot-time package installation. Warm MicroVM reuse is experimental and opt-in.
Detailed guides:
- installation
- advanced credentials
- security model
- operations and quotas
- testing and release gates
- warm-cache implementation and testing plan
- runner image
Benchmark harnesses, raw measurements, and research notes are maintained separately from the Action's product code. Public articles and reproducible summaries will be linked here as they are published.
This project is a small, purpose-built variation on existing runner and Lambda MicroVM work:
- mkdev-me/terraform-aws-github-runner-lambda-microvms for the Terraform-centric Lambda MicroVM runner approach using webhooks, GitHub Apps, and dispatcher-style orchestration.
- machulav/ec2-github-runner for the start/stop Action shape and general ephemeral runner lifecycle. This Action follows that simple workflow model, but uses Lambda MicroVMs instead of EC2 as the runtime.
- Some notes on Lambda MicroVMs by Aidan Steele for practical observations on Lambda MicroVM snapshots, lifecycle hooks, and Docker behavior inside MicroVMs.
The implementation also leans on the primary platform documentation:
- AWS Lambda MicroVMs and MicroVM images for the image-build, snapshot, hook, and launch model.
- GitHub self-hosted runner REST API and secure use of just-in-time runners for repository-scoped JIT runner registration and lifecycle behavior.
- GitHub Actions docs for running jobs in a container and service containers for the container and Redis service example.
MIT

