# Deploy a self-hosted CI runner

Source: https://docs.quake.ai/resources/deployments/deploy-ci-runner
Markdown: https://docs.quake.ai/resources/deployments/deploy-ci-runner.md

---

# Deploy a self-hosted CI runner

Stand up a GitHub Actions or GitLab CI runner on a Quake AI instance for automated builds, tests, and deployments. CI jobs are memory-intensive; configure swap on small tiers before you register the runner.

<PricingCompanion
  components={[
    { kind: "primitive", required: true, label: "CI runner instance", vm: { flavor: "s1a.small" } },
  ]}
/>

<Figure size="md" caption="Self-hosted CI runner on a Quake AI instance: repository events trigger build jobs with swap for memory headroom">

```d2
direction: right

repo: "GitHub or GitLab\nrepository" {shape: cylinder}

vm: Quake AI VM {
  runner: "Self-hosted runner\n(systemd service)"
  swap: "Swap file\n(memory headroom)"
  build: "Build, test,\ndeploy steps"
  runner -> build: pull job
  build -> swap: spills RAM
}

artifacts: "Build artifacts\n/ deployment target"

repo -> vm.runner: push triggers job
vm.build -> repo: status + logs
vm.build -> artifacts: deploy
```

</Figure>



CI runners need significant memory. On the Developer Plan (1 GiB RAM + 1 GiB swap), you can run lightweight build jobs: linting, unit tests, small compiles. For heavy builds (Docker image builds, large test suites, monorepo builds), see [Resource tiers](/docs/account/resource-tiers) for upgrade paths.



## Prerequisites

- An [SSH key pair](/docs/tools/add-ssh-key) uploaded to your account
- A running instance with swap configured, or follow [Launch your first server](/docs/quickstart/launch-your-first-server) and the swap step in [Deploy a containerized web application](/docs/quickstart/deploy-containerized-app#step-3a-configure-swap-developer-plan-and-other-low-ram-tiers)
- A GitHub or GitLab account with admin access to the repository or project where you want to register the runner

## Option A: GitHub Actions runner

### Step 1. Configure swap

If you have not already, add swap (essential for CI workloads):

```bash
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
```

### Step 2. Download and configure the runner

Go to your GitHub repository > **Settings** > **Actions** > **Runners** > **New self-hosted runner**. GitHub shows architecture-specific commands. For Linux x64:

```bash
mkdir -p ~/actions-runner && cd ~/actions-runner

# Resolve the latest runner version from the GitHub API, then download it.
RUNNER_VERSION=$(curl -fsSL https://api.github.com/repos/actions/runner/releases/latest \
  | grep -oP '"tag_name":\s*"v\K[^"]+')
curl -o actions-runner-linux-x64.tar.gz -L \
  "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"
tar xzf actions-runner-linux-x64.tar.gz
rm actions-runner-linux-x64.tar.gz
```

Register the runner with the token from your repository settings. This configuration step is required before Step 3: it generates the `svc.sh` service script the next step uses, so do not skip it:

```bash
./config.sh \
  --url https://github.com/YOUR_ORG/YOUR_REPO \
  --token YOUR_REGISTRATION_TOKEN \
  --name rumble-dev-runner \
  --labels quake-ai,developer-plan \
  --work _work
```

### Step 3. Install as a service



These commands assume `./config.sh` from Step 2 completed successfully. The configuration step generates `svc.sh` in the runner directory from `bin/systemd.svc.sh.template`, so the script exists only after registration. Running `sudo ./svc.sh install` before registering fails with `sudo: ./svc.sh: command not found`.



```bash
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status
```

The runner is now registered and waiting for jobs.

### Step 4. Target the runner in workflows

In your `.github/workflows/*.yml`, use the `runs-on` label to target this runner:

```yaml
jobs:
  test:
    runs-on: [self-hosted, quake-ai]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
```

---

## Option B: GitLab Runner

### Step 1. Configure swap

Same as GitHub Actions: see Step 1 above.

### Step 2. Install GitLab Runner

```bash
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | \
  sudo bash
sudo apt install -y gitlab-runner
```

### Step 3. Register the runner

Go to your GitLab project > **Settings** > **CI/CD** > **Runners** > **New project runner** to get a registration token. Then register:

```bash
sudo gitlab-runner register \
  --url https://gitlab.com/ \
  --token YOUR_REGISTRATION_TOKEN \
  --executor shell \
  --description "rumble-dev-runner" \
  --tag-list "quake-ai,developer-plan"
```



Use the `shell` executor on the Developer Plan. It runs jobs directly on the VM without the overhead of Docker-in-Docker. If you need Docker builds, see [Deploy a containerized web application](/docs/quickstart/deploy-containerized-app) first, then register with `--executor docker` and an Alpine-based default image.



### Step 4. Verify the runner

Check the runner status:

```bash
sudo gitlab-runner status
sudo gitlab-runner list
```

Target the runner in `.gitlab-ci.yml`:

```yaml
test:
  tags:
    - quake-ai
  script:
    - npm ci
    - npm test
```

## Memory management for CI

On the Developer Plan, CI jobs compete for the same 1 GB of RAM. Follow these practices:

- **Run one job at a time**: set `--concurrency 1` for GitLab Runner, or only register one GitHub runner.
- **Avoid Docker-in-Docker**: the `shell` executor uses less memory than launching containers per job.
- **Limit Node.js memory** if running JavaScript builds:

```bash
export NODE_OPTIONS="--max-old-space-size=384"
```

- **Monitor during builds**: SSH in and run `htop` or `free -h` while a job runs to see peak usage.
- **Use lightweight jobs**: linting, unit tests, and small compiles work well. Large Docker image builds or complete test suites may need OpenClaw Starter or Basic.

## When to upgrade

The Developer Plan runner is good for:

- Small projects with lightweight CI (lint, test, deploy scripts)
- Personal projects where build speed is not critical
- Trying out self-hosted runners before committing to larger infrastructure

Upgrade to **OpenClaw Starter** (4 GiB RAM, 4 shared vCPUs) or **Basic** (16 GiB RAM, 4 dedicated vCPUs) when you need:

- Docker-based CI with image builds
- Parallel job execution
- Large dependency trees or monorepo builds
- Consistent, fast build times

## Next steps

- [How to deploy an application to a Quake AI VM from CI](/docs/automation/how-to/app-cicd-vm): push builds from GitHub Actions or GitLab CI to this runner
- [How to deploy to a Quake AI Kubernetes cluster from CI](/docs/kubernetes/how-to/deploy-from-ci): same pattern for Magnum clusters
- [How to integrate OpenTofu with CI/CD](/docs/automation/how-to/cicd-integration): provision infrastructure in the pipeline before app deploy
- [Deploy a containerized web application](/docs/quickstart/deploy-containerized-app): add Docker-based builds
- [Automate your infrastructure with OpenTofu](/docs/quickstart/automate-infrastructure-opentofu): automate runner provisioning
- [Resource tiers](/docs/account/resource-tiers): compare plan capabilities

## Clean up

Deregister the runner from your repository settings, stop the systemd service (`sudo ./svc.sh stop` for GitHub Actions or `sudo gitlab-runner unregister` for GitLab), and delete the instance when finished.

## See also

- [How to store application secrets and inject them at runtime](/docs/security/how-to/inject-app-secrets): keep registry tokens and deploy keys out of workflow YAML
- [Instances concepts](/docs/compute/concepts/instances)
- [Security groups concepts](/docs/network/concepts/security-groups)
