Self-hosted runners let you run CI/CD jobs on your own machines. Use for custom hardware, private networks, or specific environments.
Why Self-Hosted?
- Custom hardware — GPUs, specialized CPUs
- Private network — Access internal resources
- Larger storage — Bigger disk than hosted
- Specific OS — Custom Linux, Windows, macOS
- Compliance — Data stays on-premise
Installing Runners
System Requirements
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8+ GB |
| Disk | 20 GB | 100+ GB |
| Network | Outbound HTTPS | Low latency |
Download
curl -fsSL https://kizuna.codes/install.sh | bashThe installer checks the release's signed SHA256SUMS against the Kizuna release key built into it, and the archive against SHA256SUMS, before it extracts anything. To install by hand instead, download the archive, SHA256SUMS and SHA256SUMS.minisig from the release at github.com/deep-thinking-lab/kizuna-releases and verify them first:
minisign -V -P RWTZtRmNKVOR0U5V5FxnqiwHfz7nP/olquYcs6jQDckepXx7H7tzwxEt -m SHA256SUMS -x SHA256SUMS.minisig
sha256sum -c --ignore-missing SHA256SUMS
tar xzf kizuna-runner-linux-amd64.tar.gzThe trusted comment minisign prints must name the version you downloaded (kizuna-runner v1.2.3).
Configure
Get token from repository settings:
- Go to Settings → CI/CD → Runners
- Click New Self-Hosted Runner
- Copy registration token
Configure runner:
./kizuna-runner configure \
--url https://kizuna.example.com/org/repo \
--token REGISTRATION_TOKEN \
--name "my-runner" \
--labels "gpu,self-hosted"Install as Service
Linux (systemd)
sudo ./kizuna-runner install
sudo systemctl start kizuna-runner
sudo systemctl enable kizuna-runnermacOS (launchd)
sudo ./kizuna-runner install
sudo launchctl load /Library/LaunchDaemons/kizuna.runner.plistWindows
.\kizuna-runner.exe install
Start-Service kizuna-runnerManual Start
# Run interactively
./kizuna-runner runRunner Labels
Labels route jobs to specific runners:
Assign Labels
./kizuna-runner configure \
--labels "gpu,cuda,linux"Use Labels in Workflow
jobs:
train-model:
runs-on: [self-hosted, gpu, cuda]
steps:
- run: nvidia-smi
- run: python train.pyCommon Labels
| Label | Use Case |
|---|---|
self-hosted | Any self-hosted runner |
linux | Linux OS |
windows | Windows OS |
macos | macOS |
gpu | GPU available |
arm64 | ARM architecture |
production | Production deployment |
Runner API (HTTP)
Under the hood the runner agent speaks a small HTTP protocol. The flow is enroll → register → heartbeat → claim → report.
1. Enroll (owner, one-time)
An authenticated owner mints a one-time registration token scoped to their account. Pass repository_id to pin the runner to a single repo; omit it for an owner-wide runner that serves any repo you own.
curl -X POST /api/v1/self-hosted-runners \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"name": "builder-1", "labels": ["Linux", "X64", "voxeltron-ci"]}'
# → 201 {"id": "...", "registration_token": "<one-time secret>"}The registration_token is shown once. Only its hash is stored.
2. Register (agent)
The agent trades the registration token for a long-lived auth token and declares its runtime labels. The registration token is single-use and cleared on success.
curl -X POST /api/v1/runners/register \
-d '{"registration_token": "<from step 1>",
"labels": ["Linux", "X64", "voxeltron-ci"], "os": "linux", "arch": "x64"}'
# → 201 {"id": "...", "status": "online", "auth_token": "<long-lived bearer>"}3. Heartbeat (agent)
The agent heartbeats to stay online. A runner that misses heartbeats for ~120 s is automatically marked offline and stops matching jobs.
curl -X POST /api/v1/runners/heartbeat -H "Authorization: Bearer $AUTH_TOKEN"4. Claim (agent)
Long-poll for the next job whose runs-on labels this runner satisfies. A claim is atomic — two agents racing for the same job cannot both win.
curl -X POST /api/v1/runners/jobs/claim -H "Authorization: Bearer $AUTH_TOKEN"
# → 200 {"job": {"pipeline_job_id": "...", "steps": [...], "auth_token": "...", "context": {...}}}
# → 204 when nothing is waitingThe per-job auth_token in the payload is scoped to {repository_id, ref} and is what the job uses for build-cache and artifact callbacks.
5. Report (agent)
Report status as the job progresses. Only the runner that claimed a job may report on it.
curl -X POST /api/v1/runners/jobs/$JOB_ID/status \
-H "Authorization: Bearer $AUTH_TOKEN" \
-d '{"status": "success"}' # running | success | failed | cancelledA terminal status fans out to downstream (needs:) jobs exactly as cloud and host execution do.
Label routing & scoping
- A job with
runs-on: [self-hosted, …]is not sent to an ephemeral cloud runner — it is left claimable for a registered runner whose label set is a superset of the job's labels (theself-hostedsentinel is implicit). - A repo-pinned runner only serves its repository; an owner-wide runner serves any repository owned by the same account.
- If a self-hosted job has no matching online runner, it waits (and the dispatcher logs a warning) until one registers — it is never silently dropped.
Runner Groups
Organize runners into groups:
# Create group via API
curl -X POST /api/v1/runner-groups \
-d '{
"name": "gpu-runners",
"visibility": "selected",
"repositories": ["org/ml-project"]
}'Assign runner to group:
./kizuna-runner configure \
--group "gpu-runners"Security
Job Isolation
Each job runs in clean environment:
- Fresh working directory
- Isolated network (optional)
- No access to previous job state
Network Security
Restrict runner network:
# Firewall rules
sudo ufw default deny outgoing
sudo ufw allow out to kizuna.example.com
sudo ufw allow out to npm registry
sudo ufw enableSecrets
Runners access repository secrets. Protect them:
- Limit runner access to specific repos
- Use runner groups with visibility controls
- Rotate credentials regularly
Monitoring
Runner Status
View in Settings → CI/CD → Runners:
my-runner
├── Status: Online
├── Last seen: 2 minutes ago
├── Jobs: 47 completed, 0 failed
└── Labels: [linux, gpu]Logs
# View runner logs
journalctl -u kizuna-runner -f
# Or from runner directory
./kizuna-runner logsMetrics
curl /api/v1/runners/my-runner/metricsReturns:
{
"status": "online",
"jobs_completed": 47,
"jobs_failed": 0,
"average_job_duration": 245,
"disk_usage_percent": 65,
"cpu_percent": 23
}Maintenance
Update Runner
# Stop service
sudo systemctl stop kizuna-runner
# Install the new version (verified, as above)
curl -fsSL https://kizuna.codes/install.sh | bash
# Start service
sudo systemctl start kizuna-runnerClean Working Directory
# Remove old job directories
./kizuna-runner cleanup
# Or manually
rm -rf /opt/kizuna-runner/_work/*Remove Runner
# Unregister
./kizuna-runner remove --token REGISTRATION_TOKEN
# Remove service
sudo systemctl disable kizuna-runner
sudo rm /etc/systemd/system/kizuna-runner.serviceTroubleshooting
Runner Won't Start
Check:
- Token is valid
- URL is correct
- Network connectivity
- Permissions on runner directory
# Test connectivity
curl https://kizuna.example.com/api/v1/health
# Check logs
./kizuna-runner logsJobs Not Running
Check:
- Runner is online
- Labels match workflow
- Runner has access to repository
Out of Disk Space
# Clean up
./kizuna-runner cleanup
# Or set auto-cleanup
./kizuna-runner configure --auto-cleanupNetwork Timeouts
Increase timeouts in configuration:
./kizuna-runner configure \
--http-timeout 60 \
--job-timeout 3600Summary
Self-hosted runners provide:
- Custom environments — Your hardware, your OS
- Private resources — Internal network access
- Scalability — Add runners as needed
- Security — Data stays in your control
They're essential for specialized CI/CD needs.