Virtualization

Configure a GitLab Runner for CI/CD on a Linux VPS

You have a GitLab instance running and a pipeline that keeps picking up "no available runners" errors. The missing piece is a GitLab Runner, the agent that actually executes your CI/CD jobs. This guide walks through installing and configuring a GitLab Runner on a Linux VPS, using the Docker executor, registering it with your GitLab instance, and tuning concurrency so your pipelines run reliably. All commands target Ubuntu 24.04 LTS, but they work on any systemd-based Debian or AlmaLinux distro with minor package-manager changes.

Prerequisites

Before you start, make sure you have the following:

  • Linux VPS with Ubuntu 24.04, Debian 12, or AlmaLinux 9, with at least 2 GB RAM for a single runner (4 GB recommended if you run multiple concurrent jobs or use the Linux VPS for other workloads).
  • Root or sudo access to the server.
  • A GitLab instance: gitlab.com, GitLab self-hosted CE, or GitLab EE. The runner registers the same way regardless of which one you use.
  • Docker Engine installed. If Docker is not installed, follow our guide to installing Docker on an Ubuntu VPS first.

Why you need a dedicated runner instead of shared runners

GitLab shared runners are fine for occasional builds, but they fetch jobs from a queue shared by every project on GitLab.com. Your pipeline sits in that queue behind everyone else's. A dedicated runner on your own infrastructure executes jobs as soon as they are dispatched, and you control the executor, the image cache, and the concurrency. For a self-hosted GitLab instance, a runner is mandatory because GitLab CE does not include built-in job executors.

Running a runner on a VPS also keeps your build artifacts and caches inside your own network. Jobs that compile code and upload packages to a private registry benefit from that locality. If you already run a self-hosted GitLab CE on a VPS, adding a runner on the same infrastructure keeps your entire CI/CD stack in one place.

Install the GitLab Runner binary

GitLab distributes the runner as a single binary with a systemd service. Install it from the official GitLab repository so you get automatic updates through apt.

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

The first command adds the GitLab runner repository and its signing key. The second installs the gitlab-runner package and creates a system user named gitlab-runner plus a systemd service. You should not run jobs as root, so the runner uses its own unprivileged user.

Verify the installation:

gitlab-runner --version

You should see output similar to Version: 17.x.x. The current 2026 release line is 17.x; check the installation reference if you want to confirm the latest stable version before installing.

Register the runner with GitLab

Registration links the runner binary to your GitLab instance through a registration token. You can register in interactive mode, where the tool prompts for each value, or in one command with flags. The one-command form is easier to document and reproduce.

Get the registration token from your GitLab instance. For gitlab.com, go to Settings, CI/CD, Runners, and expand "Create project runner" to see the token. For self-hosted GitLab, go to Admin, CI/CD, Runners for an instance-level runner, or Project, Settings, CI/CD, Runners for a project-level one.

sudo gitlab-runner register \
  --url https://gitlab.com \
  --token YOUR_REGISTRATION_TOKEN \
  --executor docker \
  --docker-image alpine:latest \
  --description "production-build-runner" \
  --docker-privileged

Replace YOUR_REGISTRATION_TOKEN with the actual token and adjust --url if you run self-hosted GitLab. The --executor docker flag tells the runner to execute each job inside a fresh Docker container. The --docker-image alpine:latest is the default image when a job does not specify one. --docker-privileged enables the privileged mode required for Docker-in-Docker builds, which is the standard way to build and push Docker images inside a GitLab pipeline.

Verify the runner is active:

gitlab-runner status

Expected output: gitlab-runner: Service is running. You can also check the runner appears as online in the GitLab Runners page (refresh after a few seconds).

Configure the Docker executor for real workloads

The default config.toml generated at /etc/gitlab-runner/config.toml works for a quick test but needs tuning for production builds. Open it with sudo nano /etc/gitlab-runner/config.toml and inspect the [[runners]] section.

[[runners]]
  name = "production-build-runner"
  url = "https://gitlab.com"
  token = "YOUR_TOKEN"
  executor = "docker"
  [runners.docker]
    image = "alpine:latest"
    privileged = true
    volumes = ["/cache"]
  [runners.cache]
    Type = "s3"

Several settings matter in practice:

  • volumes = ["/cache"]: gives every job a persistent cache volume instead of a throwaway one. Without this, each job starts with an empty cache, which slows down dependency-heavy builds.
  • shm_size: the default 64 MB is too small for Chromium, Playwright, or any browser-based test. Set shm_size = 536870912 (512 MB) to avoid "Shared memory connection was closed" errors.
  • pull_policy = "if-not-present": reduces bandwidth by reusing the local image when the tag is the same. The default pull policy downloads the image on every job.

Apply the changes:

sudo gitlab-runner restart

Verify the runner picks up the new config:

sudo gitlab-runner verify

Expected output: Verifying runner... is alive.

Tune concurrency and job limits

By default a runner executes only one job at a time. For a production CI/CD pipeline with parallel test suites, raise the concurrency so several jobs run simultaneously. The concurrency is capped by your VPS resources: every concurrent Docker job spawns one container, and each container needs vCPU and RAM.

concurrent = 4

Set concurrent = 4 at the top of config.toml (outside the [[runners]] section). Then lower the per-runner limit = 2 so this runner takes two jobs at a time and leaves headroom for pipeline stages that run on other runners. Restart the runner afterward. On a 4 GB RAM VPS, 3 or 4 concurrent jobs is the practical ceiling; exceeding it causes the Docker daemon to OOM-kill containers mid-build.

The trade-off is simple: more concurrency shortens pipeline wall-clock time but raises memory pressure. Use docker stats during a heavy build to see how much RAM your jobs actually consume, then adjust. A runner executing PHP or Node.js builds rarely uses more than 512 MB per job, but a suite that pulls Puppeteer or headless Chrome can hit 1.5 GB per job.

Common runner failures and how to fix them

Three failures cover most runner problems you will hit in the first weeks of operation.

Job stuck, no available runners

The pipeline is stuck even though you registered a runner. Check the runner is online in the GitLab UI, then confirm the registration token is valid. A project runner token is scoped to one project; an instance runner token is not. If your pipeline is in a different project than the token, it will never match. Register a new runner with the correct token.

sudo gitlab-runner list

This shows the configured runner and its token. Compare the token with the one stored in the GitLab runners page. A mismatch means you registered with an outdated token. Run register again with the current one.

Docker connection errors

Jobs fail with "Cannot connect to the Docker daemon at unix:///var/run/docker.sock". The runner user cannot reach the Docker socket. Add the gitlab-runner user to the docker group:

sudo usermod -aG docker gitlab-runner
sudo systemctl restart gitlab-runner

Always restart the service after the group change, otherwise the process keeps its old group membership.

OOM kills during parallel jobs

Containers die with exit code 137, the kernel OOM kill code. Lower the concurrent value, or move the runner to a VPS with more RAM. Before upgrading, check what the jobs actually consume:

journalctl -u gitlab-runner -f

The runner logs the job creation and completion. If you see any fatal error before the kill, the job hit a code problem, not memory. Exit code 137 with no other message is the OOM signal, and the fix is either fewer parallel jobs or more memory. A 2GB RAM VPS runs a single-job CI worker fine; a team with fast feedback needs a bigger plan.

FAQ

What is the difference between a shared runner and a specific runner?

A shared runner is available to every project in a GitLab instance, while a specific runner is tied to one project or group. Specific runners are the typical choice when you operate your own CI/CD because you control their concurrency, executor, and network placement.

Do I need a privileged Docker executor?

Only for jobs that build Docker images with Docker-in-Docker (dind). A plain executor without privileged mode is enough for most test, lint, and compile jobs and is more secure because the container cannot access the host's Docker daemon.

Where does the runner store its configuration?

The global config lives at /etc/gitlab-runner/config.toml. The runner reads this file on every job dispatch, so changes do not require a binary restart, only a service restart for large structural changes.

How do I update the GitLab Runner when a new version is released?

Run sudo apt update && sudo apt install --only-upgrade gitlab-runner. The package pulls the new binary and restarts the service automatically. Check the changelog in the official GitLab Runner docs before major version jumps, since the config format occasionally changes.

Can one runner serve both gitlab.com and a self-hosted instance?

Yes. Each registered runner is independent. Register the same binary twice with different tokens and URLs; each one creates its own entry in config.toml under the [[runners]] section.

Related articles

GitLab Runner CI/CD 配置要点

在 Linux VPS 上安装 GitLab Runner 时,使用官方仓库安装二进制文件,并以 Docker executor 注册是最稳定的方式。注册令牌必须与项目或实例匹配,否则流水线会卡在 no available runners。生产环境建议开启 privileged 模式以支持 Docker-in-Docker 构建,同时将并发数控制在 2 到 4 之间,避免 OOM。遇到连接 Docker 的错误时,将 gitlab-runner 用户加入 docker 组并重启服务即可解决。

Note: This guide is for general reference. Every system and infrastructure has its own specifics, so test each step in a safe environment and consult a qualified engineer before applying it in production.