1 { config, lib, pkgs, ... }:
10 unsafeDiscardStringContext
41 cfg = config.services.gitlab-runner;
42 hasDocker = config.virtualisation.docker.enable;
43 hasPodman = config.virtualisation.podman.enable && config.virtualisation.podman.dockerSocket.enable;
45 /* The whole logic of this module is to diff the hashes of the desired vs existing runners
46 The hash is recorded in the runner's name because we can't do better yet
47 See https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29350 for more details
49 genRunnerName = name: service: let
50 hash = substring 0 12 (hashString "md5" (unsafeDiscardStringContext (toJSON service)));
51 in if service ? description && service.description != null
52 then "${hash} ${service.description}"
53 else "${name}_${config.networking.hostName}_${hash}";
55 hashedServices = mapAttrs'
56 (name: service: nameValuePair (genRunnerName name service) service) cfg.services;
57 configPath = ''"$HOME"/.gitlab-runner/config.toml'';
58 configureScript = pkgs.writeShellApplication {
59 name = "gitlab-runner-configure";
60 runtimeInputs = [ cfg.package ] ++ (with pkgs; [
70 text = if (cfg.configFile != null) then ''
71 cp ${cfg.configFile} ${configPath}
72 # make config file readable by service
73 chown -R --reference="$HOME" "$(dirname ${configPath})"
75 export CONFIG_FILE=${configPath}
77 mkdir -p "$(dirname ${configPath})"
80 # update global options
81 remarshal --if toml --of json ${configPath} \
82 | jq -cM 'with_entries(select([.key] | inside(["runners"])))' \
83 | jq -scM '.[0] + .[1]' - <(echo ${escapeShellArg (toJSON cfg.settings)}) \
84 | remarshal --if json --of toml \
85 | sponge ${configPath}
87 # remove no longer existing services
88 gitlab-runner verify --delete
90 ${toShellVar "NEEDED_SERVICES" (lib.mapAttrs (name: value: 1) hashedServices)}
92 declare -A REGISTERED_SERVICES
94 while IFS="," read -r name token;
96 REGISTERED_SERVICES["$name"]="$token"
97 done < <(gitlab-runner --log-format json list 2>&1 | grep Token | jq -r '.msg +"," + .Token')
99 echo "NEEDED_SERVICES: " "''${!NEEDED_SERVICES[@]}"
100 echo "REGISTERED_SERVICES:" "''${!REGISTERED_SERVICES[@]}"
102 # difference between current and desired state
103 declare -A NEW_SERVICES
104 for name in "''${!NEEDED_SERVICES[@]}"; do
105 if [ ! -v 'REGISTERED_SERVICES[$name]' ]; then
106 NEW_SERVICES[$name]=1
110 declare -A OLD_SERVICES
111 # shellcheck disable=SC2034
112 for name in "''${!REGISTERED_SERVICES[@]}"; do
113 if [ ! -v 'NEEDED_SERVICES[$name]' ]; then
114 OLD_SERVICES[$name]=1
118 # register new services
119 ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
120 # TODO so here we should mention NEW_SERVICES
121 if [ -v 'NEW_SERVICES["${name}"]' ] ; then
122 bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
124 if service.registrationConfigFile != null
125 then service.registrationConfigFile
126 else service.authenticationTokenConfigFile} &&"
127 "gitlab-runner register"
130 "--executor ${service.executor}"
131 "--limit ${toString service.limit}"
132 "--request-concurrency ${toString service.requestConcurrency}"
134 ++ optional (service.authenticationTokenConfigFile == null)
135 "--maximum-timeout ${toString service.maximumTimeout}"
136 ++ service.registrationFlags
137 ++ optional (service.buildsDir != null)
138 "--builds-dir ${service.buildsDir}"
139 ++ optional (service.cloneUrl != null)
140 "--clone-url ${service.cloneUrl}"
141 ++ optional (service.preGetSourcesScript != null)
142 "--pre-get-sources-script ${service.preGetSourcesScript}"
143 ++ optional (service.postGetSourcesScript != null)
144 "--post-get-sources-script ${service.postGetSourcesScript}"
145 ++ optional (service.preBuildScript != null)
146 "--pre-build-script ${service.preBuildScript}"
147 ++ optional (service.postBuildScript != null)
148 "--post-build-script ${service.postBuildScript}"
149 ++ optional (service.authenticationTokenConfigFile == null && service.tagList != [ ])
150 "--tag-list ${concatStringsSep "," service.tagList}"
151 ++ optional (service.authenticationTokenConfigFile == null && service.runUntagged)
153 ++ optional (service.authenticationTokenConfigFile == null && service.protected)
154 "--access-level ref_protected"
155 ++ optional service.debugTraceDisabled
156 "--debug-trace-disabled"
157 ++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
158 ++ optionals (hasPrefix "docker" service.executor) (
160 assertMsg (service.dockerImage != null)
161 "dockerImage option is required for ${service.executor} executor (${name})");
162 [ "--docker-image ${service.dockerImage}" ]
163 ++ optional service.dockerDisableCache
164 "--docker-disable-cache"
165 ++ optional service.dockerPrivileged
166 "--docker-privileged"
167 ++ map (v: "--docker-volumes ${escapeShellArg v}") service.dockerVolumes
168 ++ map (v: "--docker-extra-hosts ${escapeShellArg v}") service.dockerExtraHosts
169 ++ map (v: "--docker-allowed-images ${escapeShellArg v}") service.dockerAllowedImages
170 ++ map (v: "--docker-allowed-services ${escapeShellArg v}") service.dockerAllowedServices
172 ))} && sleep 1 || exit 1
176 # check key is in array https://stackoverflow.com/questions/30353951/how-to-check-if-dictionary-contains-a-key-in-bash
178 echo "NEW_SERVICES: ''${NEW_SERVICES[*]}"
179 echo "OLD_SERVICES: ''${OLD_SERVICES[*]}"
180 # unregister old services
181 for NAME in "''${!OLD_SERVICES[@]}"
183 [ -n "$NAME" ] && gitlab-runner unregister \
184 --name "$NAME" && sleep 1
187 # make config file readable by service
188 chown -R --reference="$HOME" "$(dirname ${configPath})"
191 startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
192 export CONFIG_FILE=${configPath}
193 exec gitlab-runner run --working-directory $HOME
196 options.services.gitlab-runner = {
197 enable = mkEnableOption "Gitlab Runner";
198 configFile = mkOption {
199 type = types.nullOr types.path;
202 Configuration file for gitlab-runner.
204 {option}`configFile` takes precedence over {option}`services`.
205 {option}`checkInterval` and {option}`concurrent` will be ignored too.
207 This option is deprecated, please use {option}`services` instead.
208 You can use {option}`registrationConfigFile` and
209 {option}`registrationFlags`
210 for settings not covered by this module.
213 settings = mkOption {
214 type = types.submodule {
215 freeformType = (pkgs.formats.json { }).type;
219 Global gitlab-runner configuration. See
220 <https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section>
221 for supported values.
224 gracefulTermination = mkOption {
228 Finish all remaining jobs before stopping.
229 If not set gitlab-runner will stop immediately without waiting
230 for jobs to finish, which will lead to failed builds.
233 gracefulTimeout = mkOption {
235 default = "infinity";
236 example = "5min 20s";
238 Time to wait until a graceful shutdown is turned into a forceful one.
241 package = mkPackageOption pkgs "gitlab-runner" {
242 example = "gitlab-runner_1_11";
244 extraPackages = mkOption {
245 type = types.listOf types.package;
248 Extra packages to add to PATH for the gitlab-runner process.
251 services = mkOption {
252 description = "GitLab Runner services.";
254 example = literalExpression ''
256 # runner for building in docker via host's nix-daemon
257 # nix store will be readable in runner, might be insecure
259 # File should contain at least these two variables:
261 # - `REGISTRATION_TOKEN`
263 # NOTE: Support for runner registration tokens will be removed in GitLab 18.0.
264 # Please migrate to runner authentication tokens soon. For reference, the example
265 # runners below this one are configured with authentication tokens instead.
266 registrationConfigFile = "/run/secrets/gitlab-runner-registration";
268 dockerImage = "alpine";
270 "/nix/store:/nix/store:ro"
271 "/nix/var/nix/db:/nix/var/nix/db:ro"
272 "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:ro"
274 dockerDisableCache = true;
275 preBuildScript = pkgs.writeScript "setup-container" '''
276 mkdir -p -m 0755 /nix/var/log/nix/drvs
277 mkdir -p -m 0755 /nix/var/nix/gcroots
278 mkdir -p -m 0755 /nix/var/nix/profiles
279 mkdir -p -m 0755 /nix/var/nix/temproots
280 mkdir -p -m 0755 /nix/var/nix/userpool
281 mkdir -p -m 1777 /nix/var/nix/gcroots/per-user
282 mkdir -p -m 1777 /nix/var/nix/profiles/per-user
283 mkdir -p -m 0755 /nix/var/nix/profiles/per-user/root
284 mkdir -p -m 0700 "$HOME/.nix-defexpr"
286 . ''${pkgs.nix}/etc/profile.d/nix.sh
288 ''${pkgs.nix}/bin/nix-env -i ''${concatStringsSep " " (with pkgs; [ nix cacert git openssh ])}
290 ''${pkgs.nix}/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable
291 ''${pkgs.nix}/bin/nix-channel --update nixpkgs
293 environmentVariables = {
294 ENV = "/etc/profile";
296 NIX_REMOTE = "daemon";
297 PATH = "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin";
298 NIX_SSL_CERT_FILE = "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt";
302 # runner for building docker images
304 # File should contain at least these two variables:
307 authenticationTokenConfigFile = "/run/secrets/gitlab-runner-docker-images-token-env";
309 dockerImage = "docker:stable";
311 "/var/run/docker.sock:/var/run/docker.sock"
313 tagList = [ "docker-images" ];
315 # runner for executing stuff on host system (very insecure!)
316 # make sure to add required packages (including git!)
317 # to `environment.systemPackages`
319 # File should contain at least these two variables:
322 authenticationTokenConfigFile = "/run/secrets/gitlab-runner-shell-token-env";
325 tagList = [ "shell" ];
327 # runner for everything else
329 # File should contain at least these two variables:
332 authenticationTokenConfigFile = "/run/secrets/gitlab-runner-default-token-env";
333 dockerImage = "debian:stable";
337 type = types.attrsOf (types.submodule {
339 authenticationTokenConfigFile = mkOption {
340 type = with types; nullOr path;
343 Absolute path to a file containing environment variables used for
344 gitlab-runner registrations with *runner authentication tokens*.
345 They replace the deprecated *runner registration tokens*, as
346 outlined in the [GitLab documentation].
348 A list of all supported environment variables can be found with
349 `gitlab-runner register --help`.
351 The ones you probably want to set are:
352 - `CI_SERVER_URL=<CI server URL>`
353 - `CI_SERVER_TOKEN=<runner authentication token secret>`
356 Make sure to use a quoted absolute path,
357 or it is going to be copied to Nix Store.
360 [GitLab documentation]: https://docs.gitlab.com/17.0/ee/ci/runners/new_creation_workflow.html#estimated-time-frame-for-planned-changes
363 registrationConfigFile = mkOption {
364 type = with types; nullOr path;
367 Absolute path to a file with environment variables
368 used for gitlab-runner registration with *runner registration
371 A list of all supported environment variables can be found in
372 `gitlab-runner register --help`.
374 The ones you probably want to set are:
375 - `CI_SERVER_URL=<CI server URL>`
376 - `REGISTRATION_TOKEN=<registration secret>`
378 Support for *runner registration tokens* is deprecated since
379 GitLab 16.0, has been disabled by default in GitLab 17.0 and
380 will be removed in GitLab 18.0, as outlined in the
381 [GitLab documentation]. Please consider migrating to
382 [runner authentication tokens] and check the documentation on
383 {option}`services.gitlab-runner.services.<name>.authenticationTokenConfigFile`.
386 Make sure to use a quoted absolute path,
387 or it is going to be copied to Nix Store.
390 [GitLab documentation]: https://docs.gitlab.com/17.0/ee/ci/runners/new_creation_workflow.html#estimated-time-frame-for-planned-changes
391 [runner authentication tokens]: https://docs.gitlab.com/17.0/ee/ci/runners/new_creation_workflow.html#the-new-runner-registration-workflow
394 registrationFlags = mkOption {
395 type = types.listOf types.str;
397 example = [ "--docker-helper-image my/gitlab-runner-helper" ];
399 Extra command-line flags passed to
400 `gitlab-runner register`.
401 Execute `gitlab-runner register --help`
402 for a list of supported flags.
405 environmentVariables = mkOption {
406 type = types.attrsOf types.str;
408 example = { NAME = "value"; };
410 Custom environment variables injected to build environment.
411 For secrets you can use {option}`registrationConfigFile`
412 with `RUNNER_ENV` variable set.
415 description = mkOption {
416 type = types.nullOr types.str;
419 Name/description of the runner.
422 executor = mkOption {
426 Select executor, eg. shell, docker, etc.
427 See [runner documentation](https://docs.gitlab.com/runner/executors/README.html) for more information.
430 buildsDir = mkOption {
431 type = types.nullOr types.path;
433 example = "/var/lib/gitlab-runner/builds";
435 Absolute path to a directory where builds will be stored
436 in context of selected executor (Locally, Docker, SSH).
439 cloneUrl = mkOption {
440 type = types.nullOr types.str;
442 example = "http://gitlab.example.local";
444 Overwrite the URL for the GitLab instance. Used if the Runner can’t connect to GitLab on the URL GitLab exposes itself.
447 dockerImage = mkOption {
448 type = types.nullOr types.str;
451 Docker image to be used.
454 dockerVolumes = mkOption {
455 type = types.listOf types.str;
457 example = [ "/var/run/docker.sock:/var/run/docker.sock" ];
459 Bind-mount a volume and create it
460 if it doesn't exist prior to mounting.
463 dockerDisableCache = mkOption {
467 Disable all container caching.
470 dockerPrivileged = mkOption {
474 Give extended privileges to container.
477 dockerExtraHosts = mkOption {
478 type = types.listOf types.str;
480 example = [ "other-host:127.0.0.1" ];
482 Add a custom host-to-IP mapping.
485 dockerAllowedImages = mkOption {
486 type = types.listOf types.str;
488 example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ];
490 Whitelist allowed images.
493 dockerAllowedServices = mkOption {
494 type = types.listOf types.str;
496 example = [ "postgres:9" "redis:*" "mysql:*" ];
498 Whitelist allowed services.
501 preGetSourcesScript = mkOption {
502 type = types.nullOr types.path;
505 Runner-specific command script executed before code is pulled.
508 postGetSourcesScript = mkOption {
509 type = types.nullOr types.path;
512 Runner-specific command script executed after code is pulled.
515 preBuildScript = mkOption {
516 type = types.nullOr types.path;
519 Runner-specific command script executed after code is pulled,
520 just before build executes.
523 postBuildScript = mkOption {
524 type = types.nullOr types.path;
527 Runner-specific command script executed after code is pulled
528 and just after build executes.
532 type = types.listOf types.str;
537 This option has no effect for runners registered with an runner
538 authentication tokens and will be ignored.
541 runUntagged = mkOption {
545 Register to run untagged builds; defaults to
546 `true` when {option}`tagList` is empty.
548 This option has no effect for runners registered with an runner
549 authentication tokens and will be ignored.
556 Limit how many jobs can be handled concurrently by this service.
557 0 (default) simply means don't limit.
560 requestConcurrency = mkOption {
564 Limit number of concurrent requests for new jobs from GitLab.
567 maximumTimeout = mkOption {
571 What is the maximum timeout (in seconds) that will be set for
572 job when using this Runner. 0 (default) simply means don't limit.
574 This option has no effect for runners registered with an runner
575 authentication tokens and will be ignored.
578 protected = mkOption {
582 When set to true Runner will only run on pipelines
583 triggered on protected branches.
585 This option has no effect for runners registered with an runner
586 authentication tokens and will be ignored.
589 debugTraceDisabled = mkOption {
593 When set to true Runner will disable the possibility of
594 using the `CI_DEBUG_TRACE` feature.
600 clear-docker-cache = {
605 Whether to periodically prune gitlab runner's Docker resources. If
606 enabled, a systemd timer will run {command}`clear-docker-cache` as
607 specified by the `dates` option.
612 type = types.listOf types.str;
614 example = [ "prune" ];
616 Any additional flags passed to {command}`clear-docker-cache`.
624 Specification (in the format described by
625 {manpage}`systemd.time(7)`) of the time at
626 which the prune will occur.
631 default = config.virtualisation.docker.package;
632 defaultText = literalExpression "config.virtualisation.docker.package";
633 example = literalExpression "pkgs.docker";
634 description = "Docker package to use for clearing up docker cache.";
638 config = mkIf cfg.enable {
640 mapAttrsToList (name: serviceConfig: {
641 assertion = serviceConfig.registrationConfigFile == null || serviceConfig.authenticationTokenConfigFile == null;
642 message = "`services.gitlab-runner.${name}.registrationConfigFile` and `services.gitlab-runner.services.${name}.authenticationTokenConfigFile` are mutually exclusive.";
647 (name: serviceConfig: "services.gitlab-runner.services.${name}.`registrationConfigFile` points to a file in Nix Store. You should use quoted absolute path to prevent this.")
648 (filterAttrs (name: serviceConfig: isStorePath serviceConfig.registrationConfigFile) cfg.services)
650 (name: serviceConfig: "services.gitlab-runner.services.${name}.`authenticationTokenConfigFile` points to a file in Nix Store. You should use quoted absolute path to prevent this.")
651 (filterAttrs (name: serviceConfig: isStorePath serviceConfig.authenticationTokenConfigFile) cfg.services)
653 (name: serviceConfig: ''
654 Runner registration tokens have been deprecated and disabled by default in GitLab >= 17.0.
655 Consider migrating to runner authentication tokens by setting `services.gitlab-runner.services.${name}.authenticationTokenConfigFile`.
656 https://docs.gitlab.com/17.0/ee/ci/runners/new_creation_workflow.html''
659 filterAttrs (name: serviceConfig:
660 serviceConfig.authenticationTokenConfigFile == null
664 (name: serviceConfig: ''
665 `services.gitlab-runner.services.${name}.protected` with runner authentication tokens has no effect and will be ignored. Please remove it from your configuration.''
668 filterAttrs (name: serviceConfig:
669 serviceConfig.authenticationTokenConfigFile != null && serviceConfig.protected == true
673 (name: serviceConfig: ''
674 `services.gitlab-runner.services.${name}.runUntagged` with runner authentication tokens has no effect and will be ignored. Please remove it from your configuration.''
677 filterAttrs (name: serviceConfig:
678 serviceConfig.authenticationTokenConfigFile != null && serviceConfig.runUntagged == true
683 `services.gitlab-runner.services.${name}.maximumTimeout` with runner authentication tokens has no effect and will be ignored. Please remove it from your configuration.''
686 filterAttrs (name: serviceConfig:
687 serviceConfig.authenticationTokenConfigFile != null && serviceConfig.maximumTimeout != 0
692 `services.gitlab-runner.services.${name}.tagList` with runner authentication tokens has no effect and will be ignored. Please remove it from your configuration.''
695 filterAttrs (serviceName: serviceConfig:
696 serviceConfig.authenticationTokenConfigFile != null && serviceConfig.tagList != [ ]
701 environment.systemPackages = [ cfg.package ];
702 systemd.services.gitlab-runner = {
703 description = "Gitlab Runner";
704 documentation = [ "https://docs.gitlab.com/runner/" ];
705 after = [ "network.target" ]
706 ++ optional hasDocker "docker.service"
707 ++ optional hasPodman "podman.service";
709 requires = optional hasDocker "docker.service"
710 ++ optional hasPodman "podman.service";
711 wantedBy = [ "multi-user.target" ];
712 environment = config.networking.proxy.envVars // {
713 HOME = "/var/lib/gitlab-runner";
726 ++ cfg.extraPackages;
728 reloadIfChanged = true;
730 # Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig`
731 # to `lib.mkForce false` in your configuration to run this service as root.
732 # You can also set `User` and `Group` options to run this service as desired user.
733 # Make sure to restart service or changes won't apply.
735 StateDirectory = "gitlab-runner";
736 SupplementaryGroups = optional hasDocker "docker"
737 ++ optional hasPodman "podman";
738 ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
739 ExecStart = "${startScript}/bin/gitlab-runner-start";
740 ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
741 } // optionalAttrs cfg.gracefulTermination {
742 TimeoutStopSec = "${cfg.gracefulTimeout}";
743 KillSignal = "SIGQUIT";
744 KillMode = "process";
747 # Enable periodic clear-docker-cache script
748 systemd.services.gitlab-runner-clear-docker-cache = mkIf (cfg.clear-docker-cache.enable && (any (s: s.executor == "docker") (attrValues cfg.services))) {
749 description = "Prune gitlab-runner docker resources";
750 restartIfChanged = false;
751 unitConfig.X-StopOnRemoval = false;
753 serviceConfig.Type = "oneshot";
755 path = [ cfg.clear-docker-cache.package pkgs.gawk ];
758 ${pkgs.gitlab-runner}/bin/clear-docker-cache ${toString cfg.clear-docker-cache.flags}
761 startAt = cfg.clear-docker-cache.dates;
763 # Enable docker if `docker` executor is used in any service
764 virtualisation.docker.enable = mkIf (
765 any (s: s.executor == "docker") (attrValues cfg.services)
769 (mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ] )
770 (mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead" )
771 (mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner" )
773 (mkRenamedOptionModule [ "services" "gitlab-runner" "checkInterval" ] [ "services" "gitlab-runner" "settings" "check_interval" ] )
774 (mkRenamedOptionModule [ "services" "gitlab-runner" "concurrent" ] [ "services" "gitlab-runner" "settings" "concurrent" ] )
775 (mkRenamedOptionModule [ "services" "gitlab-runner" "sentryDSN" ] [ "services" "gitlab-runner" "settings" "sentry_dsn" ] )
776 (mkRenamedOptionModule [ "services" "gitlab-runner" "prometheusListenAddress" ] [ "services" "gitlab-runner" "settings" "listen_address" ] )
778 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "listenAddress" ] [ "services" "gitlab-runner" "settings" "session_server" "listen_address" ] )
779 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "advertiseAddress" ] [ "services" "gitlab-runner" "settings" "session_server" "advertise_address" ] )
780 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "sessionTimeout" ] [ "services" "gitlab-runner" "settings" "session_server" "session_timeout" ] )
783 meta.maintainers = teams.gitlab.members;