Automation CI/CD DevOps

GitLab Self-Hosted Runners Demo

In this post we’ll see how and why to use GitLab self-hosted runners. As always, I’ll show a practical demo of GitLab self-hosted runner which runs jobs in CI/CD pipelines. If you later find this article useful take a look at the disclaimer for information on how to thank me.


We have already seen numerous demos of Jenkins running on Kubernetes. It’s cool because Jenkins on Kubernetes schedules builds in dynamically provisioned Kubernetes pods thanks to Jenkins Kubernetes plugin.

We have also seen Jenkins Docker in Docker Agent and its deamonless alternative – Jenkins Podman Agent which both enable running docker/podman commands as part of CI/CD pipelines.

Finally, we have seen how GitLab acts as a great alternative to Jenkins and points to consider while migrating from Jenkins to GitLab.

Now it’s time to see how to use a self-hosted runner in GitLab.

What is GitLab runner?

As GitLab docs put:

GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline.

To elaborate, GitLab runner is essentially an app which polls GitLab for new pipelines. Once it finds one, it schedules an executor (build environment) and runs the job in it.

GitLab Kubernetes executors vs Jenkins on Kubernetes agents.

As we’ll use Kubernetes executor, let’s emphasize the differences between Jenkins on Kubernetes and GitLab using self-hosted runners and Kubernetes executors. Simply put, together they are equivalent to Jenkins Kubernetes agents. The difference lies in that Jenkins controller triggers agents on demand as Kubernetes pods where the pipelines run. Whereas, in GitLab world, runners trigger executors on demand where the pipelines’ jobs run.

While Jenkins controller is the only long running service in Jenkins world, GitLab self-hosted runners are long running and there could be many of them. In addition, if you host GitLab instance and don’t use GitLab SaaS (, GitLab instance is a long-running one as well and is equivalent to the Jenkins controller.

Why use self-hosted GitLab runners?

If you use GitLab SaaS ( website) you can use shared runners that GitLab maintains by default. It’s easy and doesn’t require any effort from you to use them. Yet, what if you need more control of hardware, OS and software than GitLab-hosted runners provide. With self-hosted runners, you can provide a build environment that meets your hardware and software requirements. You can host such an environment on-premises or in a cloud, in a container or as a running process on a VM. To provide a simple example of using a self-hosted runner, think about the case when you need to push your built container images to some corporate image registry which requires certificates for authentication. You can have a custom image bundled with the certificates which the runner will use for pushing the images when running on-premises. The runner will be registered at GitLab instance (even at and will serve the purpose of running GitLab CI/CD pipelines.

Yet, for the demo purposes we’ll use an official Docker in Docker image for GitLab self-hosted runner’s Kubernetes executor.

GitLab Self-Hosted Runner Demo

Let’s put GitLab self-hosted runner into action and run a sample GitLab pipeline on a sample repository which contains Dockerfile. You can, of course, use your own repository or fork mine.

Demo Prerequisites

I’ll use Linode’s managed Kubernetes cluster for the demo and will create the Kubernetes cluster on Linode using linode-cli. If you prefer UI you can create Kubernetes Cluster on Linode using its Cloud Manager UI. Linode is a cloud service provider recently purchased by Akamai. With this purchase, Akamai became a competitor in the cloud providers market. You can repeat this demo on your own Linode account. Create one and get 100$ credit using this link.

In addition to Kubernetes cluster, you’ll need kubectl and helm installed on your local machine.

Install GitLab Self-Hosted Runner on Kubernetes cluster

There are numerous ways to install GitLab runner. We’ll use helm. Let’s first download and edit GitLab runner’s helm chart values.yaml. Let’s download and edit it:

wget -4

Use below runners configuration. It’s based on GitLab docs.

  tags: "docker-on-prem"
  executor: kubernetes
  name: "dind"
  config: |
        namespace = "{{.Release.Namespace}}" 
        privileged = true     
        name = "docker-certs"
        mount_path = "/certs/client"
        medium = "Memory"
# creates service account necessary for scheduling Kubernetes executors for each new pipeline job
  create: true

In addition, specify in values.yaml values for below:

  • gitlabUrl to your GitLab instance url, (I’ll use
  • runnerRegistrationToken

Take both from GitLab repository settings/ci_cd -> Runners (your token will be different and the token you see is just for reference and doesn’t exist anymore, don’t use it)

You can also download the modified values.yaml file from my GitHub (replace runnerRegistrationToken with your own!)

Now, let’s install GitLab runner.

helm repo add gitlab
helm repo update gitlab
helm install --namespace gitlab gitlab-runner -f values.yaml gitlab/gitlab-runner --create-namespace

Wait till all resources are running, healthy and ready by using watch kubectl get all -n gitlab:

$ watch kubectl get all -n gitlab
NAME                                 READY   STATUS    RESTARTS   AGE
pod/gitlab-runner-6c594b74f5-x9zd2   1/1     Running   0          57s

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/gitlab-runner   1/1     1            1           19m

NAME                                       DESIRED   CURRENT   READY   AGE
replicaset.apps/gitlab-runner-6c594b74f5   1         1         1       58s

If you inspect runner’s logs, you’ll notice that it polls GitLab instance for new pipeline jobs:

$ kubectl logs -f pod/gitlab-runner-6c594b74f5-x9zd2 -n gitlab
Checking for jobs...nothing                         runner=nQ4Eh-sy                                                                   
Checking for jobs...nothing                         runner=nQ4Eh-sy   

Also note, that the runner was successfully registered in repository settings on

Use GitLab Self-Hosted Runner

Let’s now run a sample pipeline specified in .gitlab-ci.yml inside the repository.

The pipeline is straightforward (docker daemon’s --mtu configuration avoids the pipeline to be stuck)

  - build

  - name: docker:20.10.16-dind 
    command: ["--mtu=1300"]    

  tags: [docker-on-prem]
  image: docker:20.10.16
  stage: build
    DOCKER_HOST: "tcp://docker:2376"
    DOCKER_TLS_CERTDIR: "/certs"
    - docker build -t test .

We see that the runner picks it up in its logs:

Checking for jobs... received                       job=3568299435 repo_url= runner=hmcyF9vs

So what happened behind the scenes?

Run kubectl get pods -n gitlab while the pipeline is running in order to see that GitLab runner provisioned Kubernetes executor on-demand just for the duration of the pipeline. Pipeline steps (docker build command) ran inside it.

If you add sleep 600 to the pipeline steps, you could inspect the executor closely:

$ kubectl get pod -n gitlab
NAME                                                 READY   STATUS    RESTARTS   AGE
gitlab-runner-68bf85b668-hnpf2                       1/1     Running   0          20m
runner-hmcyf9vs-project-42137791-concurrent-0bm6fm   3/3     Running   0          2m41s

You’ll see that Kubernetes pod has 3 containers inside:

  • gitlab-runner-helper
  • docker container with docker client inside where the pipeline steps run
  • docker:dind – container with docker daemon inside against which docker client authenticates and runs.

We specified the images for the last 2 containers in  .gitlab-ci.yml (build.image and, respectively)


That’s it about GitLab self-hosted runners. As always, feel free to share. If you found this article useful, take a look at the disclaimer for information on how to thank me.

You may find below articles interesting:

Find out recommended GitLab books on Amazon.

Find out recommended Jenkins courses on Pluralsight:

Sign up using this link to get exclusive discounts like 50% off your first month or 15% off an annual subscription)