DeskRun: Unlocking Local Compute for GitHub Actions.
deskrun is a CLI tool for running GitHub Actions locally using kind (Kubernetes in Docker) clusters. It provides easy management of local GitHub Actions runners with optimized configurations based on lessons learned from production deployments.
- Simple CLI Interface: Easy-to-use commands for managing runner installations
- Multiple Container Modes: Support for standard, privileged, and Docker-in-Docker modes
- Persistent Caching: Host path volume caching for Docker daemon and other paths
- Kind Cluster Management: Automatic cluster creation and management
- Flexible Authentication: Support for GitHub Personal Access Tokens (PAT) and GitHub Apps
- Docker
The official way to install deskrun is via Nix flakes:
# Run directly without installing
nix run github:rkoster/deskrun -- --help
# Install to your profile
nix profile install github:rkoster/deskrun
# Or add to your NixOS/home-manager configuration
{
inputs.deskrun.url = "github:rkoster/deskrun";
# ...
}If you prefer to build from source:
git clone https://github.com/rkoster/deskrun.git
cd deskrun
# With Nix (recommended)
nix build
# Or with Go (requires Go 1.24 or later)
make build
sudo make installUnlike traditional self-hosted runners that use labels (e.g., runs-on: [self-hosted]), ARC ephemeral runners use scale set names for job routing. This is GitHub's officially supported method for ARC.
To route jobs to deskrun runners, use the scale set name in your workflow:
jobs:
build:
runs-on: my-runner # Use the runner's scale set name
steps:
- uses: actions/checkout@v4
- run: ./build.shWhy not labels? GitHub explicitly states that ARC runners cannot use additional labels for targeting. The scale set name is used as a "single label" for the runs-on target. This is simpler and more explicit than traditional label-based routing.
Add a new GitHub Actions runner installation to the kind cluster:
# Standard runner
deskrun add my-runner \
--repository https://github.com/owner/repo \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxx
# Privileged runner with Docker cache
deskrun add docker-runner \
--repository https://github.com/owner/repo \
--mode cached-privileged-kubernetes \
--cache /var/lib/docker \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxx
# Multiple instances for cache isolation (creates runner-1, runner-2, runner-3)
# Each instance automatically gets min=1, max=1 for proper cache isolation
deskrun add multi-runner \
--repository https://github.com/owner/repo \
--mode cached-privileged-kubernetes \
--cache /var/lib/docker \
--instances 3 \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxx
# Docker-in-Docker runner
deskrun add dind-runner \
--repository https://github.com/owner/repo \
--mode dind \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxxList all configured runner installations:
deskrun listCheck the status of runner installations:
# All runners
deskrun status
# Specific runner
deskrun status my-runnerRemove a runner installation:
deskrun remove my-runner- Use case: Simple repositories without nested containerization needs
- Configuration:
--mode kubernetes - Benefits: Lightweight, reliable, good for basic CI/CD
- Use case: Repositories requiring systemd, cgroup access, or nested Docker
- Configuration:
--mode cached-privileged-kubernetes - Capabilities: SYS_ADMIN, NET_ADMIN, SYS_PTRACE, SYS_CHROOT, and more
- Features:
- Runs as root (UID 0)
- Privileged container
- Full system access
- SYSTEMD_IGNORE_CHROOT=1 environment variable
- Use case: Full Docker access via TCP socket
- Configuration:
--mode dind - Benefits: Clean Docker environment with isolated daemon
For performance-critical paths like /var/lib/docker or /root/.cache, you can specify cache paths that will be mounted using hostPath volumes:
deskrun add docker-runner \
--repository https://github.com/owner/repo \
--cache /var/lib/docker \
--cache /root/.cache \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxxYou can also specify custom source and target paths using the src:target notation:
deskrun add custom-cache-runner \
--repository https://github.com/owner/repo \
--cache /host/cache/npm:/root/.npm \
--cache /host/cache/docker:/var/lib/docker \
--cache /root/.cache \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxxCache path formats:
- Target only:
--cache /target/path- Auto-generates host path - Source and target:
--cache /host/path:/container/path- Use custom host path
Cache paths are automatically partitioned per installation when auto-generated:
/tmp/github-runner-cache/{installation-name}/cache-{index}
When using custom host paths with src:target notation, the specified host path is used directly.
For better cache isolation and deterministic cache affinity, you can create multiple separate runner scale set instances:
deskrun add my-runner \
--repository https://github.com/owner/repo \
--mode cached-privileged-kubernetes \
--cache /var/lib/docker \
--instances 3 \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxxThis creates 3 separate AutoscalingRunnerSets:
my-runner-1my-runner-2my-runner-3
Each instance:
- Has dedicated cache partitions (no coordination overhead)
- Runs exactly 1 runner (min=1, max=1) for deterministic behavior
- Provides deterministic cache behavior
- Can be targeted independently by workflows
Use modulo-based routing for deterministic distribution:
jobs:
build:
runs-on: my-runner-${{ github.event.issue.number % 3 + 1 }}
steps:
- run: echo "Running on deterministic instance"Benefits:
- Simplified cache management (no locking required)
- Better cache isolation and predictable performance
- Issue-based cache affinity for related workflows
- Improved cache hit rates for follow-up work
Create a PAT with repo and workflow scopes:
deskrun add my-runner \
--repository https://github.com/owner/repo \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxxCreate a GitHub App and use its private key:
deskrun add my-runner \
--repository https://github.com/owner/repo \
--auth-type github-app \
--auth-value "$(cat private-key.pem)"Configuration is stored in ~/.deskrun/config.json:
{
"cluster_name": "deskrun",
"installations": {
"my-runner": {
"Name": "my-runner",
"Repository": "https://github.com/owner/repo",
"ContainerMode": "kubernetes",
"MinRunners": 1,
"MaxRunners": 5,
"CachePaths": [],
"AuthType": "pat",
"AuthValue": "ghp_xxxxxxxxxxxxx"
}
}
}deskrun uses the following components:
- kind: Creates a local Kubernetes cluster
- Actions Runner Controller (ARC): Manages GitHub Actions runners in Kubernetes
- AutoscalingRunnerSet: Kubernetes custom resource for runner scale sets
The tool automatically:
- Creates a kind cluster if it doesn't exist
- Installs the ARC controller and CRDs (Custom Resource Definitions) on first runner installation using Helm
- Deploys each runner scale set using Helm with optimized configurations
- Manages authentication via Helm chart values
Note: The first time you add a runner, deskrun will automatically install the GitHub Actions Runner Controller using Helm. This may take a minute or two. Each runner is then deployed as a separate Helm release.
If jobs remain queued:
- Verify runner is online:
deskrun status my-runner - Check pod status:
kubectl get pods -n arc-systems - Check logs:
kubectl logs -n arc-systems -l app=my-runner - Verify you're using scale set name in workflow:
runs-on: my-runnernotruns-on: [self-hosted]
# Check cluster status
deskrun cluster status
# Recreate cluster if needed
deskrun cluster delete
deskrun cluster createFor operations requiring elevated permissions (Docker, systemd), use privileged mode:
deskrun add my-runner \
--mode cached-privileged-kubernetes \
--repository https://github.com/owner/repo \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxxCache paths are mounted using hostPath volumes. Recommended cache paths:
/var/lib/dockerfor Docker layer caching/root/.cachefor application caches- Custom paths like
/tmp/build-cache
You can use custom host paths for better control:
# Use custom host paths
deskrun add my-runner \
--cache /host/persistent/docker:/var/lib/docker \
--cache /host/persistent/npm:/root/.npm \
--repository https://github.com/owner/repo \
--auth-type pat \
--auth-value ghp_xxxxxxxxxxxxx# Remove specific runner
deskrun remove my-runner
# Clean cache directories
rm -rf /tmp/github-runner-cache/my-runner
# Reset everything
deskrun cluster delete
rm -rf ~/.deskrunmake buildmake testmake lintmake fmtLicensed under the Apache License, Version 2.0. See LICENSE for details.
- Scale Set Name Routing: Must use scale set names in workflows, not labels like
[self-hosted] - Single Cluster: Manages one kind cluster at a time
- Local Development: Designed for local development, not production deployments
Contributions are welcome! Please open an issue or pull request at https://github.com/rkoster/deskrun.