GitLab CI custom runners: install, register with glrt- tokens, and autoscale
You run your own GitLab runner by installing the gitlab-runner binary on a machine, creating a runner in the GitLab UI or API to get a glrt- authentication token, and registering with gitlab-runner register --token. Tags on the runner decide which jobs it picks up. For scale, use the Docker Autoscaler executor on cloud VMs or the Kubernetes executor through the Helm chart.
Runners, scopes and executors
GitLab Runner is a Go binary that polls GitLab for jobs and runs them through an executor. A runner has one of three scopes: instance (admins, all projects), group (all projects in a group) or project (one project). On GitLab.com your jobs on GitLab-hosted instance runners use compute minutes, and the Free tier includes 400 per month. Jobs on your own project or group runners do not consume compute minutes.
The executor decides where the job's script runs:
| Executor | Runs jobs | Status |
|---|---|---|
shell | On the host itself, as the gitlab-runner user | Maintenance mode |
docker | In a fresh container per job on one host | Active |
docker-autoscaler | In containers on cloud VMs that the runner creates and deletes | Active |
instance | On autoscaled cloud VMs, no container | Active |
kubernetes | In a pod per job | Active |
docker+machine | Docker Machine VMs | Deprecated |
Start with docker on a single VM. Move to docker-autoscaler or kubernetes once one host stops keeping up.
Prerequisites
- A Linux VM (x86_64 or arm64) with outbound HTTPS to your GitLab instance. GitLab never connects in to the runner.
- Docker Engine installed if you plan to use the Docker executor.
- Maintainer role on the project for a project runner, Owner on the group for a group runner, or admin access for an instance runner.
Create the runner and get a glrt- token
GitLab replaced the old registration-token flow with runner authentication tokens. You create the runner record in GitLab first, then register a machine against it. In GitLab 17.0 and later, the legacy registration token flow is off by default; group owners or admins can still re-enable it, and GitLab plans to remove it in 20.0. Use the new flow for new runners.
In the UI:
- Project runner: Settings > CI/CD > Runners > Create project runner.
- Group runner: Build > Runners > Create group runner.
- Instance runner: Admin > CI/CD > Runners > Create instance runner.
Set tags, a description, "Run untagged jobs", protection and the maximum timeout here, then select Create runner. GitLab shows a token starting with glrt-. Copy it now; GitLab does not display it again.
For automation, call POST /user/runners with a personal access token that has the create_runner scope:
curl --request POST "https://gitlab.com/api/v4/user/runners" \
--header "PRIVATE-TOKEN: $GITLAB_PAT" \
--data "runner_type=project_type" \
--data "project_id=12345678" \
--data "description=docker-builder-01" \
--data "tag_list=docker,linux,x64"
The response contains "token": "glrt-...". runner_type accepts instance_type, group_type or project_type.
Install and register on Linux
Add GitLab's package repository and install the runner. On Debian or Ubuntu:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" -o script.deb.sh
sudo bash script.deb.sh
sudo apt install gitlab-runner
On RHEL, Fedora or Amazon Linux use script.rpm.sh and sudo dnf install gitlab-runner. The package installs a systemd service. Register against the token you created:
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--token "$RUNNER_TOKEN" \
--executor "docker" \
--docker-image alpine:latest \
--description "docker-builder-01"
With a glrt- token you cannot pass --tag-list, --run-untagged, --locked, --access-level, --maximum-timeout, --paused or --maintenance-note. Those settings live on the runner record in GitLab, so you change them in the UI or API. Check the result with sudo gitlab-runner verify and sudo gitlab-runner list.
Run GitLab Runner in Docker
If you prefer not to install packages on the host, run the runner itself as a container and mount the Docker socket so it can start job containers:
docker run -d --name gitlab-runner --restart always \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest
docker run --rm -v /srv/gitlab-runner/config:/etc/gitlab-runner \
gitlab/gitlab-runner register --non-interactive \
--url "https://gitlab.com/" --token "$RUNNER_TOKEN" \
--executor docker --docker-image alpine:latest
Mounting the socket gives job containers a path to root on the host. Run it on a dedicated build VM and keep it off machines that host other workloads.
config.toml, concurrent and tags
Registration writes /etc/gitlab-runner/config.toml (or ~/.gitlab-runner/config.toml when the runner runs as a non-root user). The runner rereads the file every 3 seconds, so you do not need a restart after edits.
concurrent = 8 # max jobs across all [[runners]] entries in this process
[[runners]]
name = "docker-builder-01"
url = "https://gitlab.com/"
token = "glrt-xxxxxxxxxxxxxxxxxxxx"
executor = "docker"
limit = 6 # max jobs for this entry, 0 = no limit
request_concurrency = 2
[runners.docker]
image = "alpine:latest"
privileged = false
volumes = ["/cache"]
Registration sets concurrent = 1, which leaves a 16-core machine running one job at a time. Raise it to match CPU and memory. limit caps one entry, and concurrent caps the whole process.
Jobs target runners with tags. A runner picks a job only if the runner has every tag the job lists:
build:
image: node:22
tags: [docker, linux, x64]
script:
- npm ci
- npm run build
Untagged jobs go to runners with "Run untagged jobs" turned on. To stop GitLab.com instance runners from taking your jobs, go to Settings > CI/CD > Runners and switch off the toggle in the Instance runners section.
Autoscaling with docker-autoscaler and instance executors
The Docker Autoscaler executor replaces Docker Machine. The runner manager talks to a cloud instance group (an AWS Auto Scaling group, a Google Cloud managed instance group or an Azure VM scale set) through a fleeting plugin, scales it with the queue and runs each job in a container on one of those VMs. You build the VM image with Docker installed and create the group; the runner changes its size.
concurrent = 20
[[runners]]
name = "aws-autoscaler"
url = "https://gitlab.com/"
token = "glrt-xxxxxxxxxxxxxxxxxxxx"
executor = "docker-autoscaler"
[runners.docker]
image = "alpine:latest"
[runners.autoscaler]
plugin = "aws"
capacity_per_instance = 1
max_use_count = 1 # one job per VM, then delete it
max_instances = 20
[runners.autoscaler.plugin_config]
name = "gitlab-runner-asg"
[[runners.autoscaler.policy]]
idle_count = 2
idle_time = "20m0s"
Install the plugin with gitlab-runner fleeting install. max_use_count = 1 gives you ephemeral, one-job-per-VM isolation. idle_count keeps warm machines ready so jobs start without a boot delay; set it to 0 to pay nothing while idle. The instance executor uses the same [runners.autoscaler] block but runs the job script on the VM itself, which suits jobs that need full kernel access or nested virtualisation. Details are in the Docker Autoscaler docs.
Kubernetes: Helm chart and operator
On Kubernetes, the runner manager runs as a pod and creates a pod per job. Install it with the official Helm chart:
# values.yaml
gitlabUrl: https://gitlab.com/
runnerToken: glrt-xxxxxxxxxxxxxxxxxxxx
concurrent: 10
rbac:
create: true
runners:
config: |
[[runners]]
[runners.kubernetes]
namespace = "{{.Release.Namespace}}"
image = "alpine:latest"
helm repo add gitlab https://charts.gitlab.io
helm install --namespace gitlab-runner --create-namespace \
gitlab-runner -f values.yaml gitlab/gitlab-runner
Pair the chart with a cluster autoscaler or Karpenter so nodes follow the pod count. On OpenShift, or if you prefer an operator, install the GitLab Runner Operator from OperatorHub and create a Runner resource (apiVersion: apps.gitlab.com/v1beta2) that references a Secret holding runner-token.
Security hardening
- Mark deploy runners as protected. A protected runner picks up jobs only on protected branches and tags, so a feature branch cannot reach production credentials. Pair this with protected CI/CD variables.
- Watch forks. Merge requests from forks run in the fork's project by default. If a maintainer runs a fork pipeline in the parent project, that code runs on your runners with your variables, so review the diff first.
- Avoid
privileged = trueon shared runners. Docker-in-Docker needs it; prefer Kaniko or Buildah for image builds. - Use one job per VM (
max_use_count = 1) for untrusted code. - Rotate tokens. Reset the runner authentication token from the runner's page if a host leaks.
Troubleshooting
- "This job is stuck because the project doesn't have any runners online assigned to it." The job's tags do not match an online runner, or the runner has "Run untagged jobs" off and the job has no tags.
- Jobs queue although the runner is idle.
concurrentis still 1. Raise it inconfig.toml. registerfails with 403 Forbidden. You passed a legacy registration token, a token from another GitLab instance, or a token from a deleted runner. Create a new runner and use itsglrt-token.- "Cannot connect to the Docker daemon" with the shell executor. Add the user to the docker group:
sudo usermod -aG docker gitlab-runner. - Autoscaler creates no instances. Check the cloud credentials in
plugin_configand that the group's maximum size is at leastmax_instances. Rungitlab-runner --debug runto see plugin errors.
Cost trade-offs
A single always-on VM is cheap to set up and wastes money at night. An autoscaled fleet bills only for busy machines, and you take on the image pipeline, the instance group, runner upgrades and on-call. Distributed cache (S3 or GCS) adds storage cost but saves minutes on every job. If your team builds static sites, see GitLab Pages on a custom runner. For repositories on GitHub or Bitbucket that use GitLab CI, read GitLab CI for GitHub and Bitbucket repos. Running GitHub Actions too? The self-hosted GitHub Actions runner guide covers the other side.
FAQ
Do my own GitLab runners use compute minutes on GitLab.com?
No. Only jobs on GitLab-hosted instance runners count toward the compute minutes quota. Project and group runners you host keep processing jobs after the quota runs out.
Can I still use a GitLab runner registration token?
GitLab has deprecated legacy registration tokens, turned them off by default in 17.0 and plans to remove them in 20.0. Group owners and admins can still re-enable them until then. Create the runner in the UI or with POST /user/runners and register with the glrt- token.
Can one gitlab-runner process serve several projects?
Yes. Register one [[runners]] entry per runner record, or create a single group runner that serves every project in the group.
Docker executor or Kubernetes executor: which one should I use?
Use Docker on one VM or docker-autoscaler on cloud VMs if you have no cluster. Use Kubernetes if you already operate one and want pods per job.
Managed runners with cirunner.dev
cirunner.dev runs one-job-per-VM GitLab runners for you. It provisions a fresh VM per job, registers it with GitLab using a runner token you provide, scales with the queue and destroys the machine after the job. You pick CPU, RAM, x86_64 or arm64, region, base image and an optional GPU, and pay per compute minute.
The service is in early access, with GitLab CI and GitHub Actions as launch platforms. Join the early access list.
Skip the runner fleet
cirunner.dev boots a fresh VM for every GitLab CI/CD job, registers it, and destroys it when the job ends. GitHub Actions and GitLab CI are our launch platforms.
Join early access