Runners

GitLab Runners self-hosted setup

Deploy and manage self-hosted runners for GitLab CI/CD


GitLab Runners are self-hosted agents that run CI/CD jobs for GitLab projects. This guide covers setting up and managing runners across different executors and platforms for optimal DevOps workflow automation.

Platform Overview#

GitLab Runners connect to GitLab instances and execute CI/CD jobs in isolated environments. The runner architecture supports multiple executor types:

  • Docker Executor: Container-based isolation using Docker images
  • Kubernetes Executor: Scalable execution on Kubernetes clusters
  • Shell Executor: Direct execution on the host system
  • SSH Executor: Remote execution on other machines

Each executor provides different levels of isolation, performance, and resource management capabilities.

Prerequisites#

Before setting up GitLab Runners, ensure you have:

  • GitLab account with project access
  • Runner registration token from GitLab project settings
  • Server or infrastructure for hosting runners
  • Administrative access to installation environment
  • Docker installed (for Docker executor)
  • Kubernetes cluster access (for Kubernetes executor)

System Requirements#

Executor TypeMinimum RAMCPU CoresStorage
Docker2 GB220 GB
Kubernetes4 GB450 GB
Shell1 GB110 GB
SSH1 GB110 GB

Infrastructure Setup#

Server Provisioning#

For production workloads, provision dedicated infrastructure:

1
# AWS EC2 instance example
2
aws ec2 run-instances \
3
--image-id ami-0c02fb55956c7d316 \
4
--instance-type t3.medium \
5
--key-name your-key-pair \
6
--security-groups runner-security-group \
7
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=gitlab-runner}]'

Security Group Configuration#

Configure network access for GitLab communication:

1
# Allow outbound HTTPS to GitLab
2
aws ec2 authorize-security-group-egress \
3
--group-id sg-runner \
4
--protocol tcp \
5
--port 443 \
6
--cidr 0.0.0.0/0
7
8
# Allow SSH access for management
9
aws ec2 authorize-security-group-ingress \
10
--group-id sg-runner \
11
--protocol tcp \
12
--port 22 \
13
--source-group sg-admin

Runner Installation#

Linux Installation#

Install GitLab Runner on Ubuntu/Debian systems:

1
# Add GitLab official repository
2
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
3
4
# Install GitLab Runner
5
sudo apt-get install gitlab-runner
6
7
# Verify installation
8
gitlab-runner --version

For RHEL/CentOS systems:

1
# Add GitLab repository
2
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
3
4
# Install runner
5
sudo yum install gitlab-runner
6
7
# Start and enable service
8
sudo systemctl start gitlab-runner
9
sudo systemctl enable gitlab-runner

macOS Installation#

Install using Homebrew or manual installation:

1
# Using Homebrew
2
brew install gitlab-runner
3
4
# Start as service
5
brew services start gitlab-runner

Windows Installation#

Download and install the Windows executable:

1
# Download GitLab Runner
2
Invoke-WebRequest -Uri "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe" -OutFile "gitlab-runner.exe"
3
4
# Install as Windows service
5
.\gitlab-runner.exe install
6
.\gitlab-runner.exe start

Registration#

GitLab 16.0+ uses authentication tokens instead of registration tokens. This is the recommended method — registration tokens are deprecated and will be removed in GitLab 18.0.

Create a runner in the GitLab UI first, then authenticate it on your server:

  1. Navigate to Settings → CI/CD → Runners → New project runner (or group/instance level)
  2. Configure the runner's tags, description, and access settings in the UI
  3. Copy the authentication token (glrt-...) shown after creation
1
# Register using authentication token (GitLab 16.0+)
2
sudo gitlab-runner register \
3
--url "https://gitlab.com/" \
4
--token "glrt-YOUR_AUTHENTICATION_TOKEN" \
5
--name "prod-runner-01" \
6
--executor "docker" \
7
--docker-image "alpine:latest"

Key differences from legacy registration:

  • No registration token needed — the runner is created in the UI first
  • Token format: glrt- prefix (authentication) vs no prefix (legacy registration)
  • Runner management: edit tags, description, and access in the GitLab UI
  • Token rotation: authentication tokens can be rotated without re-registering

Legacy registration (deprecated)#

For GitLab instances before 16.0, use the legacy registration token method:

1
sudo gitlab-runner register \
2
--url "https://gitlab.com/" \
3
--registration-token "your-registration-token" \
4
--name "devops-hub-runner" \
5
--tag-list "docker,linux,devops" \
6
--executor "docker" \
7
--docker-image "alpine:latest"

Token rotation#

Rotate runner authentication tokens without downtime:

1
# Reset the runner token via the API
2
curl --request POST \
3
--header "PRIVATE-TOKEN: YOUR_ADMIN_PAT" \
4
"https://gitlab.com/api/v4/runners/RUNNER_ID/reset_authentication_token"
5
6
# The response contains the new token — update config.toml
7
sudo sed -i "s/token = .*/token = \"NEW_TOKEN\"/" /etc/gitlab-runner/config.toml
8
sudo systemctl restart gitlab-runner

Configuration#

Configuration file#

GitLab Runner configuration is stored in /etc/gitlab-runner/config.toml:

1
concurrent = 4
2
check_interval = 30
3
4
[session_server]
5
session_timeout = 1800
6
7
[[runners]]
8
name = "devops-hub-runner"
9
url = "https://gitlab.com/"
10
token = "runner-token"
11
executor = "docker"
12
[runners.docker]
13
tls_verify = false
14
image = "alpine:latest"
15
privileged = false
16
disable_entrypoint_overwrite = false
17
oom_kill_disable = false
18
disable_cache = false
19
volumes = ["/cache"]
20
shm_size = 0

Tag Management#

Configure runner tags for job targeting:

1
# Register with specific tags
2
gitlab-runner register \
3
--tag-list "docker,production,x86_64" \
4
--run-untagged=false \
5
--locked=false

DevOps Hub Integration#

CI/CD Variables Configuration#

Set up environment variables for DevOps Hub API integration:

1
# In GitLab project settings > CI/CD > Variables
2
variables:
3
DEVOPS_HUB_TOKEN: $DEVOPS_HUB_API_TOKEN
4
DEVOPS_HUB_PROJECT_ID: $PROJECT_ID
5
DEVOPS_HUB_REGION: us-east-1

API Integration Pipeline#

Example .gitlab-ci.yml with DevOps Hub integration:

1
stages:
2
- environment
3
- build
4
- test
5
- deploy
6
- cleanup
7
8
variables:
9
BRANCH_NAME: "feature-${CI_PIPELINE_ID}"
10
11
create_environment:
12
stage: environment
13
image: curlimages/curl:latest
14
script:
15
- |
16
echo "Creating DevOps Hub environment..."
17
RESPONSE=$(curl -X POST "https://console.assistance.bg/api/v2/projects/${DEVOPS_HUB_PROJECT_ID}/branches" \
18
-H "Authorization: Bearer ${DEVOPS_HUB_TOKEN}" \
19
-H "Content-Type: application/json" \
20
-d "{\"branch\": {\"name\": \"${BRANCH_NAME}\", \"parent_id\": \"main\"}}")
21
22
echo "Environment created: $RESPONSE"
23
24
# Extract connection string
25
CONNECTION_STRING=$(echo $RESPONSE | jq -r '.connection_uri')
26
echo "CONNECTION_STRING=${CONNECTION_STRING}" >> environment.env
27
artifacts:
28
reports:
29
dotenv: environment.env
30
only:
31
- branches
32
33
build_application:
34
stage: build
35
image: node:18-alpine
36
script:
37
- npm ci
38
- npm run build
39
- npm run test:unit
40
artifacts:
41
paths:
42
- dist/
43
expire_in: 1 hour
44
dependencies:
45
- create_environment
46
47
run_integration_tests:
48
stage: test
49
image: node:18-alpine
50
services:
51
- postgres:14
52
variables:
53
POSTGRES_DB: testdb
54
POSTGRES_USER: testuser
55
POSTGRES_PASSWORD: testpass
56
script:
57
- npm ci
58
- npm run test:integration
59
dependencies:
60
- build_application
61
- create_environment
62
63
cleanup_environment:
64
stage: cleanup
65
image: curlimages/curl:latest
66
script:
67
- |
68
echo "Cleaning up DevOps Hub environment..."
69
curl -X DELETE "https://console.assistance.bg/api/v2/projects/${DEVOPS_HUB_PROJECT_ID}/branches/${BRANCH_NAME}" \
70
-H "Authorization: Bearer ${DEVOPS_HUB_TOKEN}"
71
when: always
72
dependencies:
73
- create_environment

Executor Types#

Docker Executor#

Provides containerized job execution with full isolation:

1
[[runners]]
2
name = "docker-runner"
3
executor = "docker"
4
[runners.docker]
5
image = "node:18-alpine"
6
privileged = true
7
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
8
pull_policy = "if-not-present"

Use Cases:

  • Application builds with specific runtime requirements
  • Multi-language project support
  • Isolated testing environments

Kubernetes Executor#

Scalable execution on Kubernetes clusters. Each job runs in its own pod, which is destroyed after completion.

1
[[runners]]
2
name = "k8s-runner"
3
executor = "kubernetes"
4
[runners.kubernetes]
5
namespace = "gitlab-runner"
6
image = "ubuntu:22.04"
7
cpu_limit = "2"
8
memory_limit = "4Gi"
9
cpu_request = "500m"
10
memory_request = "1Gi"
11
service_cpu_limit = "1"
12
service_memory_limit = "1Gi"
13
poll_interval = 5
14
poll_timeout = 3600
15
16
# Pod annotations for monitoring/networking
17
[runners.kubernetes.pod_annotations]
18
"prometheus.io/scrape" = "true"
19
"prometheus.io/port" = "9252"
20
21
# Node selection
22
[runners.kubernetes.node_selector]
23
"kubernetes.io/os" = "linux"
24
"node-role" = "ci"
25
26
# Pod tolerations for dedicated CI nodes
27
[[runners.kubernetes.node_tolerations]]
28
key = "ci-workload"
29
operator = "Equal"
30
value = "true"
31
effect = "NoSchedule"
32
33
# Service account for RBAC
34
service_account = "gitlab-runner"

Kubernetes executor with Docker-in-Docker#

For jobs that need to build Docker images inside the Kubernetes executor:

1
[[runners]]
2
name = "k8s-dind-runner"
3
executor = "kubernetes"
4
[runners.kubernetes]
5
namespace = "gitlab-runner"
6
image = "docker:24-cli"
7
privileged = true
8
[[runners.kubernetes.volumes.empty_dir]]
9
name = "docker-certs"
10
mount_path = "/certs/client"
11
medium = "Memory"
12
[runners.kubernetes.pod_security_context]
13
run_as_non_root = false
1
# .gitlab-ci.yml using DinD in Kubernetes
2
build_image:
3
image: docker:24-cli
4
services:
5
- docker:24-dind
6
variables:
7
DOCKER_HOST: tcp://localhost:2376
8
DOCKER_TLS_CERTDIR: "/certs"
9
script:
10
- docker build -t myapp:$CI_COMMIT_SHA .
11
- docker push myapp:$CI_COMMIT_SHA

Deploying the runner on Kubernetes via Helm#

1
helm repo add gitlab https://charts.gitlab.io
2
helm repo update
3
4
helm install gitlab-runner gitlab/gitlab-runner \
5
--namespace gitlab-runner \
6
--create-namespace \
7
--set gitlabUrl=https://gitlab.com \
8
--set runnerToken="glrt-YOUR_TOKEN" \
9
--set runners.config='
10
[[runners]]
11
executor = "kubernetes"
12
[runners.kubernetes]
13
namespace = "gitlab-runner"
14
image = "ubuntu:22.04"
15
cpu_limit = "2"
16
memory_limit = "4Gi"
17
'

Use Cases:

  • High-concurrency CI/CD workloads
  • Auto-scaling based on demand
  • Resource-constrained environments
  • Multi-tenant runner infrastructure

Shell Executor#

Direct execution on the host system:

1
[[runners]]
2
name = "shell-runner"
3
executor = "shell"
4
shell = "bash"
5
[runners.cache]
6
Type = "local"
7
Path = "/opt/cache"

Use Cases:

  • Legacy applications requiring specific system dependencies
  • Performance-critical builds
  • Direct hardware access requirements

SSH Executor#

Remote execution on designated machines:

1
[[runners]]
2
name = "ssh-runner"
3
executor = "ssh"
4
[runners.ssh]
5
host = "build.example.com"
6
port = "22"
7
user = "gitlab-runner"
8
identity_file = "/home/gitlab-runner/.ssh/id_rsa"

Use Cases:

  • Builds requiring specific hardware architectures
  • Distributed build systems
  • Legacy system integration

Multi-OS Support#

Linux Configuration#

Standard configuration for Linux-based runners:

1
runner_linux:
2
stage: build
3
tags:
4
- linux
5
- docker
6
image: ubuntu:20.04
7
script:
8
- apt-get update && apt-get install -y build-essential
9
- make build

macOS Configuration#

Configure macOS runners for iOS/macOS builds:

1
runner_macos:
2
stage: build
3
tags:
4
- macos
5
- xcode
6
script:
7
- xcodebuild -project MyApp.xcodeproj -scheme MyApp
8
only:
9
- /^ios-.*$/

Windows Configuration#

Windows runner setup for .NET applications:

1
runner_windows:
2
stage: build
3
tags:
4
- windows
5
- dotnet
6
script:
7
- dotnet restore
8
- dotnet build --configuration Release
9
artifacts:
10
paths:
11
- bin/Release/

Production Deployment#

The fleeting framework is GitLab's modern autoscaling solution, replacing the deprecated Docker Machine autoscaler. It supports pluggable cloud providers and manages VM lifecycle automatically.

Install a fleeting plugin for your cloud provider:

1
# AWS plugin
2
gitlab-runner fleeting install gitlab-runner-fleeting-plugin-aws
3
4
# Google Cloud plugin
5
gitlab-runner fleeting install gitlab-runner-fleeting-plugin-googlecloud
6
7
# Azure plugin
8
gitlab-runner fleeting install gitlab-runner-fleeting-plugin-azure

Configure autoscaling in config.toml:

1
concurrent = 50
2
3
[[runners]]
4
name = "autoscale-runner"
5
url = "https://gitlab.com"
6
token = "glrt-YOUR_TOKEN"
7
executor = "docker+autoscaler"
8
9
[runners.docker]
10
image = "alpine:latest"
11
12
[runners.autoscaler]
13
plugin = "fleeting-plugin-aws"
14
capacity_per_instance = 1
15
max_use_count = 1 # Ephemeral: 1 job per VM
16
max_instances = 20
17
18
[runners.autoscaler.plugin_config]
19
name = "gitlab-ci-fleet"
20
region = "eu-central-1"
21
22
[runners.autoscaler.connector_config]
23
username = "ubuntu"
24
use_external_addr = true
25
26
# Scale-to-zero: idle VMs are terminated after 5 minutes
27
[[runners.autoscaler.policy]]
28
idle_count = 2
29
idle_time = "5m"
30
31
# Scale up aggressively during business hours
32
[[runners.autoscaler.policy]]
33
periods = ["* * 8-18 * * mon-fri *"]
34
idle_count = 5
35
idle_time = "10m"

Legacy autoscaling with Docker Machine (deprecated)#

Docker Machine autoscaling is deprecated and will be removed in a future GitLab Runner release. Migrate to fleeting plugins instead.

1
[[runners]]
2
limit = 10
3
[runners.docker]
4
[runners.docker.machine]
5
IdleCount = 2
6
IdleTime = 300
7
MaxBuilds = 10
8
MachineDriver = "amazonec2"
9
MachineName = "gitlab-runner-%s"
10
MachineOptions = [
11
"amazonec2-region=us-east-1",
12
"amazonec2-instance-type=t3.medium",
13
"amazonec2-ssh-user=ubuntu"
14
]

Monitoring and Logging#

Enable comprehensive monitoring:

1
# Prometheus metrics endpoint
2
gitlab-runner run --metrics-server ":9252"
3
4
# Structured logging
5
gitlab-runner --log-format json --log-level info run

Runner scopes#

ScopeCreated inVisibilityUse case
InstanceAdmin → CI/CD → RunnersAll projects on the instanceShared infrastructure
GroupGroup → Settings → CI/CD → RunnersAll projects in the groupTeam-level runners
ProjectProject → Settings → CI/CD → RunnersSingle project onlyDedicated workloads

With authentication tokens (GitLab 16.0+), the scope is set when creating the runner in the UI. The runner inherits the scope of the page where it was created.

Protected runners only run jobs on protected branches or tags:

1
[[runners]]
2
name = "protected-runner"
3
token = "glrt-YOUR_TOKEN"
4
executor = "docker"
5
# Set via UI: Settings → CI/CD → Runners → Edit → Protected

Security Best Practices#

  • Use dedicated service accounts for runner registration
  • Implement network segmentation for runner infrastructure
  • Enable runner token rotation policies
  • Configure resource limits to prevent abuse
  • Regular security updates for runner software and host systems
  • Use signed container images for Docker executors
  • Implement secrets management for sensitive variables

Troubleshooting#

Common diagnostic commands#

1
# Check runner status
2
gitlab-runner status
3
4
# View runner logs with debug output
5
gitlab-runner run --debug
6
7
# Verify all registered runners can connect
8
gitlab-runner verify
9
10
# List all registered runners
11
gitlab-runner list
12
13
# Restart runner service
14
sudo systemctl restart gitlab-runner
15
16
# Check systemd logs
17
journalctl -u gitlab-runner -n 50 --no-pager

Jobs stuck in "Pending"#

Common causes:

  • No matching tags: runner tags don't match the job's tags: field
  • Runner paused: check runner status in GitLab UI → CI/CD → Runners
  • Concurrent limit reached: increase concurrent in config.toml
  • Runner offline: verify with gitlab-runner verify
1
# Check how many jobs are running vs limit
2
grep concurrent /etc/gitlab-runner/config.toml
3
gitlab-runner list

Docker executor pull failures#

1
# Test Docker connectivity from the runner
2
sudo -u gitlab-runner docker pull alpine:latest
3
4
# Check Docker daemon status
5
systemctl status docker
6
7
# Verify registry authentication
8
sudo -u gitlab-runner docker login registry.gitlab.com

For private registries, add credentials to config.toml:

1
[[runners]]
2
[runners.docker]
3
[runners.docker.credentials]
4
helper = "store"

Certificate errors#

1
# For self-hosted GitLab with custom CA
2
sudo gitlab-runner register \
3
--tls-ca-file /etc/ssl/certs/gitlab-ca.crt \
4
--url "https://gitlab.internal/" \
5
--token "glrt-YOUR_TOKEN"

Or set it globally in config.toml:

1
[[runners]]
2
tls-ca-file = "/etc/ssl/certs/gitlab-ca.crt"

Next steps#

GitLab Runners self-hosted setup | BA Docs