Docs / Programming & Development / GitLab CI/CD Runners

GitLab CI/CD Runners

By Admin · Mar 15, 2026 · Updated Apr 24, 2026 · 247 views · 3 min read

GitLab CI/CD is a powerful integrated pipeline system, and self-hosted runners give you unlimited build minutes, faster execution, and access to private resources. This guide covers installing, configuring, and optimizing GitLab runners on your VPS.

Installing GitLab Runner

# Add GitLab Runner repository
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install gitlab-runner

# Or install from binary
sudo curl -L --output /usr/local/bin/gitlab-runner "https://s3.dualstack.us-east-1.amazonaws.com/gitlab-runner-downloads/latest/binaries/gitlab-runner-linux-amd64"
sudo chmod +x /usr/local/bin/gitlab-runner
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

Registering a Runner

# Get registration token from GitLab:
# Project > Settings > CI/CD > Runners > Expand

# Register with Docker executor (recommended)
sudo gitlab-runner register \
    --non-interactive \
    --url "https://gitlab.com/" \
    --token "YOUR_REGISTRATION_TOKEN" \
    --executor "docker" \
    --docker-image "alpine:latest" \
    --description "VPS Docker Runner" \
    --tag-list "docker,linux,vps" \
    --docker-privileged=false \
    --docker-volumes "/cache"

Runner Configuration

# /etc/gitlab-runner/config.toml
concurrent = 4  # Max concurrent jobs
check_interval = 3

[[runners]]
  name = "VPS Docker Runner"
  url = "https://gitlab.com/"
  token = "TOKEN"
  executor = "docker"

  [runners.docker]
    image = "alpine:latest"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
    shm_size = 0
    pull_policy = ["if-not-present"]  # Don't pull if image exists locally

  [runners.cache]
    Type = "s3"
    Shared = true
    [runners.cache.s3]
      BucketName = "gitlab-cache"
      Insecure = false

Example .gitlab-ci.yml

stages:
  - test
  - build
  - deploy

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

test:
  stage: test
  image: node:20-alpine
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
  script:
    - npm ci
    - npm test
  tags:
    - docker

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $DOCKER_IMAGE .
    - docker push $DOCKER_IMAGE
  tags:
    - docker
  only:
    - main

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
  script:
    - ssh -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_HOST "docker pull $DOCKER_IMAGE && docker-compose up -d"
  tags:
    - docker
  only:
    - main
  environment:
    name: production
    url: https://example.com

Performance Optimization

# Pre-pull common images to avoid download time
docker pull node:20-alpine
docker pull python:3.12-slim
docker pull golang:1.22-alpine
docker pull docker:24

# Use Docker layer caching
# Mount Docker socket for Docker-in-Docker without privileged mode
volumes = ["/var/run/docker.sock:/var/run/docker.sock"]

# Enable distributed cache
# Use S3-compatible storage (MinIO self-hosted) for shared cache between runners

Security

# Never run privileged containers in shared environments
privileged = false

# Use Docker socket binding instead of DinD when possible
# Restrict runner to specific projects using tags

# Protect CI/CD variables
# Settings > CI/CD > Variables > Protected/Masked

# Run runner as non-root
sudo gitlab-runner install --user=gitlab-runner

Monitoring

# Check runner status
sudo gitlab-runner status
sudo gitlab-runner verify

# View job logs
sudo journalctl -u gitlab-runner -f

# Prometheus metrics (enable in config.toml)
listen_address = ":9252"
# Metrics at http://localhost:9252/metrics

Summary

Self-hosted GitLab runners eliminate the per-minute cost of shared runners and provide faster builds with local Docker image caching. The Docker executor provides clean, isolated build environments for each job. Configure caching carefully, pre-pull common images, and set appropriate concurrency limits based on your VPS resources. For teams using GitLab, a dedicated runner VPS typically pays for itself by eliminating shared runner wait times and CI minute charges.

Was this article helpful?