--- title: "How to Install GitLab Runner on Ubuntu 24.04" description: "Install GitLab Runner on Ubuntu 24.04, register it with a runner authentication token, run CI/CD jobs with the Docker executor and tune config.toml safely." category: "CI/CD DevOps" language: en source: https://cubepath.com/docs/ci-cd-devops/gitlab-runner-installation-and-configuration updated: 2026-09-25 --- # How to Install GitLab Runner on Ubuntu 24.04 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: ```bash sudo apt update sudo apt install docker.io ``` Make sure the service is enabled and running: ```bash sudo systemctl enable --now docker sudo docker run --rm hello-world ``` The test container prints a greeting that starts like this: ```output Hello from Docker! This message shows that your installation appears to be working correctly. ``` > Note: If you prefer the latest Docker release, install Docker Engine from Docker's official repository instead. The runner works with either. ## 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: ```bash 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: ```bash 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: ```bash gitlab-runner --version sudo systemctl status gitlab-runner ``` The status output should show the service as `active (running)`: ```output ● 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. > Note: Tags, the untagged option, the protected flag and the timeout are stored in GitLab for runners created this way. You change them in the UI, not with command line flags. ## 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: ```bash 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`: ```output 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: ```bash sudo gitlab-runner verify ``` ```output 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: ```bash 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: ```toml 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: | Setting | Meaning | |---|---| | `concurrent` | Maximum number of jobs this machine runs at the same time, across all runners in the file. | | `executor` | How jobs run. `docker` starts one container per job. | | `image` | Default job image when the pipeline does not set one. | | `privileged` | Gives job containers full access to the host. Keep it `false` unless you need Docker-in-Docker (Step 7). | | `pull_policy` | `if-not-present` reuses images already on the host, which speeds up jobs. | | `memory`, `cpus` | Hard 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: ```bash sudo systemctl restart gitlab-runner ``` If the file contains a syntax error, the logs tell you where: ```bash 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: ```yaml 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: ```output 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: ```bash 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: ```yaml 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: ```toml [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: ```bash 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: ```yaml 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" ``` > Tip: If you do not want privileged containers at all, build images with a daemonless tool such as Buildah or BuildKit in rootless mode instead of Docker-in-Docker. ## Step 8 - Managing the runner List the runners configured on this host: ```bash sudo gitlab-runner list ``` ```output docker-runner-01 Executor=docker Token=glrt-... URL=https://gitlab.com ``` Remove a runner from this host and from GitLab: ```bash 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: ```bash 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.