A GitLab Runner is the agent that executes CI/CD jobs. GitLab.com provides shared runners, but your own runner on a VPS gives you unlimited build minutes, predictable performance, access to private networks and your own caching. The Docker executor — each job in a fresh container — is the most common and flexible choice.
1. Prepare the server
Use a separate VPS from your GitLab instance and production servers: CI jobs run arbitrary code from your repositories and merge requests.
- 2 vCPU / 4 GB RAM is a good start; builds (especially Docker image builds and JavaScript bundling) like more.
- Install Docker: guide.
2. Install GitLab Runner
From GitLab’s package repository:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner
gitlab-runner --version
(Alternatively run the runner itself in a Docker container — gitlab/gitlab-runner.)
3. Create a runner and register it
In GitLab: Project (or Group) → Settings → CI/CD → Runners → New project runner. Add tags if you want to target this runner specifically (e.g. docker, vps), then create it and copy the runner authentication token (glrt-…).
On the server:
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com" \
--token "glrt-XXXXXXXXXXXX" \
--executor "docker" \
--docker-image "alpine:3.20" \
--description "vps-runner-1"
Use your own GitLab URL for self-managed instances. The runner appears as online in the GitLab UI within seconds.
4. Tune config.toml
Settings live in /etc/gitlab-runner/config.toml:
concurrent = 4 # max jobs at once across all runners on this host
check_interval = 3
[[runners]]
name = "vps-runner-1"
url = "https://gitlab.com"
token = "glrt-…"
executor = "docker"
[runners.docker]
image = "alpine:3.20"
privileged = false
pull_policy = ["if-not-present"]
volumes = ["/cache"]
shm_size = 268435456
[runners.cache]
Type = ""
concurrent— set according to CPU and RAM; too many parallel jobs slow everything down.pull_policy = if-not-presentreuses cached images, speeding up jobs (usealwaysif you need the latest tags every time).volumes = ["/cache"]gives jobs a persistent local cache directory forcache:in.gitlab-ci.yml.
Apply changes with sudo gitlab-runner restart.
5. Building Docker images in CI
Two common approaches:
Docker-in-Docker (dind) — requires privileged = true in config.toml:
build:
image: docker:27
services: [docker:27-dind]
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
Mounting the host’s Docker socket — volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"] — faster with shared layer caches, but every job gets root-equivalent control of the host.
Both give jobs significant power over the runner host. Only use them on runners dedicated to trusted projects. Rootless alternatives like BuildKit (rootless) or Kaniko-style builders reduce the risk.
6. Security
- Don’t share a runner between untrusted projects. Use project or group runners rather than instance-wide ones for sensitive code.
- Protected runners: in the runner settings, enable “Run only on protected branches”, so jobs from unreviewed branches or forks can’t run on a runner that has deployment access.
- Keep
privileged = falseunless needed. - Keep the runner and Docker updated.
7. Clean up disk space
Docker executors accumulate images, volumes and build cache. Schedule a cleanup:
# /etc/cron.d/runner-cleanup
0 4 * * * root docker system prune -af --filter "until=72h" > /dev/null && docker volume prune -f > /dev/null
GitLab also provides a clear-docker-cache script with the runner package. See docker prune.
8. Using it in pipelines
Target the runner with tags:
test:
image: node:22
tags: [docker, vps]
script:
- npm ci
- npm test
Troubleshooting
sudo gitlab-runner status
sudo gitlab-runner verify
sudo journalctl -u gitlab-runner -f
- Jobs stuck in “pending” — tags don’t match, the runner is paused, or it only runs protected branches.
- “Cannot connect to the Docker daemon” in dind jobs — the runner isn’t privileged or the TLS variables are wrong.
- Disk full — prune images and volumes.
Related: GitLab CI/CD deploys over SSH and self-hosted GitHub runners. A dedicated KVM VPS makes an ideal, isolated CI runner.