How to set up gitlab-runner for GitLab CI on macOS
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
Settingprivileged = 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 addingdocker.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 yourrunners.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 thepull_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 requiringdind
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 thewait_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 thandind
.
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