Who this is for
Anyone running GitLab CI, whether on gitlab.com or a self-managed instance, who wants jobs on a machine with persistent NVMe storage and fixed monthly cost. It suits private projects and trusted contributors. It is a poor fit for public projects accepting merge requests from forks unless you add the isolation described below.
Start from the Debian 12 or Ubuntu 24.04 image with a sudo user. The commands are identical on both.
Step 1: install Docker first
The Docker executor talks to a local Docker Engine. Install it from Docker's own repository, as set out in the Docker install guide, and check it works:
sudo docker run --rm hello-world
Add your log rotation settings to /etc/docker/daemon.json now. CI containers are short-lived, but the runner's helper containers and any long builds still write logs.
Step 2: add GitLab's repository and install
GitLab publishes a script that adds its signed apt repository. Download it, read it, then run it:
curl -fsSLo /tmp/runner-repo.sh \
https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh
less /tmp/runner-repo.sh
sudo bash /tmp/runner-repo.sh
sudo apt install -y gitlab-runner
gitlab-runner --version
The package creates a gitlab-runner system user and an enabled systemd service. With the Docker executor, jobs run inside containers rather than as that user, so it does not need to be in the docker group; the service itself runs as root and talks to the socket directly.
How does registration work with glrt- tokens?
You create the runner in the GitLab UI first, choosing its tags and scope there, and GitLab shows a one-time authentication token starting with glrt-. You then pass that token and the server URL to gitlab-runner register. The older shared registration tokens are deprecated, work only if an admin re-enables them, and are scheduled for removal in GitLab 20.0.
Where to create it depends on scope:
- Project runner: project, Settings, CI/CD, expand Runners, Create project runner.
- Group runner: group, Build, Runners, Create group runner.
- Instance runner (self-managed only): Admin, CI/CD, Runners, Create instance runner.
Set tags such as london and docker, tick "Run untagged jobs" only if you want this box to take everything, then create it and copy the token. Register on the server:
sudo gitlab-runner register --non-interactive \
--url https://gitlab.example.com \
--token glrt-xxxxxxxxxxxxxxxxxxxx \
--executor docker \
--docker-image debian:12-slim \
--description lon-glr-01
Use https://gitlab.com as the URL if you are on the hosted service. The runner shows as online in the same UI screen within a few seconds. The --docker-image value is only the default for jobs that don't name an image:.
Step 3: tune config.toml
Registration writes /etc/gitlab-runner/config.toml. The defaults allow one job at a time and put no limits on containers. A version sized for a four-vCPU, 12 GB server looks like this:
concurrent = 3
check_interval = 3
[[runners]]
name = "lon-glr-01"
url = "https://gitlab.example.com"
token = "glrt-xxxxxxxxxxxxxxxxxxxx"
executor = "docker"
limit = 3
request_concurrency = 2
[runners.docker]
image = "debian:12-slim"
privileged = false
cpus = "2"
memory = "3g"
memory_swap = "3g"
shm_size = 268435456
pull_policy = ["if-not-present", "always"]
volumes = ["/cache"]
disable_cache = false
What each line is doing:
concurrentis the global ceiling across every[[runners]]block in the file.limitcaps one block. With a single block, keep them equal.cpusandmemorycap each job container. Three jobs at 3 GB leaves about 3 GB for the host, Docker and page cache. Settingmemory_swapequal tomemorystops a job swapping itself into a crawl.pull_policytries the local image first and falls back to pulling. That keeps builds fast and avoids registry rate limits, but means a moved tag is not picked up until you pull or prune.volumes = ["/cache"]gives each project a Docker volume for thecache:keyword in your.gitlab-ci.yml. It lives on local NVMe and persists between jobs.
The runner notices edits to config.toml and reloads most settings on its own. Restart it anyway after changing executor settings: sudo systemctl restart gitlab-runner.
Using the cache from a pipeline
build:
image: node:24
tags: [london, docker]
cache:
key:
files: [package-lock.json]
paths: [.npm/]
script:
- npm ci --cache .npm --prefer-offline
- npm run build
The cache key changes only when the lockfile does, so most jobs restore a warm .npm directory from the local volume in under a second. No S3-style distributed cache is needed while all jobs land on one server.
How do I stop the disk filling up?
Prune on a schedule. Every job leaves stopped helper containers, cache volumes accumulate per project and branch, and pulled images are never removed by the runner itself. GitLab ships a cleanup script for this, and a weekly run combined with an image prune keeps a typical server well under its disk size.
sudo /usr/share/gitlab-runner/clear-docker-cache prune-volumes
sudo docker image prune -af --filter "until=240h"
Wrap both in a systemd timer for Sunday night, when nothing is queued. The first command removes only containers and volumes the runner created, so it is safe on a box that also runs other containers. Check progress with docker system df.
Security notes
- Leave
privileged = falseunless a trusted project needs Docker-in-Docker. If it does, register a second runner with its own tag and the privileged flag, and only let protected branches use it. - Tick "Protected" when creating a runner that holds deploy credentials. It will then only pick up jobs from protected branches and tags.
- Keep CI variables in GitLab, masked and protected, not in files on the host.
- The runner polls the GitLab server outbound over HTTPS. No inbound ports are required, so use the portal firewall to allow only SSH from known addresses.
Self-managed GitLab on another server
If your GitLab instance also runs on FastNode, keep the runner on a separate server. A heavy build should never starve the web UI or Sidekiq of memory. Both sit in London, so clones and artefact uploads between them are short trips over the public network on unmetered 1 Gbps ports. There is no private network between servers, so leave TLS on and point the runner at the public HTTPS URL. The self-hosted Git page covers sizing the GitLab side.
Sizing
A runner doing tests for one or two projects fits Boost with concurrent = 1. Docker image builds want Turbo and its 80 GB disk. For three parallel jobs with real memory limits, Nitro at $15 a month (billed in US dollars) gives 12 GB of RAM to share out. If you outgrow it, email support to move up a plan.