Skip to content

How to self-host a CI server on a VM

How-to · Updated Jul 2026
Before this

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 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.

Git provider(GitHub, GitLab, Forgejo)Quake AI VMDeploy targets/ artifact storeSecurity groupDockerBlock volumeconfig + build dataCI servercontainerBuild runner(s) dispatch jobpersists statewebhookweb UI + webhookdeploypipeline status
Click to zoom
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

Prerequisites#

You need:

  • A Quake AI account with an active project. See the Quickstart if you have not signed up.
  • An SSH key pair uploaded to your project.
  • A git provider you can configure webhooks on (GitHub, GitLab, or a self-hosted git forge).
  • 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 blockWhat it doesQuake AI primitive
ComputeRuns the server process and, often, the build runnersA VM sized for the server and its concurrency
Persistent storageHolds server config, credentials, job history, and build caches across rebuildsA block storage volume mounted into the container
Network ingressAccepts the web UI, the git webhook, and any external agentsA security group plus a floating IP
Container runtimeRuns the server and runners as containersDocker 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, or provision it as code from the 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, 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:

PortSourcePurpose
443 (HTTPS)Your team's IP ranges, or a VPNWeb UI and API behind TLS
22 (SSH)Your admin IP rangesHost administration
Webhook portYour git provider's published IP rangesPipeline 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 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:

SSH into your VM:

ssh ubuntu@YOUR_FLOATING_IP

Install Docker using the official repository:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Add your user to the Docker group so you can run commands without sudo:

sudo usermod -aG docker ubuntu
newgrp docker

Group membership changes take effect on your next login. The newgrp docker line above activates the group in your current shell. If you skip it or open a new shell, log out and SSH back in before you run docker commands.

Confirm the install with a command that needs the daemon socket:

docker run --rm hello-world

A daemon-less check such as docker --version passes even when the group is not active yet, so it hides this gotcha.

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 is a community fork of Drone with a small server and per-pipeline runner agents. It connects to GitHub, GitLab, Gitea, and Forgejo.
  • Drone uses the same container-per-step model and a single configuration file per repository.
  • The Forgejo 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, 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.
  • 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.
  • 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#

See also#

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

For the full policy, see Usage Guidelines.

Last validated: 10.07.2026

Was this page helpful?