GitLab Runner is the agent that picks up CI/CD jobs from GitLab and executes them on your own infrastructure. Running your own runner gives you control over CPU, memory, caching and network access, and avoids the minute limits of shared runners. In this tutorial you will install GitLab Runner from GitLab's official package repository on Ubuntu 24.04, register it with a runner authentication token, run jobs in Docker containers and verify everything with a test pipeline.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 vCPUs and 4 GB of RAM (builds are usually the limiting factor, not the runner itself).
  • A non-root user with sudo privileges.
  • A GitLab project on GitLab.com or on a self-managed GitLab instance, with the Maintainer or Owner role so you can create runners.
  • Outbound HTTPS access from the server to your GitLab instance. The runner polls GitLab, so no inbound ports are required.

Step 1 - Installing Docker

This guide uses the Docker executor, which runs every job in a fresh container. That keeps builds isolated from each other and from the host, and lets each project choose its own image. Install Docker Engine from the Ubuntu repositories:

sudo apt update
sudo apt install docker.io

Make sure the service is enabled and running:

sudo systemctl enable --now docker
sudo docker run --rm hello-world

The test container prints a greeting that starts like this:

Hello from Docker!
This message shows that your installation appears to be working correctly.

Step 2 - Installing GitLab Runner

GitLab publishes the runner in its own APT repository. GitLab provides a script that adds the repository and its signing key. Download it first so you can review it before running it with root privileges:

curl -fsSL -o /tmp/gitlab-runner-repo.sh https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh
less /tmp/gitlab-runner-repo.sh

When you are satisfied with its contents, run it and install the package:

sudo bash /tmp/gitlab-runner-repo.sh
sudo apt install gitlab-runner

The package creates a gitlab-runner system user, installs the binary to /usr/bin/gitlab-runner and registers a systemd service. Check the version and the service state:

gitlab-runner --version
sudo systemctl status gitlab-runner

The status output should show the service as active (running):

● gitlab-runner.service - GitLab Runner
     Loaded: loaded (/etc/systemd/system/gitlab-runner.service; enabled; preset: enabled)
     Active: active (running)

At this point the runner is running but has no configuration, so it does not pick up any jobs yet.

Step 3 - Creating the runner in GitLab

Since GitLab 16, runners are created in the GitLab UI first and then registered on the server with a runner authentication token (it starts with glrt-). The old registration tokens are deprecated and disabled by default on recent GitLab versions, so use the new workflow.

In your project, go to Settings > CI/CD, expand Runners and select New project runner. Fill in the form:

  • Tags: docker, linux. Jobs use tags to select a runner.
  • Run untagged jobs: enable it if you want this runner to also pick up jobs without tags.
  • Runner description: a name such as docker-runner-01.
  • Maximum job timeout: optional, for example 3600 seconds.

Select Create runner. GitLab shows the registration command with the token. Copy the token now; it is only displayed once.

Step 4 - Registering the runner with the Docker executor

Register the runner on the server. Replace https://gitlab.com with the URL of your GitLab instance if you are self-hosting, and your_runner_token with the glrt- token you just copied:

sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com" \
  --token "your_runner_token" \
  --executor "docker" \
  --docker-image "alpine:latest" \
  --description "docker-runner-01"

--docker-image is the default image for jobs whose .gitlab-ci.yml does not set image:. The command writes the configuration to /etc/gitlab-runner/config.toml:

Runtime platform                                    arch=amd64 os=linux ...
Verifying runner... is valid                        runner=AbCdEfGh
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

Confirm that the runner can authenticate against GitLab:

sudo gitlab-runner verify
Verifying runner... is valid                        runner=AbCdEfGh

Back in Settings > CI/CD > Runners, the runner now appears with a green dot, meaning it is online.

Step 5 - Reviewing config.toml

Open the configuration file to see what registration created and to adjust concurrency and resource limits:

sudo nano /etc/gitlab-runner/config.toml

A tuned configuration for a 4 vCPU server looks like this. Keep the id, token and token_* values that registration wrote for you:

concurrent = 2
check_interval = 0
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "docker-runner-01"
  url = "https://gitlab.com"
  id = 12345678
  token = "glrt-your_runner_token"
  token_obtained_at = 2026-09-25T10:00:00Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker"
  [runners.docker]
    image = "alpine:latest"
    privileged = false
    pull_policy = ["if-not-present"]
    memory = "3g"
    cpus = "2"
    volumes = ["/cache"]
    shm_size = 0

The settings that matter most:

SettingMeaning
concurrentMaximum number of jobs this machine runs at the same time, across all runners in the file.
executorHow jobs run. docker starts one container per job.
imageDefault job image when the pipeline does not set one.
privilegedGives job containers full access to the host. Keep it false unless you need Docker-in-Docker (Step 7).
pull_policyif-not-present reuses images already on the host, which speeds up jobs.
memory, cpusHard limits for each job container, so one build cannot starve the others.
volumes/cache is a Docker volume used for the job cache between runs.

GitLab Runner reloads config.toml automatically when it changes. To apply it immediately, restart the service:

sudo systemctl restart gitlab-runner

If the file contains a syntax error, the logs tell you where:

sudo journalctl -u gitlab-runner -n 50 --no-pager

Step 6 - Running a test pipeline

Add a .gitlab-ci.yml file to the root of your repository to confirm that jobs reach your runner. The tags value must match the tags you set in Step 3:

stages:
  - test

runner-check:
  stage: test
  image: alpine:3.20
  tags:
    - docker
  script:
    - echo "Running on $CI_RUNNER_DESCRIPTION"
    - cat /etc/os-release
    - nproc

Commit and push the file. In GitLab, open Build > Pipelines and select the job. The log shows which runner executed it:

Running with gitlab-runner 18.x (...)
  on docker-runner-01 AbCdEfGh, system ID: s_0123456789ab
Preparing the "docker" executor
Using Docker executor with image alpine:3.20 ...
$ echo "Running on $CI_RUNNER_DESCRIPTION"
Running on docker-runner-01
...
Job succeeded

On the server you can follow jobs as they are picked up:

sudo journalctl -u gitlab-runner -f

Using the cache

The Docker executor stores caches in the /cache volume on the runner host, so dependencies survive between pipelines without extra setup. For example, for a Node.js project:

build:
  image: node:22
  tags:
    - docker
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 7 days

The cache key is derived from package-lock.json, so the cache is rebuilt only when dependencies change. artifacts are uploaded to GitLab and passed to later stages; caches stay on the runner.

Step 7 - Building Docker images with Docker-in-Docker (optional)

Jobs that run docker build need access to a Docker daemon. The most common approach is the docker:dind service, which requires privileged containers. Only enable this on a runner dedicated to trusted projects, because a privileged job can take over the host.

Edit /etc/gitlab-runner/config.toml and change the [runners.docker] block:

  [runners.docker]
    image = "docker:27"
    privileged = true
    volumes = ["/certs/client", "/cache"]

The /certs/client volume lets the job and the dind service share the TLS certificates that Docker generates. Restart the runner:

sudo systemctl restart gitlab-runner

Then use a job like this, which builds an image and pushes it to the project's GitLab Container Registry:

build-image:
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  tags:
    - docker
  script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

Step 8 - Managing the runner

List the runners configured on this host:

sudo gitlab-runner list
docker-runner-01    Executor=docker Token=glrt-... URL=https://gitlab.com

Remove a runner from this host and from GitLab:

sudo gitlab-runner unregister --name docker-runner-01

Docker images and stopped containers from past jobs accumulate over time. Clean up unused data regularly with a weekly cron entry or run it manually:

sudo docker system prune --all --force --filter "until=168h"

To back up the runner, save /etc/gitlab-runner/config.toml. It contains the runner token, so store the copy somewhere with restricted access.

Troubleshooting

Jobs stay pending with "This job is stuck because the project doesn't have any runners online assigned to it". The job's tags do not match any online runner. Check the runner's tags in Settings > CI/CD > Runners, or enable Run untagged jobs for jobs without tags.

ERROR: Failed to load config stat /etc/gitlab-runner/config.toml: permission denied. You ran gitlab-runner without sudo. The system service uses /etc/gitlab-runner/config.toml, which only root can read.

Jobs fail with Cannot connect to the Docker daemon at tcp://docker:2375. The job uses the dind service but the runner is not privileged, or DOCKER_TLS_CERTDIR does not match the /certs/client volume. Review Step 7.

Shell executor jobs fail at "Preparing environment". On Ubuntu the gitlab-runner home directory can contain a .bash_logout file that clears the terminal and breaks the shell executor. Remove it with sudo rm /home/gitlab-runner/.bash_logout.

Conclusion

You now have a self-hosted GitLab Runner on Ubuntu 24.04 that executes jobs in isolated Docker containers, with resource limits, a local cache and an optional Docker-in-Docker setup for building images. From here you can register more runners on additional servers to scale out, add a separate protected runner for production deployments, or configure an S3-compatible distributed cache so several runners share the same cache.