Purpose-built GitHub Actions self-hosted runner for the christmas-island GitHub org. Contains only the tools our repos actually use — ~2-3GB instead of GitHub's ~50GB full runner image.
# Clone and configure
git clone https://github.com/christmas-island/xmas-runner.git
cd xmas-runner
cp .env.example .env # Edit with your PAT and org settings
# Run with docker compose
docker compose up -dOr run directly:
docker run -d \
--name xmas-runner \
-e GITHUB_PAT="ghp_your_token" \
-e RUNNER_SCOPE="org" \
-e RUNNER_TARGET="christmas-island" \
-e RUNNER_NAME="my-runner" \
-e RUNNER_LABELS="self-hosted,linux,x64,xmas-runner" \
-v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/christmas-island/xmas-runner:latest| Variable | Default | Description |
|---|---|---|
GITHUB_PAT |
— | Personal access token with admin:org scope (for auto-registration) |
RUNNER_TOKEN |
— | Direct registration token (alternative to PAT) |
RUNNER_SCOPE |
— | org or repo (required with PAT) |
RUNNER_TARGET |
— | Org name or owner/repo (required with PAT) |
RUNNER_NAME |
hostname | Display name in GitHub |
RUNNER_LABELS |
self-hosted,linux,x64,xmas-runner |
Comma-separated labels |
RUNNER_GROUP |
default |
Runner group name |
EPHEMERAL |
true |
Exit after one job (recommended) |
GITHUB_URL |
https://github.com |
Base URL (change for GHE) |
git, curl, jq, bash, ca-certificates, wget, zip, unzip, sudo
- Go 1.24 (official tarball)
- Node.js 22 LTS + npm + yarn
- Python 3.12 + pip
- Rust stable (via rustup) + clippy + rustfmt
- Docker CLI + Buildx plugin (host socket, no daemon)
- QEMU user-static (cross-platform builds)
- goreleaser, prek (pre-commit-rs)
- hadolint, shellcheck, shfmt
- golangci-lint
- bats-core
- OpenTofu, tflint, terraform-docs
- PostgreSQL client (psql, pg_isready)
- MySQL client
- yarn
- Create a PAT with
admin:orgscope (for org-level runners) orreposcope (for repo-level) - Set the PAT as
GITHUB_PATenvironment variable - Set
RUNNER_SCOPE=organdRUNNER_TARGET=christmas-island - Start the container — it auto-registers with GitHub
- The runner appears in Settings → Actions → Runners
For repo-level runners, use RUNNER_SCOPE=repo and RUNNER_TARGET=christmas-island/repo-name.
The image is built for linux/amd64. On Apple Silicon Macs you need a few extra steps to match the CI x86_64 environment and work around Docker Desktop quirks.
-
Clone the repo and copy the env file
git clone https://github.com/christmas-island/xmas-runner.git cd xmas-runner cp .env.example .env -
Get a registration token or PAT
Option A — Registration token (quick, expires ~1hr)
- GitHub org → Settings → Actions → Runners → New self-hosted runner
- Copy the token that starts with
A
Option B — Classic PAT (recommended for persistent runners)
- Go to https://github.com/settings/tokens → Generate new token (classic)
- Scopes needed:
admin:org ⚠️ Fine-grained PATs do NOT work for self-hosted runner management — you must use a classic PAT
-
Edit
.env# Use one of: RUNNER_TOKEN=AXXXXXXXXX # from GitHub UI (Option A) GITHUB_PAT=ghp_xxxxxxxxx # classic PAT (Option B) RUNNER_LABELS=self-hosted,linux,X64,xmas-isle,xmas-runner RUNNER_NAME=my-mac-runner # unique name if running multiple -
Start the runner
docker compose up -d
-
Verify it connected
docker logs -f xmas-runner-runner-1 # Look for: "Connected to GitHub"
The docker-compose.yml already includes the Apple Silicon fixes:
platform: linux/amd64— runs via Rosetta to match CI x86_64 environmentprivileged: true+ entrypoint chmod — fixes Docker socket permissions on Docker Desktop Mac- No
runner-workvolume mount — avoids permission denied errors on Docker Desktop
The platform: linux/amd64 line in docker-compose.yml must be set. This is already the default in this repo but if you see this warning, verify it's uncommented.
Check that your runner's labels match the runs-on value in your workflow. For christmas-island workflows, the runner must have the xmas-isle label:
runs-on: [self-hosted, linux, X64, xmas-isle]Check registered labels in GitHub → Settings → Actions → Runners.
The runner name is already registered but the container is gone. Either:
- Change
RUNNER_NAMEin.envto something unique - Or go to GitHub → Settings → Actions → Runners → find the ghost runner → Remove
Don't mount a named volume at /home/runner/actions-runner/_work. Docker Desktop for Mac's volume ownership doesn't match the runner user inside the container. The runner-work volume mount has been removed from docker-compose.yml for this reason.
Docker Desktop for Mac creates the socket as root:root. The entrypoint in docker-compose.yml runs sudo chmod 666 /var/run/docker.sock before starting the runner to fix this. If you see socket permission errors, make sure you're using the entrypoint override and privileged: true from docker-compose.yml.
Registration tokens from the GitHub UI expire in ~1 hour. If your container restarts and fails to register, get a new token — or switch to GITHUB_PAT with a classic PAT (admin:org scope) which auto-renews on each start.
# Build for current architecture
docker build -t xmas-runner:local .
# Build multi-arch
docker buildx build --platform linux/amd64,linux/arm64 -t xmas-runner:local .
# Run tests
docker run --rm --entrypoint "" xmas-runner:local bats tests/tools.bats