How to set up gitlab-runner for GitLab CI on macOS

Niklas Klein
The Startup
Published in
5 min readMay 6, 2020

Using your local development machine as GitLab CI runner instead of the shared runners provided on gitlab.com comes with some compelling advantages:

  • Own runners don’t count towards the usage quota
    GitLab has the generous offer of 2000 free CI minutes per month for every group (not project). However, this limit is reached quickly during active development. So instead of purchasing additional CI minutes you might as well execute the CI jobs yourself.
  • Shared runners have very limited resources
    In the past, shared runners had 4GB of memory available, nowadays they are unfortunately limited to only 2GB. Depending on your environment this limit is easily reached.
  • Local execution can be significantly faster
    If your CI setup has a lot of pipelines, you might have observed already that the cache setup and initialization of each runner take their toll. When all pipelines are executed locally, they can access the same docker daemon. This will significantly reduce your CI execution times.

Installing gitlab-runner on macOS is straightforward and well documented in the official GitLab documentation. However, every time I’ve got to install the runner on a new system, I keep stumbling over configuration issues.

Installing gitlab-runner

For the sake of completeness, I will briefly go through the entire installation process. If these steps don’t work for you, please take a look at the official installation instructions.

Install the gitlab-runner application

> brew install gitlab-runner

Register your first runner

> gitlab-runner register
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
> https://gitlab.com/
Please enter the gitlab-ci token for this runner:
// There are two options to consider for the token
// - Group
// the runner is available to all projects of this group
// https://gitlab.com/groups/[group]/-/settings/ci_cd
// - Project
// the runner is only available to a specific project
// https://gitlab.com/[group]/[project]/-/settings/ci_cd
> XXX
Please enter the gitlab-ci description for this runner:
> [XXX]:
Please enter the gitlab-ci tags for this runner (comma separated):
>
Please enter the executor: ssh, virtualbox, docker+machine, kubernetes, parallels, shell, docker-ssh, docker-ssh+machine, custom, docker:
> docker
Please enter the default Docker image (e.g. ruby:2.6):
> docker:latest
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

Optionally register gitlab-runner to automatically start at login

> brew services start gitlab-runner

Start gitlab-runner

Before you proceed, make sure the service is running via gitlab-runner status. If it is not, start it manually via gitlab-runner start.

Then navigate to the CI settings pane of an arbitrary project that should use the runner (https://gitlab.com/[group]/[project]/-/settings/ci_cd) and make sure that your runner is listed under “Specific Runners” or “Group Runners”, depending on which token you supplied earlier.

To be extra safe you can also disable the shared runners on your projects, so that only your local runner is eligible to execute CI jobs. To check on which executor a pipeline is running, simply open the live logs view (e.g. https://gitlab.com/[group]/[project]/-/jobs/[id]) and look at the column on the right.

You are now (almost) ready to go 🎉

Fine-tuning the configuration

In ~/.gitlab-runner/config.toml you will now find the initial configuration of your runner. It should look like this:

concurrent = 1
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "XXX"
url = "https://gitlab.com/"
token = "XXX"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.docker]
tls_verify = false
image = "docker:latest"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0

But to be honest, these default settings do usually not work for me at all. I will immediately run into errors. So here are my tweaks for a smooth ride, make sure to gitlab-runner restart after editing ~/.gitlab-runner/config.toml.

  • concurrent
    This setting is completely up to you and optional. It defines how many pipelines your runner is allowed to execute concurrently. Naturally you want to increase this number, but make sure that your “Docker Desktop” application is assigned sufficient resources in the settings pane.
  • runners.docker.privileged
    Setting privileged = true is necessary when using docker-in-docker (dind), otherwise you will run into errors such as (#4416):
$ docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $GITLAB_REGISTRYWARNING! Using --password via the CLI is insecure. Use --password-stdin.error during connect: Post http://docker:2375/v1.40/auth: dial tcp: lookup docker on 192.168.65.1:53: no such host
  • runners.docker.volumes
    Without adding docker.sock to the volumes explicitly, my pipelines fail when interacting with the docker daemon. The solution is to add /var/run/docker.sock:/var/run/docker.sock to your runners.docker.volumes. Without that adjustment, you might run into errors such as (#1986):
$ docker build --cache-from $GITLAB_REPOSITORY .
Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?
  • runners.docker.pull_policy
    Without setting the pull_policy to either "if_not_present" or "never", the runner will attempt to fetch the image from the GitLab registry instead of your local docker installation. Your pipeline will typically fail right at the start with a message like:
ERROR: Job failed: Error response from daemon: pull access denied for [project], repository does not exist or may require ‘docker login’: denied: requested access to the resource is denied (docker.go:198:1s)
  • runners.docker.wait_for_services_timeout
    When requiring dind as a service, the pipeline usually spends quite some time waiting for the “services to be up and running”. After several seconds it stops and yields warning messages to the logs. In the end the pipeline executes just fine though. I didn’t investigate on this, but found out that you can set the wait_for_services_timeout = 3 and thereby reduce the unnecessary waiting time. But be careful: your mileage may vary here, especially when relying on other services than dind.
Using Docker executor with image docker ...
Starting service docker:dind ...
Using locally found image version due to if-not-present pull policy
Using docker image sha256:c814ba3a for docker:dind ...
Waiting for services to be up and running...
*** WARNING: Service runner-ukv probably didn’t start properly.
Health check error:
service “runner-ukv” timeout
...

After applying these changes, you will end up with a configuration that should look like:

concurrent = 4
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "XXX"
url = "https://gitlab.com/"
token = "XXX"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.docker]
tls_verify = false
image = "docker:latest"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
shm_size = 0
pull_policy = "if-not-present"
wait_for_services_timeout = 3

--

--