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 Type | Minimum RAM | CPU Cores | Storage |
|---|---|---|---|
| Docker | 2 GB | 2 | 20 GB |
| Kubernetes | 4 GB | 4 | 50 GB |
| Shell | 1 GB | 1 | 10 GB |
| SSH | 1 GB | 1 | 10 GB |
Infrastructure Setup#
Server Provisioning#
For production workloads, provision dedicated infrastructure:
1# AWS EC2 instance example2aws 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 GitLab2aws ec2 authorize-security-group-egress \3 --group-id sg-runner \4 --protocol tcp \5 --port 443 \6 --cidr 0.0.0.0/078# Allow SSH access for management9aws ec2 authorize-security-group-ingress \10 --group-id sg-runner \11 --protocol tcp \12 --port 22 \13 --source-group sg-adminRunner Installation#
Linux Installation#
Install GitLab Runner on Ubuntu/Debian systems:
1# Add GitLab official repository2curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash34# Install GitLab Runner5sudo apt-get install gitlab-runner67# Verify installation8gitlab-runner --versionFor RHEL/CentOS systems:
1# Add GitLab repository2curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash34# Install runner5sudo yum install gitlab-runner67# Start and enable service8sudo systemctl start gitlab-runner9sudo systemctl enable gitlab-runnermacOS Installation#
Install using Homebrew or manual installation:
1# Using Homebrew2brew install gitlab-runner34# Start as service5brew services start gitlab-runnerWindows Installation#
Download and install the Windows executable:
1# Download GitLab Runner2Invoke-WebRequest -Uri "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe" -OutFile "gitlab-runner.exe"34# Install as Windows service5.\gitlab-runner.exe install6.\gitlab-runner.exe startRegistration#
Authentication token registration (recommended)#
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:
- Navigate to Settings → CI/CD → Runners → New project runner (or group/instance level)
- Configure the runner's tags, description, and access settings in the UI
- Copy the authentication token (
glrt-...) shown after creation
1# Register using authentication token (GitLab 16.0+)2sudo 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:
1sudo 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 API2curl --request POST \3 --header "PRIVATE-TOKEN: YOUR_ADMIN_PAT" \4 "https://gitlab.com/api/v4/runners/RUNNER_ID/reset_authentication_token"56# The response contains the new token — update config.toml7sudo sed -i "s/token = .*/token = \"NEW_TOKEN\"/" /etc/gitlab-runner/config.toml8sudo systemctl restart gitlab-runnerConfiguration#
Configuration file#
GitLab Runner configuration is stored in /etc/gitlab-runner/config.toml:
1concurrent = 42check_interval = 3034[session_server]5 session_timeout = 180067[[runners]]8 name = "devops-hub-runner"9 url = "https://gitlab.com/"10 token = "runner-token"11 executor = "docker"12 [runners.docker]13 tls_verify = false14 image = "alpine:latest"15 privileged = false16 disable_entrypoint_overwrite = false17 oom_kill_disable = false18 disable_cache = false19 volumes = ["/cache"]20 shm_size = 0Tag Management#
Configure runner tags for job targeting:
1# Register with specific tags2gitlab-runner register \3 --tag-list "docker,production,x86_64" \4 --run-untagged=false \5 --locked=falseDevOps Hub Integration#
CI/CD Variables Configuration#
Set up environment variables for DevOps Hub API integration:
1# In GitLab project settings > CI/CD > Variables2variables:3 DEVOPS_HUB_TOKEN: $DEVOPS_HUB_API_TOKEN4 DEVOPS_HUB_PROJECT_ID: $PROJECT_ID5 DEVOPS_HUB_REGION: us-east-1API Integration Pipeline#
Example .gitlab-ci.yml with DevOps Hub integration:
1stages:2 - environment3 - build4 - test5 - deploy6 - cleanup78variables:9 BRANCH_NAME: "feature-${CI_PIPELINE_ID}"1011create_environment:12 stage: environment13 image: curlimages/curl:latest14 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\"}}")2122 echo "Environment created: $RESPONSE"2324 # Extract connection string25 CONNECTION_STRING=$(echo $RESPONSE | jq -r '.connection_uri')26 echo "CONNECTION_STRING=${CONNECTION_STRING}" >> environment.env27 artifacts:28 reports:29 dotenv: environment.env30 only:31 - branches3233build_application:34 stage: build35 image: node:18-alpine36 script:37 - npm ci38 - npm run build39 - npm run test:unit40 artifacts:41 paths:42 - dist/43 expire_in: 1 hour44 dependencies:45 - create_environment4647run_integration_tests:48 stage: test49 image: node:18-alpine50 services:51 - postgres:1452 variables:53 POSTGRES_DB: testdb54 POSTGRES_USER: testuser55 POSTGRES_PASSWORD: testpass56 script:57 - npm ci58 - npm run test:integration59 dependencies:60 - build_application61 - create_environment6263cleanup_environment:64 stage: cleanup65 image: curlimages/curl:latest66 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: always72 dependencies:73 - create_environmentExecutor 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 = true7 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 = 514 poll_timeout = 36001516 # Pod annotations for monitoring/networking17 [runners.kubernetes.pod_annotations]18 "prometheus.io/scrape" = "true"19 "prometheus.io/port" = "9252"2021 # Node selection22 [runners.kubernetes.node_selector]23 "kubernetes.io/os" = "linux"24 "node-role" = "ci"2526 # Pod tolerations for dedicated CI nodes27 [[runners.kubernetes.node_tolerations]]28 key = "ci-workload"29 operator = "Equal"30 value = "true"31 effect = "NoSchedule"3233 # Service account for RBAC34 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 = true8 [[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 = false1# .gitlab-ci.yml using DinD in Kubernetes2build_image:3 image: docker:24-cli4 services:5 - docker:24-dind6 variables:7 DOCKER_HOST: tcp://localhost:23768 DOCKER_TLS_CERTDIR: "/certs"9 script:10 - docker build -t myapp:$CI_COMMIT_SHA .11 - docker push myapp:$CI_COMMIT_SHADeploying the runner on Kubernetes via Helm#
1helm repo add gitlab https://charts.gitlab.io2helm repo update34helm 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:
1runner_linux:2 stage: build3 tags:4 - linux5 - docker6 image: ubuntu:20.047 script:8 - apt-get update && apt-get install -y build-essential9 - make buildmacOS Configuration#
Configure macOS runners for iOS/macOS builds:
1runner_macos:2 stage: build3 tags:4 - macos5 - xcode6 script:7 - xcodebuild -project MyApp.xcodeproj -scheme MyApp8 only:9 - /^ios-.*$/Windows Configuration#
Windows runner setup for .NET applications:
1runner_windows:2 stage: build3 tags:4 - windows5 - dotnet6 script:7 - dotnet restore8 - dotnet build --configuration Release9 artifacts:10 paths:11 - bin/Release/Production Deployment#
Autoscaling with the Fleeting plugin (recommended)#
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 plugin2gitlab-runner fleeting install gitlab-runner-fleeting-plugin-aws34# Google Cloud plugin5gitlab-runner fleeting install gitlab-runner-fleeting-plugin-googlecloud67# Azure plugin8gitlab-runner fleeting install gitlab-runner-fleeting-plugin-azureConfigure autoscaling in config.toml:
1concurrent = 5023[[runners]]4 name = "autoscale-runner"5 url = "https://gitlab.com"6 token = "glrt-YOUR_TOKEN"7 executor = "docker+autoscaler"89 [runners.docker]10 image = "alpine:latest"1112 [runners.autoscaler]13 plugin = "fleeting-plugin-aws"14 capacity_per_instance = 115 max_use_count = 1 # Ephemeral: 1 job per VM16 max_instances = 201718 [runners.autoscaler.plugin_config]19 name = "gitlab-ci-fleet"20 region = "eu-central-1"2122 [runners.autoscaler.connector_config]23 username = "ubuntu"24 use_external_addr = true2526 # Scale-to-zero: idle VMs are terminated after 5 minutes27 [[runners.autoscaler.policy]]28 idle_count = 229 idle_time = "5m"3031 # Scale up aggressively during business hours32 [[runners.autoscaler.policy]]33 periods = ["* * 8-18 * * mon-fri *"]34 idle_count = 535 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 = 103 [runners.docker]4 [runners.docker.machine]5 IdleCount = 26 IdleTime = 3007 MaxBuilds = 108 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 endpoint2gitlab-runner run --metrics-server ":9252"34# Structured logging5gitlab-runner --log-format json --log-level info runRunner scopes#
| Scope | Created in | Visibility | Use case |
|---|---|---|---|
| Instance | Admin → CI/CD → Runners | All projects on the instance | Shared infrastructure |
| Group | Group → Settings → CI/CD → Runners | All projects in the group | Team-level runners |
| Project | Project → Settings → CI/CD → Runners | Single project only | Dedicated 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 → ProtectedSecurity 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 status2gitlab-runner status34# View runner logs with debug output5gitlab-runner run --debug67# Verify all registered runners can connect8gitlab-runner verify910# List all registered runners11gitlab-runner list1213# Restart runner service14sudo systemctl restart gitlab-runner1516# Check systemd logs17journalctl -u gitlab-runner -n 50 --no-pagerJobs 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
concurrentinconfig.toml - Runner offline: verify with
gitlab-runner verify
1# Check how many jobs are running vs limit2grep concurrent /etc/gitlab-runner/config.toml3gitlab-runner listDocker executor pull failures#
1# Test Docker connectivity from the runner2sudo -u gitlab-runner docker pull alpine:latest34# Check Docker daemon status5systemctl status docker67# Verify registry authentication8sudo -u gitlab-runner docker login registry.gitlab.comFor 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 CA2sudo 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#
- Platform comparison — Compare GitLab with GitHub Actions, Jenkins, and Bazel
- Security hardening — Lock down your runner infrastructure
- Troubleshooting — Comprehensive error resolution guide
- Bare metal deployment — Production infrastructure setup