GitLab Runners

A complete guide to GitLab Runners covering shared, group, and project runners, installation, registration, executors, and best practices for CI/CD job execution.

Shared Group Project Installation
What are GitLab Runners?

GitLab Runners are lightweight agents that execute the jobs defined in your CI/CD pipelines. Every time you push code and a pipeline is created, GitLab needs something to actually run the jobs — compile the code, run the tests, build the images, deploy the application. That's the runner's job. Without a runner, a pipeline is just a definition; the runner is what brings it to life.

What makes runners special is their architectural flexibility. They run outside GitLab itself, on infrastructure you control or that GitLab.com provides. This means you decide where your jobs execute: on your own servers, in your private data center, in a cloud VM, inside a Kubernetes cluster, or on GitLab's shared infrastructure. You also decide what environment the jobs run in: a container, a shell, a VM, or a pod. This separation between GitLab (the orchestrator) and the runner (the executor) is a fundamental design decision that gives GitLab CI/CD enormous flexibility.

Runners are also the primary place where security and isolation matter. Since runners execute arbitrary code from your repository, they need to be isolated from each other and from the host they run on. Modern runners use containers and Kubernetes pods to provide this isolation. Understanding runners — their types, executors, and configuration — is essential for running pipelines safely and efficiently at scale.

Key Concept: A runner is an agent that picks up jobs from GitLab and executes them. GitLab server coordinates the pipeline; runners do the actual work. This separation lets you scale runner capacity independently and choose the right environment for each job.
Types of GitLab Runners

GitLab organizes runners into three scopes: shared, group, and project. Each scope determines who can use the runner and how it's managed. Choosing the right scope is a balance between convenience (shared runners require no setup) and control (project runners give you complete isolation and customization).

The scope determines which jobs a runner can pick up. A shared runner serves every project in the GitLab instance that has shared runners enabled. A group runner serves every project within a specific group (and its subgroups). A project runner serves only one project. This hierarchy means you can start with shared runners for simplicity and graduate to group or project runners as your needs become more specialized.

Type Scope Managed By Best For
Shared All projects (if enabled) GitLab administrators Getting started, general CI, small teams
Group All projects in a group Group owners Teams with shared infrastructure needs
Project One specific project Project maintainers Specialized requirements, sensitive workloads

Shared Runners

Provided by GitLab.com or your GitLab administrator. Available to all projects. Zero setup. Compute minutes may be limited on paid tiers.

Group Runners

Registered at the group level. Available to all projects in the group and its subgroups. Good for team-shared infrastructure.

Project Runners

Registered for a single project. Complete isolation. Ideal for sensitive builds or projects needing specialized hardware.
# Runner scope priority (which runner picks up a job): # 1. Project runners (highest priority) # 2. Group runners # 3. Shared runners (lowest priority) # # A job is picked up by the first available runner # that matches its tags and is in the right scope. # Check which runners are available to a project: # Project → Settings → CI/CD → Runners # - Shared runners: enabled/disabled # - Group runners: listed if any # - Project runners: listed if any # Enable/disable shared runners for a project: # Project → Settings → CI/CD → Runners → Shared runners # - Enable shared runners for this project # Best practice hierarchy: # 1. Start with shared runners (no setup) # 2. Move to group runners for team-specific needs # 3. Use project runners for sensitive/production workloads
Priority Rule: If a project has project runners, they take priority over group and shared runners. If only shared runners are available, they run the job. This lets you "pin" a project to its own infrastructure while still allowing fallback to shared runners.
Installing GitLab Runner

GitLab Runner is a single binary that can be installed on Linux, macOS, Windows, or as a container. For most production use cases, installing it as a package (deb/rpm) on a dedicated host, or running it as a container, is the recommended approach. The container approach is particularly elegant because it keeps the runner itself isolated from the host and makes updates trivial.

The installation method you choose affects how the runner is managed (systemd, Homebrew services, Docker restart policies) and how it's upgraded. For production, always install from the official GitLab repository rather than downloading binaries manually — this ensures you get security updates through your normal package manager. The installer script detects your operating system and configures the correct repository automatically.

# Install GitLab Runner on Debian/Ubuntu curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash sudo apt-get install gitlab-runner # Install GitLab Runner on RHEL/CentOS curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash sudo yum install gitlab-runner # Install GitLab Runner on macOS (Homebrew) brew install gitlab-runner brew services start gitlab-runner # Install GitLab Runner as a binary (any Linux) sudo curl -L --output /usr/local/bin/gitlab-runner \ "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64" sudo chmod +x /usr/local/bin/gitlab-runner # Create a dedicated user sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash # Install as a service sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner sudo gitlab-runner start # Install GitLab Runner in Docker 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 # Verify installation gitlab-runner --version sudo systemctl status gitlab-runner gitlab-runner list
Installation Best Practices:
  • Use the official installer script for your OS
  • Install on dedicated hosts for isolation
  • Use a dedicated non-root user for the runner
  • Keep the runner updated with security patches
  • Use the Docker container for easy upgrades
Registering a Runner

After installing the runner binary, you need to register it with GitLab. Registration is the process of linking a runner to a specific GitLab instance and scope, and it's how the runner obtains the credentials it uses to pick up jobs. Without registration, the runner doesn't know which GitLab server to talk to or which jobs it's allowed to run.

Registration requires a registration token, which you obtain from GitLab. The token encodes the scope (instance, group, or project) — a token from a project's settings only lets you register a project runner, a token from a group's settings only lets you register a group runner, and so on. Tokens are sensitive: anyone with a registration token can register a runner, so treat them like secrets.

During registration, you specify the executor (docker, shell, kubernetes, etc.) and answer a few setup questions. The runner then generates a config file (/etc/gitlab-runner/config.toml) containing its configuration and authentication tokens. After registration, the runner appears in the GitLab UI under Settings → CI/CD → Runners, and it can start picking up jobs.

# Get the registration token: # For project runners: Project → Settings → CI/CD → Runners # For group runners: Group → Settings → CI/CD → Runners # For instance runners: Admin Area → Overview → Runners # Interactive registration sudo gitlab-runner register # Prompts: # Enter the GitLab instance URL: https://gitlab.com/ # Enter the registration token: YOUR_REGISTRATION_TOKEN # Enter a description for the runner: My Docker Runner # Enter tags for the runner: docker,linux # Enter optional maintenance note: Production runner # Enter an executor: docker # Enter the default Docker image: alpine:latest # Runner registered successfully! # Non-interactive registration sudo gitlab-runner register \ --non-interactive \ --url "https://gitlab.com/" \ --registration-token "YOUR_REGISTRATION_TOKEN" \ --executor "docker" \ --docker-image "alpine:latest" \ --description "Docker Runner" \ --tag-list "docker,linux" \ --run-untagged="true" \ --locked="false" # Register a runner with a specific name and tags sudo gitlab-runner register \ --non-interactive \ --url "https://gitlab.com/" \ --registration-token "TOKEN" \ --name "prod-runner-01" \ --executor "docker" \ --docker-image "node:18" \ --tag-list "production,node" \ --access-level "not_protected" # List registered runners sudo gitlab-runner list # Verify a registered runner sudo gitlab-runner verify # View the runner's configuration file sudo cat /etc/gitlab-runner/config.toml # The config.toml contains: # - concurrent: number of parallel jobs # - [[runners]]: list of runner configurations # - runners.executor: docker, shell, kubernetes, etc. # - runners.token: runner authentication token # Best practice: use one registration token per runner # Rotate tokens if a runner is compromised # Store tokens as masked, protected variables if scripted
Registration Token Security:
  • Anyone with a registration token can register a runner
  • Treat registration tokens like passwords
  • Use project runners for sensitive projects (not shared)
  • Rotate tokens if they're exposed
  • Don't store tokens in .gitlab-ci.yml
  • Audit registered runners periodically
Runner Configuration File

The runner's behavior is controlled by /etc/gitlab-runner/config.toml, which is generated during registration and can be edited to tune performance and behavior. This file has two parts: a top-level section with global settings and a [runners] array containing one entry per registered runner.

The most important global setting is concurrent, which controls how many jobs can run simultaneously on this runner. Setting this higher lets you run more jobs in parallel, but it also uses more resources — so it should be tuned to match your host's capacity. Each runner entry also has its own executor-specific configuration: for Docker, this includes the default image, privileged mode, and volume mounts; for Kubernetes, this includes namespace, CPU/memory limits, and pod labels.

Editing config.toml while the runner is running is safe — the runner watches the file and reloads on changes. This means you can tune configuration without restarting the service. Just be careful to keep the TOML valid; a syntax error will prevent reload and the runner will keep using the old configuration until the file is fixed.

# /etc/gitlab-runner/config.toml - complete example concurrent = 4 # Max parallel jobs check_interval = 3 # Seconds between job checks log_level = "info" # trace, debug, info, warn, error [session_server] session_timeout = 1800 # 30 minutes [[runners]] name = "Docker Runner" url = "https://gitlab.com/" token = "RUNNER_TOKEN" executor = "docker" limit = 2 # Max jobs for this runner request_concurrency = 1 # Concurrent requests [runners.docker] tls_verify = false image = "alpine:latest" privileged = false disable_cache = false volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"] shm_size = 0 pull_policy = "if-not-present" network_mode = "bridge" [runners.cache] type = "s3" path = "runner" shared = true [runners.cache.s3] server_address = "s3.amazonaws.com" bucket_name = "my-runner-cache" bucket_location = "us-east-1" authentication_type = "iam" [[runners]] name = "Kubernetes Runner" url = "https://gitlab.com/" token = "RUNNER_TOKEN" executor = "kubernetes" limit = 10 [runners.kubernetes] namespace = "gitlab-runner" image = "alpine:latest" privileged = false cpu_limit = "1" memory_limit = "2Gi" service_cpu_limit = "1" service_memory_limit = "1Gi" helper_cpu_limit = "500m" helper_memory_limit = "512Mi" poll_timeout = 180 [runners.kubernetes.node_selector] "kubernetes.io/os" = "linux" [runners.kubernetes.pod_labels] "app" = "gitlab-runner" # Reload after editing sudo gitlab-runner restart # Or reload without restart sudo kill -HUP $(cat /run/gitlab-runner.pid 2>/dev/null) # Verify configuration sudo gitlab-runner verify sudo gitlab-runner list
Key Configuration Settings:
  • concurrent: Global max parallel jobs across all runners
  • limit: Per-runner max parallel jobs
  • executor: How jobs run (docker, kubernetes, shell)
  • docker.image: Default image for jobs
  • docker.privileged: Allow privileged containers
  • kubernetes.namespace: Namespace for job pods
  • kubernetes.cpu_limit: CPU limit per job
Runner Tags: Routing Jobs to Runners

Tags are the mechanism that decides which runner picks up a specific job. When a job declares tags, GitLab will only assign it to runners that have all the same tags. This is how you route jobs to specific infrastructure — for example, sending GPU-intensive jobs to a runner that has the gpu tag, or sending deployment jobs to a runner that has network access to production.

If a job has no tags, it can be picked up by any runner that has run_untagged: true. This is fine for simple pipelines, but in a multi-runner environment it can lead to jobs landing on the wrong runner — for example, a build job meant for a specialized runner being picked up by a shared one. The discipline of using tags consistently is what makes multi-runner setups predictable.

A runner can have multiple tags, and can be configured to run untagged jobs or only tagged jobs. A common pattern is to tag runners by environment (staging, production), by capability (docker, gpu, arm64), or by team (backend, frontend). Jobs then declare exactly which tags they need, and GitLab handles the routing.

# Register a runner with tags sudo gitlab-runner register \ --non-interactive \ --url "https://gitlab.com/" \ --registration-token "TOKEN" \ --executor "docker" \ --docker-image "alpine:latest" \ --description "Production Runner" \ --tag-list "production,docker,linux" \ --run-untagged="false" \ --locked="true" # Using tags in .gitlab-ci.yml build: stage: build tags: - docker - linux script: - docker build -t myapp . deploy_production: stage: deploy tags: - production script: - deploy.sh production # Job with no tags (picks up any untagged runner) quick_test: script: - npm test # Tags are matched with AND logic: # - All tags must match # - Extra runner tags are OK # - Job fails if no runner matches # Runner tag best practices: # - Use descriptive tags # - Tag by capability (docker, gpu, arm64) # - Tag by environment (staging, production) # - Avoid redundant tags # - Document tags for your team # View runners and their tags: # Project → Settings → CI/CD → Runners # - Available runners: name, tags, status # Debugging tag mismatches: # "This job is stuck because you don't have any active runners" # - Check job tags # - Check runner tags # - Ensure at least one runner has all the required tags
Common Tag Pitfall: If a job has tags but no runner has all of them, the job stays in "pending" state forever (until a matching runner appears). This is one of the most common causes of stuck pipelines. Always ensure at least one runner has the required tags.
Runner Security

Runners execute code from your repository, which means they're a potential attack vector. A compromised runner can access the secrets it handles, attack the host it runs on, and pivot to other resources in your infrastructure. For this reason, runners should be treated as untrusted infrastructure and hardened accordingly.

The most important security control is isolation. Using the Docker or Kubernetes executor means each job runs in its own container or pod, isolated from other jobs and from the host. If you use the shell executor, jobs run directly on the host with the runner user's permissions, which is much harder to secure. Modern deployments should use Docker or Kubernetes executors almost exclusively.

Beyond isolation, several other controls matter: protected runners (only on protected branches), ephemeral runners (created for a single job and destroyed after), and limited runner permissions (least privilege). In high-security environments, you might use one runner per project, run it in a dedicated VM or namespace, and destroy it after each use. This is what GitLab's "runners autoscaling" and "ephemeral runners" features provide.

# Protected runners # Only allow runner to pick up jobs on protected branches sudo gitlab-runner register \ --non-interactive \ --url "https://gitlab.com/" \ --registration-token "TOKEN" \ --executor "docker" \ --docker-image "alpine:latest" \ --description "Protected Runner" \ --tag-list "production" \ --access-level "ref_protected" # Docker executor with security options [[runners]] name = "Secure Docker Runner" executor = "docker" [runners.docker] image = "alpine:latest" privileged = false # No privileged access disable_entrypoint_overwrite = false oom_kill_disable = false disable_cache = false volumes = ["/cache"] # Limited volumes shm_size = 0 network_mode = "bridge" # Isolated network pull_policy = "always" # Kubernetes executor with security [[runners]] name = "Secure K8s Runner" executor = "kubernetes" [runners.kubernetes] namespace = "gitlab-runner" image = "alpine:latest" privileged = false cpu_limit = "1" memory_limit = "2Gi" [runners.kubernetes.pod_security_context] run_as_non_root = true run_as_user = 1000 fs_group = 1000 [runners.kubernetes.container_security_context] allow_privilege_escalation = false read_only_root_filesystem = true capabilities: drop = ["ALL"] # Ephemeral runners (one job per runner) # Configure autoscaling with ephemeral instances # Restrict runner to specific branches [[runners]] name = "Main-only Runner" [runners.referees] [runners.referees.main] only = ["main", "release/*"] # Security best practices: # 1. Use Docker or Kubernetes executor (isolation) # 2. Never use privileged mode without reason # 3. Use protected runners for production # 4. Limit volume mounts to what's needed # 5. Use ephemeral runners for high-security workloads # 6. Keep runners updated with security patches # 7. Audit registered runners regularly # 8. Rotate runner tokens periodically # 9. Isolate runners by trust level # 10. Monitor runner logs for suspicious activity
Runner Security Checklist:
  • Use Docker or Kubernetes executor for isolation
  • Disable privileged mode unless absolutely needed
  • Use protected runners for production secrets
  • Limit volume mounts to necessary paths
  • Run jobs as non-root users
  • Keep the runner binary updated
  • Audit registered runners and tokens
  • Monitor runner activity for anomalies
Runner Troubleshooting
# Job is stuck in "pending" # 1. Check if any runner is available # Project → Settings → CI/CD → Runners # 2. Check tags match: # - Job tags must be a subset of a runner's tags # 3. Check runner is online: sudo gitlab-runner verify # 4. Check runner logs: sudo journalctl -u gitlab-runner -f # Runner not picking up jobs # 1. Verify runner is registered: sudo gitlab-runner list # 2. Check runner's URL and token: sudo cat /etc/gitlab-runner/config.toml # 3. Check the runner status in the UI: # Project → Settings → CI/CD → Runners # 4. Restart the runner: sudo gitlab-runner restart # "This job is stuck because you don't have any active runners" # - No runner is available for the job's tags # - All runners are offline or busy # - Shared runners are disabled for the project # - Fix: add a runner with matching tags, enable shared runners # "This job is stuck because it has no tags and there are no runners" # - The job has no tags but no runner has run_untagged=true # - Fix: set --run-untagged=true when registering a runner # Permission denied errors # - Runner user lacks permissions (docker socket, files) # - Check runner user: ps aux | grep gitlab-runner # - Add runner user to docker group: sudo usermod -aG docker gitlab-runner # - Restart the runner: sudo systemctl restart gitlab-runner # Docker executor issues # - Docker daemon not accessible: sudo docker info # - Docker socket not mounted: sudo cat /etc/gitlab-runner/config.toml | grep volumes # - Add /var/run/docker.sock to volumes # Runner logs and debugging # System logs: sudo journalctl -u gitlab-runner -f # Runner logs (if configured): sudo tail -f /var/log/gitlab-runner/*.log # Run runner in debug mode: sudo gitlab-runner --debug run # Test runner connectivity sudo gitlab-runner verify --delete sudo gitlab-runner list # View runner metrics curl http://localhost:9252/metrics # Reset a runner sudo gitlab-runner unregister --name "runner-name" sudo gitlab-runner register ...
Debugging Tips:
  • Check job logs for error messages
  • Check runner logs with journalctl -u gitlab-runner
  • Verify tags match between job and runner
  • Test with gitlab-runner verify
  • Run the runner in debug mode temporarily
  • Check the runner's metrics for queue depth
Frequently Asked Questions
What's the difference between shared, group, and project runners?
Shared runners serve all projects (subject to availability). Group runners serve projects within a specific group. Project runners serve only one project. Priority: project → group → shared.
Do I need to install a runner for GitLab.com?
No, GitLab.com provides shared runners. You only need to install a runner for self-hosted GitLab or if you need custom capabilities (specific hardware, network access, or software).
What's a registration token and why is it sensitive?
A registration token is a secret used to register a runner with GitLab. Anyone with the token can register a runner in that scope, so treat tokens like passwords and rotate them if exposed.
What's the difference between tags and scope?
Scope decides which projects a runner can serve (shared/group/project). Tags decide which jobs a runner can pick up within those projects. Both must match for a runner to receive a job.
How many concurrent jobs can a runner handle?
Controlled by the global concurrent setting and per-runner limit. Tune based on host resources. Docker and Kubernetes executors handle concurrency better than the shell executor.
How do I secure my runners?
Use Docker or Kubernetes executor for isolation, disable privileged mode, use protected runners for production secrets, limit volume mounts, keep the runner updated, and audit registered runners regularly.
Why is my job stuck in "pending"?
Common causes: no runner with matching tags, all runners busy/offline, or shared runners disabled. Check the runner list in the project settings, verify tags, and check runner logs.
Should I use the shell executor?
Generally no. The shell executor runs jobs directly on the host, providing no isolation between jobs. Use Docker or Kubernetes executors for better isolation and reproducibility.
Previous: Rules & Conditional Pipelines Next: Runner Executors

Runners are the workhorses of GitLab CI/CD. Understanding their types, executors, and security implications is essential for running pipelines reliably and safely at scale.