Set up GitLab CI/CD with Kubernetes runners for scalable deployments

Intermediate 45 min May 28, 2026 105 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Configure GitLab Runner with Kubernetes executor to automatically scale CI/CD workloads. Set up RBAC permissions, deploy pipelines to Kubernetes clusters, and implement resource management policies for efficient container orchestration.

Prerequisites

  • Kubernetes cluster with admin access
  • GitLab instance (self-hosted or GitLab.com)
  • kubectl configured for cluster access
  • Helm 3 installed
  • Docker registry access

What this solves

GitLab CI/CD with Kubernetes runners automatically scales your build and deployment workloads based on demand. This setup eliminates the need to provision dedicated build servers and enables you to deploy applications directly to Kubernetes clusters using GitOps workflows.

Step-by-step configuration

Install kubectl and Helm

Install the Kubernetes command-line tool and Helm package manager to manage your cluster resources.

curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt update && sudo apt install -y helm
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

Create GitLab Runner namespace and service account

Create a dedicated namespace for GitLab Runner with proper RBAC permissions to manage pods and services.

apiVersion: v1
kind: Namespace
metadata:
  name: gitlab-runner
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gitlab-runner
  namespace: gitlab-runner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: gitlab-runner
rules:
  • apiGroups: [""]
resources: ["pods", "pods/exec", "pods/attach", "pods/log"] verbs: ["get", "list", "watch", "create", "patch", "delete"]
  • apiGroups: [""]
resources: ["secrets", "configmaps"] verbs: ["get", "list", "watch", "create", "patch", "delete"]
  • apiGroups: [""]
resources: ["persistentvolumeclaims"] verbs: ["get", "list", "watch", "create", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: gitlab-runner roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: gitlab-runner subjects:
  • kind: ServiceAccount
name: gitlab-runner namespace: gitlab-runner
kubectl apply -f /tmp/gitlab-runner-rbac.yaml

Get GitLab Runner registration token

Navigate to your GitLab project or group settings to obtain the runner registration token. Go to Settings > CI/CD > Runners and copy the registration token.

Security note: Store runner tokens securely and rotate them regularly. Never commit tokens to version control.

Install GitLab Runner with Helm

Deploy GitLab Runner to your Kubernetes cluster using the official Helm chart with Kubernetes executor configuration.

gitlabUrl: https://gitlab.example.com/
runnerToken: glrt-xxxxxxxxxxxxxxxxxx

runners:
  config: |
    [[runners]]
      [runners.kubernetes]
        namespace = "gitlab-runner"
        image = "ubuntu:24.04"
        privileged = false
        service_account = "gitlab-runner"
        
        # Resource requests and limits
        cpu_request = "100m"
        memory_request = "128Mi"
        cpu_limit = "1000m"
        memory_limit = "1Gi"
        
        # Helper image configuration
        helper_cpu_request = "50m"
        helper_memory_request = "64Mi"
        helper_cpu_limit = "100m"
        helper_memory_limit = "128Mi"
        
        # Pod security context
        [runners.kubernetes.pod_security_context]
          run_as_non_root = true
          run_as_user = 1000
          run_as_group = 1000
          fs_group = 1000
        
        # Node selector for specific worker nodes
        [runners.kubernetes.node_selector]
          "node-role.kubernetes.io/worker" = "true"
        
        # Tolerations for dedicated CI nodes
        [[runners.kubernetes.tolerations]]
          key = "ci-workload"
          operator = "Equal"
          value = "true"
          effect = "NoSchedule"

Resource limits for the runner manager pod

resources: requests: memory: 128Mi cpu: 100m limits: memory: 512Mi cpu: 500m

Security context for runner manager

securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: false runAsNonRoot: true runAsUser: 999 capabilities: drop: ["ALL"] serviceAccount: create: false name: gitlab-runner
helm repo add gitlab https://charts.gitlab.io
helm repo update
helm install gitlab-runner gitlab/gitlab-runner \
  --namespace gitlab-runner \
  --values /tmp/gitlab-runner-values.yaml

Configure resource quotas and limits

Set up resource quotas to prevent CI workloads from consuming all cluster resources and affecting other applications.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: gitlab-runner-quota
  namespace: gitlab-runner
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    pods: "20"
    persistentvolumeclaims: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: gitlab-runner-limits
  namespace: gitlab-runner
spec:
  limits:
  - default:
      cpu: "1"
      memory: 1Gi
    defaultRequest:
      cpu: 100m
      memory: 128Mi
    type: Container
  - default:
      storage: 1Gi
    type: PersistentVolumeClaim
kubectl apply -f /tmp/gitlab-runner-quota.yaml

Create network policies for security

Implement network policies to restrict traffic between CI pods and other cluster workloads for enhanced security.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: gitlab-runner-netpol
  namespace: gitlab-runner
spec:
  podSelector:
    matchLabels:
      app: gitlab-runner
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: gitlab-runner
  egress:
  - to: []
    ports:
    - protocol: TCP
      port: 80
    - protocol: TCP
      port: 443
    - protocol: TCP
      port: 53
    - protocol: UDP
      port: 53
  - to:
    - namespaceSelector:
        matchLabels:
          name: gitlab-runner
kubectl apply -f /tmp/gitlab-runner-netpol.yaml

Set up horizontal pod autoscaler

Configure HPA to automatically scale GitLab Runner pods based on CPU and memory usage during high CI workload periods.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: gitlab-runner-hpa
  namespace: gitlab-runner
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: gitlab-runner
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
kubectl apply -f /tmp/gitlab-runner-hpa.yaml

Create GitLab CI pipeline for Kubernetes deployment

Set up a sample CI/CD pipeline that builds Docker images and deploys applications to your Kubernetes cluster.

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""
  KUBECONFIG: /tmp/kubeconfig

stages:
  - build
  - test
  - deploy

build-image:
  stage: build
  image: docker:24-dind
  services:
    - docker:24-dind
  variables:
    DOCKER_HOST: tcp://docker:2376
  before_script:
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

run-tests:
  stage: test
  image: ubuntu:24.04
  script:
    - apt-get update && apt-get install -y curl
    - echo "Running application tests..."
    - curl --version
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy-staging:
  stage: deploy
  image: bitnami/kubectl:latest
  before_script:
    - echo $KUBECONFIG_CONTENT | base64 -d > $KUBECONFIG
    - chmod 600 $KUBECONFIG
  script:
    - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n staging
    - kubectl rollout status deployment/myapp -n staging --timeout=300s
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy-production:
  stage: deploy
  image: bitnami/kubectl:latest
  before_script:
    - echo $KUBECONFIG_CONTENT | base64 -d > $KUBECONFIG
    - chmod 600 $KUBECONFIG
  script:
    - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n production
    - kubectl rollout status deployment/myapp -n production --timeout=600s
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - when: manual

Configure pod disruption budget

Set up a pod disruption budget to ensure CI workloads remain available during cluster maintenance and node upgrades.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: gitlab-runner-pdb
  namespace: gitlab-runner
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: gitlab-runner
kubectl apply -f /tmp/gitlab-runner-pdb.yaml

Verify your setup

Check that GitLab Runner is running and properly registered with your GitLab instance.

kubectl get pods -n gitlab-runner
kubectl logs -n gitlab-runner deployment/gitlab-runner
kubectl get hpa -n gitlab-runner
kubectl describe quota gitlab-runner-quota -n gitlab-runner

Verify the runner appears in your GitLab project settings under CI/CD > Runners. The status should show as "online" with the Kubernetes tag.

Configure scaling policies

Set up cluster autoscaler integration

Configure cluster autoscaler to automatically provision new nodes when CI workloads exceed current cluster capacity.

apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-autoscaler-status
  namespace: kube-system
data:
  nodes.max: "10"
  nodes.min: "2"
  scale-down-delay-after-add: "10m"
  scale-down-unneeded-time: "10m"
  skip-nodes-with-local-storage: "false"
  skip-nodes-with-system-pods: "false"

Configure priority classes

Create priority classes to ensure critical CI jobs get scheduled before lower-priority workloads during resource contention.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: ci-high-priority
value: 1000
globalDefault: false
description: "High priority for critical CI/CD jobs"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: ci-low-priority
value: 100
globalDefault: false
description: "Low priority for background CI jobs"
kubectl apply -f /tmp/priority-classes.yaml

Common issues

SymptomCauseFix
Runner shows offline in GitLabInvalid registration token or network connectivityCheck token validity and firewall rules for GitLab access
Pods stuck in Pending stateInsufficient cluster resources or node selector constraintsScale cluster or adjust resource requests in runner config
Permission denied errorsInsufficient RBAC permissions for service accountVerify ClusterRole includes required verbs for pod management
Build failures with "docker: command not found"Missing Docker-in-Docker configurationUse docker:24-dind service and set DOCKER_HOST variable
Network policy blocking buildsOverly restrictive egress rulesAllow egress to ports 80, 443, and DNS for package downloads

Next steps

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and tuned across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.