Raise a CI runner host
$ grim server:runner:setup 203.0.113.10 --org=acme --app-id=123456 --app-key=~/keys/acme-ci.pem
Hosted CI minutes run out, or the builds need a Docker cache that survives from one job to the next. A VPS of your own solves both. grim server:runner:setup makes a bare machine into a host for GitHub Actions runners that belong to your organization.
The runners are ephemeral. Each one registers for a single job, runs it, and registers again as new, so no job inherits the leftovers of the one before. No personal token is stored on the machine. The runners identify themselves through a GitHub App, and its private key is the only secret on the box.
Usage
$ grim server:runner:setup 203.0.113.10 --org=acme --app-id=123456 --app-key=~/keys/acme-ci.pem
$ grim server:runner:setup 203.0.113.10 --org=acme --app-id=123456 --app-key=~/keys/acme-ci.pem --replicas=5
$ grim server:runner:setup 203.0.113.10 --user=ubuntu --alias=ci2 --org=acme --app-id=123456 --app-key=~/keys/acme-ci.pemThe first line gives you three runners under the alias ci. The second is the same command run again to scale to five. The third is a cloud image that does not let root in. Run it from anywhere; --org, --app-id and --app-key are required on every run, and the key file is checked on your machine before any connection is made.
Arguments and options
Provision a VPS as a self-hosted GitHub Actions runner host (ephemeral, GitHub App JIT)
Usage
grim server:runner:setup [--alias ALIAS] [--user USER] [--org ORG] [--app-id APP-ID] [--app-key APP-KEY] [--replicas REPLICAS] [--] <host>Arguments
| Argument | Description |
|---|---|
host |
Server IP address or hostname (required) |
Options
| Option | Description |
|---|---|
--alias=ALIAS |
Short alias for the server (default: ci) |
--user=USER |
SSH user for initial connection (default: root) |
--org=ORG |
GitHub organization the runners register to |
--app-id=APP-ID |
GitHub App id used to mint runner JIT configs |
--app-key=APP-KEY |
Local path to the GitHub App private key (.pem), uploaded once |
--replicas=REPLICAS |
Number of runner replicas (default: 3) |
Before you run it
The command does not create the GitHub App. You need one that is installed on the organization and allowed to manage its self-hosted runners, its numeric id, and a private key for it downloaded as a .pem file. You also need SSH access to the machine as root or as a user with sudo.
What it actually does
Each step is printed with its result, and the first failure stops the run.
- Checks SSH as
--user, then reads which OS the machine runs. - Installs Docker where it is missing.
- Creates the
runneruser, in thedockergroup, with your own public key from~/.ssh/id_ed25519.pubor~/.ssh/id_rsa.pub. This account is for you and forgrim server:runner:status, not for the jobs. - Fills
/opt/grim-runner. The App key asgrim-runner.pem, readable by root and the runner containers only, aregister.shthat every replica starts with, and adocker-compose.ymlwith one service per replica,runner-1torunner-N. - Creates the
grim-ci-cachevolume, pulls the officialactions-runnerimage and starts the replicas. Removing replicas on a scale-down is part of the same step. - Installs a weekly refresh in
/etc/cron.d/grim-runner-update. Every Monday at 04:00 it pulls the runner image and recreates the replicas, logging to/var/log/grim-runner-update.log. - Counts the running containers. Fewer than
--replicasfails the run withOnly 1 of 3 runner containers are runningand the command to read their logs. - Asks GitHub how many runners are online, through the
ghCLI on your machine. This step only warns when it cannot. - Remembers the server in your
~/.grim/servers.jsonwith the roleci-runner, the organization, the App id and the replica count.
The summary ends with the labels the runners carry: self-hosted, linux and grim.
How a job reaches the box
A workflow asks for the runners by label.
jobs:
test:
runs-on: [self-hosted, linux, grim]The replicas are named grim-runner-1 to grim-runner-N. They talk to the Docker daemon of the host, so a job's container: and services: run as containers on the VPS next to the runner, not inside it. The grim-ci-cache volume is shared by all replicas and survives restarts; a job that runs in a container can mount it by name.
Pitfalls
Running containers do not prove a registration
The containers start and stay up even when GitHub refuses them, because register.sh retries every 30 seconds for ever. Step 7 is green in that case. Look at the GitHub count in the summary, and when it is zero, read the logs on the box: GitHub App is not installed on org acme or a line starting generate-jitconfig HTTP points at the App, not at the server.
The GitHub check needs a scope gh does not have by default
gh api failed (needs gh auth refresh -h github.com -s admin:org) is a warning about your own machine. The runners are not affected. Run the command it names and the check works next time.
Scaling still needs all three App flags
--replicas=5 on its own stops with --org is required. Repeat the full command with the new number. Every step checks what is already there, so nothing is reinstalled.
Jobs can do anything Docker can
A job on these runners has the host's Docker socket, which is root on that machine in practice. Keep the box for CI only, never on a server that also runs an app, and do not let workflows from forks of public repositories onto it.
After it finishes
Check the host, then point a workflow at it.
$ grim server:runner:status cigrim server:runner:status shows the containers, the disk and the runners as GitHub sees them.
Related spells
grim server:runner:status
Check the CI runners
Show the runner containers, cache volume, disk use and GitHub registration of a CI runner host, and fail when runners are missing.
grim server:setup
Raise a production server
Turn a bare Ubuntu or Debian VPS into a host for deployed projects, with Docker, MySQL, nginx, Traefik and a deploy user.
grim server:dev:setup
Raise a shared dev box
Turn a bare VPS or LAN machine into a shared development server with Docker, Traefik, one dev user and grim itself.
grim infra:aws-ec2
Raise a server on AWS
Create one EC2 instance with a static address, a firewall and your SSH key in your AWS account, then hand it to grim server:setup.
grim server:keys
Grant or revoke deploy access
List, add and remove the SSH keys that may log in as a server's deploy user, each under a label you choose.