Deploy ephemeral, rootless GitHub Actions runners using Podman with automated token management and resource allocation. Runners execute in isolated containers with tmpfs mounts for security and state isolation. The deployment script builds a local image and runs one or more rootless Podman containers that register as ephemeral self-hosted runners with GitHub.
- Podman: For rootless container execution.
- GitHub CLI (
gh): To fetch runner registration tokens.- You must authenticate first:
gh auth login
- You must authenticate first:
jq: To parse the token JSON response from the GitHub API.
-
Copy the example environment file:
cp .env.example .env
-
(Optional) Set your default target repository in
.env:GITHUB_REPO=your-user/your-repo
Note: Real runner tokens will be written to
.envautomatically when the deployment script runs. This file is ignored by git. -
(Optional) Set a base name for your runner(s) in
.envusingRUNNER_NAME:RUNNER_NAME=my-ci-runner
When set, the script derives container and runner names from this value (see Name resolution below). If unset, reasonable defaults are used (e.g.
gh-runner,gh-vps).
The deploy_runner.sh script automates fetching tokens via the GitHub API, building the Podman image, and running the container(s).
Deploy a single runner for a specific repository:
./deploy_runner.sh -r owner/repoIf you configured GITHUB_REPO in your .env, you can omit the -r flag:
./deploy_runner.shYou can deploy multiple runners for the same repository simultaneously by specifying the -n flag. The script will automatically calculate CPU and RAM limits to distribute your host's resources evenly among instances.
# Deploy 4 runners
./deploy_runner.sh -n 4 -r owner/repoThe script supports a RUNNER_NAME value in .env to make naming predictable when deploying multiple instances. Behavior:
- Set → single: multi: -1, -2, …
- Unset → single: gh-runner multi: gh-runner-1, gh-runner-2, …
The per-runner RUNNER_NAME value also mirrors the container name and is passed to the runner process (overriding any value in .env), ensuring each instance registers with a unique name when multiple runners are created.
The script supports a time-to-live for runners using the -t TIME flag (for example 5m, 12h, 2d), which configures how long a launched runner should remain active. This enables looping JIT-style deployments where the script can create ephemeral runners that unregister and exit after the configured TTL.
Under the hood the deployment includes improved JIT fetch logic with exponential backoff and enhanced logging to make token retrieval and transient network errors more resilient and easier to diagnose.
- Token Retrieval: The script uses
gh apito fetch ephemeral runner registration tokens. The token handling and JIT fetch logic include retries with exponential backoff and clearer logging on failure. - Container Build & Runner Versioning: A custom Ubuntu-based image is built containing Node.js, Python, Rust, and other CI tools (defined in
Containerfile). The script checks the GitHub Actions runner releases and will attempt to update theARG RUNNER_VERSIONinContainerfileto the latest available release before building. The image is taggedlocalhost/<base>where<base>is derived fromRUNNER_NAMEwhen present, otherwisegh-runner. - Rootless Execution:
podmancreates the runner instances in rootless mode with limited privileges. - Ephemeral Configuration: When a container starts,
entrypoint.shinitializes the runner config using--ephemeral(one-job lifecycle) if configured, ensuring it only runs one job before unregistering and exiting. The image itself is immutable; the container's rootfs is a throwaway overlay layer (image = lower, container layer = upper) discarded when the container is recreated (--replaceon deploy, or TTL exit), with/tmpon tmpfs. This lets workflow stepssudo apt-get installbuild deps at runtime while keeping the base image clean. For strict per-job hygiene, recreate the container per job rather than relying on the in-process restart loop.
- The script will update
RUNNER_URLin your.envto point to the target repository being deployed to. - Image tagging uses
localhost/<name>so local images are easy to identify and do not collide with remote registries. - When deploying many runners, ensure your host has enough CPU and RAM; the script will attempt to split resources across instances but cannot exceed the physical limits of the host.
For full details, check deploy_runner.sh, Containerfile, and entrypoint.sh in the repository.