# How to self-host a CI server on a VM

Source: https://docs.quake.ai/docs/automation/how-to/self-host-ci-server
Markdown: https://docs.quake.ai/docs/automation/how-to/self-host-ci-server.md

---

# How to self-host a CI server on a VM

Run a continuous integration server on a Quake AI VM to watch your repositories, queue pipelines, and dispatch build jobs. A CI server is the orchestrator that schedules work. It is one component above a single CI runner, which is the agent that executes one job. If you only need an agent that plugs into GitHub Actions or GitLab CI, deploy a [self-hosted CI runner](/resources/deployments/deploy-ci-runner) instead.

Self-hosted CI covers a range of software, from container-native servers that run in a few hundred megabytes of RAM to plugin-rich servers that need 4 GB of RAM or more. This guide gives you one host pattern that runs any of them, a recommended default for small teams, and the sizing and persistence adjustments the heavier servers need.



Quake AI provides the VM, the network, the block storage, and the floating IP. The CI server is open-source or third-party software you install, run, upgrade, and secure inside that VM. You own the host and the data on it.



<Figure size="md" caption="The host pattern: a VM runs the CI server in Docker, build data lives on an attached volume, a security group fronts the web UI and webhook traffic, and the git provider triggers pipelines">

```d2
direction: right

git: "Git provider\n(GitHub, GitLab, Forgejo)" {shape: cylinder}

vm: Quake AI VM {
  sg: Security group
  docker: Docker {
    server: "CI server\ncontainer"
    runner: "Build runner(s)"
    server -> runner: dispatch job
  }
  vol: "Block volume\nconfig + build data"
  docker.server -> vol: persists state
}

targets: "Deploy targets\n/ artifact store"

git -> vm.sg: webhook
vm.sg -> vm.docker.server: web UI + webhook
vm.docker.runner -> targets: deploy
vm.docker.server -> git: pipeline status
```

</Figure>

## Prerequisites

You need:

- A Quake AI account with an active project. See the [Quickstart](/docs/quickstart) if you have not signed up.
- An [SSH key pair](/docs/tools/add-ssh-key) uploaded to your project.
- A git provider you can configure webhooks on (GitHub, GitLab, or a self-hosted [git forge](/resources/deployments/deploy-forgejo-git-ci-template)).
- Familiarity with the CI server you intend to run. This guide covers the host. Each server's own docs cover its pipeline syntax.

## The generic pattern

Self-hosted CI servers share the same four building blocks on Quake AI. Size them up for heavier servers; the shape does not change.

| Building block | What it does | Quake AI primitive |
|---|---|---|
| Compute | Runs the server process and, often, the build runners | A VM sized for the server and its concurrency |
| Persistent storage | Holds server config, credentials, job history, and build caches across rebuilds | A [block storage volume](/docs/block/how-to/create-volume) mounted into the container |
| Network ingress | Accepts the web UI, the git webhook, and any external agents | A [security group](/docs/network/how-to/create-security-group) plus a floating IP |
| Container runtime | Runs the server and runners as containers | Docker on the VM |

The steps below provision this pattern once. Step 5 then runs the server of your choice on top of it.

## Step 1: Size and provision the host

Pick a VM tier from the workload, then create the instance:

- **Lightweight, container-native servers** (Woodpecker, Drone, the Forgejo Actions runner) run comfortably on a small shared-vCPU tier for a single team. Give them headroom for the build jobs themselves, which are heavier than the server.
- **Heavyweight servers** (Jenkins-class) want at least 2 vCPUs and 4 GB of RAM for the server alone, plus more for concurrent builds. Plan for disk growth from plugins and artifacts.

Create the instance from the [Compute how-to](/docs/compute/how-to/create-instance), or provision it as code from the [Simple VM template](/resources/deployments/deploy-simple-vm-template). Assign a floating IP so the git provider can reach the webhook endpoint.

## Step 2: Attach a persistent volume for build data

A CI server accumulates state you do not want to lose on a rebuild: server configuration, encrypted credentials, pipeline history, and caches. Keep that state on a block volume rather than the root disk, so you can detach it, snapshot it, and reattach it to a replacement VM.

Create and attach a volume from the [block storage how-to](/docs/block/how-to/create-volume), then mount it on the host. Confirm the device path before you format: run `openstack volume show VOLUME_NAME -c attachments` or `lsblk` inside the instance. On Quake AI, the first attached data volume appears as `/dev/sdb`.

```bash
sudo mkfs.ext4 /dev/sdb
sudo mkdir -p /srv/ci
echo '/dev/sdb /srv/ci ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab
sudo mount /srv/ci
```

Point your server's data directory at `/srv/ci` when you run it in Step 5. Confirm the mount before continuing:

```bash
df -h /srv/ci
```

The output shows `/dev/sdb` mounted at `/srv/ci`.

## Step 3: Open the security group

A CI server needs a small, deliberate set of inbound ports. Add rules to the instance's [security group](/docs/network/how-to/create-security-group-rules):

| Port | Source | Purpose |
|---|---|---|
| 443 (HTTPS) | Your team's IP ranges, or a VPN | Web UI and API behind TLS |
| 22 (SSH) | Your admin IP ranges | Host administration |
| Webhook port | Your git provider's published IP ranges | Pipeline triggers on push |

Restrict each rule to the narrowest source range that works. A CI server holds deploy credentials, so an open `0.0.0.0/0` web UI is a standing risk. Terminate TLS at a reverse proxy on the host (for example [Caddy](https://caddyserver.com/) or nginx) and serve the UI over HTTPS only.

## Step 4: Install Docker

Install Docker on the host. This pattern runs the server and its runners as containers:

<InstallDocker username="ubuntu" method="official" />

## Step 5: Run your CI server

With the host, the volume, the ports, and Docker in place, run the server of your choice. Bind its data directory to the `/srv/ci` mount so its state lives on the persistent volume.

A minimal Docker Compose file follows this shape:

```yaml
services:
  ci-server:
    image: YOUR_CI_SERVER_IMAGE
    restart: unless-stopped
    ports:
      - "8000:8000"
    volumes:
      - /srv/ci:/data
    environment:
      CI_SERVER_HOST: https://ci.YOUR_DOMAIN
```

Start it and confirm the server is listening:

```bash
docker compose up -d
docker compose ps
```

The next two sections cover which image to put in `YOUR_CI_SERVER_IMAGE`.

## Lightweight, container-native CI servers

For a small team or a solo developer, a container-native server is the recommended default. These servers are built to run as containers, carry a modest memory footprint, and define pipelines as files in your repository:

- **[Woodpecker CI](https://woodpecker-ci.org/)** is a community fork of Drone with a small server and per-pipeline runner agents. It connects to GitHub, GitLab, Gitea, and Forgejo.
- **[Drone](https://www.drone.io/)** uses the same container-per-step model and a single configuration file per repository.
- **The [Forgejo Actions](https://forgejo.org/docs/latest/user/actions/) runner** (and the equivalent Gitea Actions runner) reuses GitHub Actions workflow syntax against a self-hosted forge. If you already run a [self-hosted git forge](/resources/deployments/deploy-forgejo-git-ci-template), this keeps CI on the same host pattern.

Woodpecker as a representative example wires into the Compose file from Step 5: set the server image, the volume mount, and your git provider's OAuth credentials, then register a runner agent against the server. Pipeline definitions live in your repository, so the server holds only configuration and history on `/srv/ci`.

## Heavyweight CI servers

Plugin-rich servers in the Jenkins class (Jenkins as the representative, with TeamCity and similar belonging to the same bucket) trade a larger footprint for a deep ecosystem of plugins and integrations. The host pattern above runs them with three adjustments:

- **More compute.** Give the server at least 2 vCPUs and 4 GB of RAM before adding build executors. Each concurrent build adds to that.
- **State on the volume.** Put the server's home directory (for Jenkins, `JENKINS_HOME`) on the `/srv/ci` mount. It holds job configuration, plugins, credentials, and build history.
- **Disk planning.** Plugins, build logs, and artifacts grow over time. Size the volume for that growth and prune old build data on a schedule.

You install, run, and operate these servers yourself on the VM. Quake AI provides the infrastructure underneath; the CI server is software you manage.

## Secure a long-lived CI server

A CI server runs for months and holds the credentials that deploy your software. Treat it as production infrastructure:

- **Front it with TLS.** Serve the UI over HTTPS through a reverse proxy. Do not expose the raw server port.
- **Limit who can reach it.** Restrict the web UI and SSH to your team's IP ranges or a VPN, as in Step 3.
- **Protect the admin account.** Set a strong password, enable single sign-on where the server supports it, and define least-privilege roles for everyone else.
- **Keep deploy credentials out of pipeline YAML.** Store registry tokens, SSH keys, and cloud credentials in the server's secret store and inject them at runtime. See [How to store application secrets and inject them at runtime](/docs/security/how-to/inject-app-secrets).
- **Patch the host and the server.** Apply OS updates and track the server's own security releases.

## Back up the CI server

The persistent volume holds the state that makes the server reproducible: configuration, credentials, plugins, and build history. Back it up so you can restore the server onto a fresh VM:

- Snapshot the `/srv/ci` volume on a schedule. See [How to schedule volume snapshots and restore data](/docs/block/how-to/schedule-snapshots-and-restore).
- Keep pipeline definitions in git. The repository is the source of truth for what the server runs, which keeps a rebuild fast.
- Test a restore before you rely on it. Attach a backup to a spare VM and confirm the server starts against it.

## Next steps

- [Deploy a self-hosted CI runner](/resources/deployments/deploy-ci-runner): run an agent that plugs into GitHub Actions or GitLab CI without hosting the server
- [How to deploy an application to a Quake AI VM from CI](/docs/automation/how-to/app-cicd-vm): ship builds from your pipeline to a target VM over SSH
- [Deploy a self-hosted git forge and CI with OpenTofu](/resources/deployments/deploy-forgejo-git-ci-template): provision a git server and CI together as code
- [CI/CD pipelines](/resources/solutions/cicd-pipelines): the broader pattern for building, testing, and shipping on Quake AI

## See also

- [How to create a block storage volume](/docs/block/how-to/create-volume): the persistent volume this guide mounts
- [How to store application secrets and inject them at runtime](/docs/security/how-to/inject-app-secrets): keep deploy credentials out of pipeline files
