1{ config, lib, options, pkgs, utils, ... }:
2
3with lib;
4
5let
6 cfg = config.services.gitlab;
7 opt = options.services.gitlab;
8
9 toml = pkgs.formats.toml {};
10 yaml = pkgs.formats.yaml {};
11
12 postgresqlPackage = if config.services.postgresql.enable then
13 config.services.postgresql.package
14 else
15 pkgs.postgresql_13;
16
17 gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
18 gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
19 pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url;
20
21 gitlabVersionAtLeast = version: lib.versionAtLeast (lib.getVersion cfg.packages.gitlab) version;
22
23 databaseConfig = let
24 val = {
25 adapter = "postgresql";
26 database = cfg.databaseName;
27 host = cfg.databaseHost;
28 username = cfg.databaseUsername;
29 encoding = "utf8";
30 pool = cfg.databasePool;
31 } // cfg.extraDatabaseConfig;
32 in {
33 production = (
34 if (gitlabVersionAtLeast "15.0")
35 then { main = val; }
36 else val
37 ) // lib.optionalAttrs (gitlabVersionAtLeast "15.9") {
38 ci = val // {
39 database_tasks = false;
40 };
41 };
42 };
43
44 # We only want to create a database if we're actually going to connect to it.
45 databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "";
46
47 gitalyToml = pkgs.writeText "gitaly.toml" ''
48 socket_path = "${lib.escape ["\""] gitalySocket}"
49 runtime_dir = "/run/gitaly"
50 bin_dir = "${cfg.packages.gitaly}/bin"
51 prometheus_listen_addr = "localhost:9236"
52
53 [git]
54 bin_path = "${pkgs.git}/bin/git"
55
56 [gitlab-shell]
57 dir = "${cfg.packages.gitlab-shell}"
58
59 [hooks]
60 custom_hooks_dir = "${cfg.statePath}/custom_hooks"
61
62 [gitlab]
63 secret_file = "${cfg.statePath}/gitlab_shell_secret"
64 url = "http+unix://${pathUrlQuote gitlabSocket}"
65
66 [gitlab.http-settings]
67 self_signed_cert = false
68
69 ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: ''
70 [[storage]]
71 name = "${lib.escape ["\""] k}"
72 path = "${lib.escape ["\""] v.path}"
73 '') gitlabConfig.production.repositories.storages))}
74 '';
75
76 gitlabShellConfig = flip recursiveUpdate cfg.extraShellConfig {
77 user = cfg.user;
78 gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}";
79 http_settings.self_signed_cert = false;
80 repos_path = "${cfg.statePath}/repositories";
81 secret_file = "${cfg.statePath}/gitlab_shell_secret";
82 log_file = "${cfg.statePath}/log/gitlab-shell.log";
83 };
84
85 redisConfig.production.url = cfg.redisUrl;
86
87 cableYml = yaml.generate "cable.yml" {
88 production = {
89 adapter = "redis";
90 url = cfg.redisUrl;
91 channel_prefix = "gitlab_production";
92 };
93 };
94
95 # Redis configuration file
96 resqueYml = pkgs.writeText "resque.yml" (builtins.toJSON redisConfig);
97
98 gitlabConfig = {
99 # These are the default settings from config/gitlab.example.yml
100 production = flip recursiveUpdate cfg.extraConfig {
101 gitlab = {
102 host = cfg.host;
103 port = cfg.port;
104 https = cfg.https;
105 user = cfg.user;
106 email_enabled = true;
107 email_display_name = "GitLab";
108 email_reply_to = "noreply@localhost";
109 default_theme = 2;
110 default_projects_features = {
111 issues = true;
112 merge_requests = true;
113 wiki = true;
114 snippets = true;
115 builds = true;
116 container_registry = true;
117 };
118 };
119 repositories.storages.default.path = "${cfg.statePath}/repositories";
120 repositories.storages.default.gitaly_address = "unix:${gitalySocket}";
121 artifacts.enabled = true;
122 lfs.enabled = true;
123 gravatar.enabled = true;
124 cron_jobs = { };
125 gitlab_ci.builds_path = "${cfg.statePath}/builds";
126 ldap.enabled = false;
127 omniauth.enabled = false;
128 shared.path = "${cfg.statePath}/shared";
129 gitaly.client_path = "${cfg.packages.gitaly}/bin";
130 backup = {
131 gitaly_backup_path = "${cfg.packages.gitaly}/bin/gitaly-backup";
132 path = cfg.backup.path;
133 keep_time = cfg.backup.keepTime;
134 } // (optionalAttrs (cfg.backup.uploadOptions != {}) {
135 upload = cfg.backup.uploadOptions;
136 });
137 gitlab_shell = {
138 path = "${cfg.packages.gitlab-shell}";
139 hooks_path = "${cfg.statePath}/shell/hooks";
140 secret_file = "${cfg.statePath}/gitlab_shell_secret";
141 upload_pack = true;
142 receive_pack = true;
143 };
144 workhorse.secret_file = "${cfg.statePath}/.gitlab_workhorse_secret";
145 gitlab_kas.secret_file = "${cfg.statePath}/.gitlab_kas_secret";
146 git.bin_path = "git";
147 monitoring = {
148 ip_whitelist = [ "127.0.0.0/8" "::1/128" ];
149 sidekiq_exporter = {
150 enable = true;
151 address = "localhost";
152 port = 3807;
153 };
154 };
155 registry = lib.optionalAttrs cfg.registry.enable {
156 enabled = true;
157 host = cfg.registry.externalAddress;
158 port = cfg.registry.externalPort;
159 key = cfg.registry.keyFile;
160 api_url = "http://${config.services.dockerRegistry.listenAddress}:${toString config.services.dockerRegistry.port}/";
161 issuer = cfg.registry.issuer;
162 };
163 elasticsearch.indexer_path = "${pkgs.gitlab-elasticsearch-indexer}/bin/gitlab-elasticsearch-indexer";
164 extra = {};
165 uploads.storage_path = cfg.statePath;
166 pages = optionalAttrs cfg.pages.enable {
167 enabled = cfg.pages.enable;
168 port = 8090;
169 host = cfg.pages.settings.pages-domain;
170 secret_file = cfg.pages.settings.api-secret-key;
171 };
172 };
173 };
174
175 gitlabEnv = cfg.packages.gitlab.gitlabEnv // {
176 HOME = "${cfg.statePath}/home";
177 PUMA_PATH = "${cfg.statePath}/";
178 GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/";
179 SCHEMA = "${cfg.statePath}/db/structure.sql";
180 GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
181 GITLAB_LOG_PATH = "${cfg.statePath}/log";
182 prometheus_multiproc_dir = "/run/gitlab";
183 RAILS_ENV = "production";
184 MALLOC_ARENA_MAX = "2";
185 } // cfg.extraEnv;
186
187 runtimeDeps = with pkgs; [
188 nodejs
189 gzip
190 git
191 gnutar
192 postgresqlPackage
193 coreutils
194 procps
195 findutils # Needed for gitlab:cleanup:orphan_job_artifact_files
196 ];
197
198 gitlab-rake = pkgs.stdenv.mkDerivation {
199 name = "gitlab-rake";
200 nativeBuildInputs = [ pkgs.makeWrapper ];
201 dontBuild = true;
202 dontUnpack = true;
203 installPhase = ''
204 mkdir -p $out/bin
205 makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rake $out/bin/gitlab-rake \
206 ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
207 --set PATH '${lib.makeBinPath runtimeDeps}:$PATH' \
208 --set RAKEOPT '-f ${cfg.packages.gitlab}/share/gitlab/Rakefile' \
209 --chdir '${cfg.packages.gitlab}/share/gitlab'
210 '';
211 };
212
213 gitlab-rails = pkgs.stdenv.mkDerivation {
214 name = "gitlab-rails";
215 nativeBuildInputs = [ pkgs.makeWrapper ];
216 dontBuild = true;
217 dontUnpack = true;
218 installPhase = ''
219 mkdir -p $out/bin
220 makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rails $out/bin/gitlab-rails \
221 ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
222 --set PATH '${lib.makeBinPath runtimeDeps}:$PATH' \
223 --chdir '${cfg.packages.gitlab}/share/gitlab'
224 '';
225 };
226
227 extraGitlabRb = pkgs.writeText "extra-gitlab.rb" cfg.extraGitlabRb;
228
229 smtpSettings = pkgs.writeText "gitlab-smtp-settings.rb" ''
230 if Rails.env.production?
231 Rails.application.config.action_mailer.delivery_method = :smtp
232
233 ActionMailer::Base.delivery_method = :smtp
234 ActionMailer::Base.smtp_settings = {
235 address: "${cfg.smtp.address}",
236 port: ${toString cfg.smtp.port},
237 ${optionalString (cfg.smtp.username != null) ''user_name: "${cfg.smtp.username}",''}
238 ${optionalString (cfg.smtp.passwordFile != null) ''password: "@smtpPassword@",''}
239 domain: "${cfg.smtp.domain}",
240 ${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"}
241 enable_starttls_auto: ${boolToString cfg.smtp.enableStartTLSAuto},
242 tls: ${boolToString cfg.smtp.tls},
243 ca_file: "/etc/ssl/certs/ca-certificates.crt",
244 openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}'
245 }
246 end
247 '';
248
249in {
250
251 imports = [
252 (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
253 (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
254 (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
255 (mkRemovedOptionModule [ "services" "gitlab" "logrotate" "extraConfig" ] "Modify services.logrotate.settings.gitlab directly instead")
256 (mkRemovedOptionModule [ "services" "gitlab" "pagesExtraArgs" ] "Use services.gitlab.pages.settings instead")
257 ];
258
259 options = {
260 services.gitlab = {
261 enable = mkOption {
262 type = types.bool;
263 default = false;
264 description = ''
265 Enable the gitlab service.
266 '';
267 };
268
269 packages.gitlab = mkPackageOption pkgs "gitlab" {
270 example = "gitlab-ee";
271 };
272
273 packages.gitlab-shell = mkPackageOption pkgs "gitlab-shell" { };
274
275 packages.gitlab-workhorse = mkPackageOption pkgs "gitlab-workhorse" { };
276
277 packages.gitaly = mkPackageOption pkgs "gitaly" { };
278
279 packages.pages = mkPackageOption pkgs "gitlab-pages" { };
280
281 statePath = mkOption {
282 type = types.str;
283 default = "/var/gitlab/state";
284 description = ''
285 GitLab state directory. Configuration, repositories and
286 logs, among other things, are stored here.
287
288 The directory will be created automatically if it doesn't
289 exist already. Its parent directories must be owned by
290 either `root` or the user set in
291 {option}`services.gitlab.user`.
292 '';
293 };
294
295 extraEnv = mkOption {
296 type = types.attrsOf types.str;
297 default = {};
298 description = ''
299 Additional environment variables for the GitLab environment.
300 '';
301 };
302
303 backup.startAt = mkOption {
304 type = with types; either str (listOf str);
305 default = [];
306 example = "03:00";
307 description = ''
308 The time(s) to run automatic backup of GitLab
309 state. Specified in systemd's time format; see
310 {manpage}`systemd.time(7)`.
311 '';
312 };
313
314 backup.path = mkOption {
315 type = types.str;
316 default = cfg.statePath + "/backup";
317 defaultText = literalExpression ''config.${opt.statePath} + "/backup"'';
318 description = "GitLab path for backups.";
319 };
320
321 backup.keepTime = mkOption {
322 type = types.int;
323 default = 0;
324 example = 48;
325 apply = x: x * 60 * 60;
326 description = ''
327 How long to keep the backups around, in
328 hours. `0` means “keep forever”.
329 '';
330 };
331
332 backup.skip = mkOption {
333 type = with types;
334 let value = enum [
335 "db"
336 "uploads"
337 "builds"
338 "artifacts"
339 "lfs"
340 "registry"
341 "pages"
342 "repositories"
343 "tar"
344 ];
345 in
346 either value (listOf value);
347 default = [];
348 example = [ "artifacts" "lfs" ];
349 apply = x: if isString x then x else concatStringsSep "," x;
350 description = ''
351 Directories to exclude from the backup. The example excludes
352 CI artifacts and LFS objects from the backups. The
353 `tar` option skips the creation of a tar
354 file.
355
356 Refer to <https://docs.gitlab.com/ee/raketasks/backup_restore.html#excluding-specific-directories-from-the-backup>
357 for more information.
358 '';
359 };
360
361 backup.uploadOptions = mkOption {
362 type = types.attrs;
363 default = {};
364 example = literalExpression ''
365 {
366 # Fog storage connection settings, see http://fog.io/storage/
367 connection = {
368 provider = "AWS";
369 region = "eu-north-1";
370 aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX";
371 aws_secret_access_key = { _secret = config.deployment.keys.aws_access_key.path; };
372 };
373
374 # The remote 'directory' to store your backups in.
375 # For S3, this would be the bucket name.
376 remote_directory = "my-gitlab-backups";
377
378 # Use multipart uploads when file size reaches 100MB, see
379 # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html
380 multipart_chunk_size = 104857600;
381
382 # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
383 encryption = "AES256";
384
385 # Specifies Amazon S3 storage class to use for backups, this is optional
386 storage_class = "STANDARD";
387 };
388 '';
389 description = ''
390 GitLab automatic upload specification. Tells GitLab to
391 upload the backup to a remote location when done.
392
393 Attributes specified here are added under
394 `production -> backup -> upload` in
395 {file}`config/gitlab.yml`.
396 '';
397 };
398
399 databaseHost = mkOption {
400 type = types.str;
401 default = "";
402 description = ''
403 GitLab database hostname. An empty string means
404 “use local unix socket connection”.
405 '';
406 };
407
408 databasePasswordFile = mkOption {
409 type = with types; nullOr path;
410 default = null;
411 description = ''
412 File containing the GitLab database user password.
413
414 This should be a string, not a nix path, since nix paths are
415 copied into the world-readable nix store.
416 '';
417 };
418
419 databaseCreateLocally = mkOption {
420 type = types.bool;
421 default = true;
422 description = ''
423 Whether a database should be automatically created on the
424 local host. Set this to `false` if you plan
425 on provisioning a local database yourself. This has no effect
426 if {option}`services.gitlab.databaseHost` is customized.
427 '';
428 };
429
430 databaseName = mkOption {
431 type = types.str;
432 default = "gitlab";
433 description = "GitLab database name.";
434 };
435
436 databaseUsername = mkOption {
437 type = types.str;
438 default = "gitlab";
439 description = "GitLab database user.";
440 };
441
442 databasePool = mkOption {
443 type = types.int;
444 default = 5;
445 description = "Database connection pool size.";
446 };
447
448 extraDatabaseConfig = mkOption {
449 type = types.attrs;
450 default = {};
451 description = "Extra configuration in config/database.yml.";
452 };
453
454 redisUrl = mkOption {
455 type = types.str;
456 default = "unix:/run/gitlab/redis.sock";
457 example = "redis://localhost:6379/";
458 description = "Redis URL for all GitLab services.";
459 };
460
461 extraGitlabRb = mkOption {
462 type = types.str;
463 default = "";
464 example = ''
465 if Rails.env.production?
466 Rails.application.config.action_mailer.delivery_method = :sendmail
467 ActionMailer::Base.delivery_method = :sendmail
468 ActionMailer::Base.sendmail_settings = {
469 location: "/run/wrappers/bin/sendmail",
470 arguments: "-i -t"
471 }
472 end
473 '';
474 description = ''
475 Extra configuration to be placed in config/extra-gitlab.rb. This can
476 be used to add configuration not otherwise exposed through this module's
477 options.
478 '';
479 };
480
481 host = mkOption {
482 type = types.str;
483 default = config.networking.hostName;
484 defaultText = literalExpression "config.networking.hostName";
485 description = "GitLab host name. Used e.g. for copy-paste URLs.";
486 };
487
488 port = mkOption {
489 type = types.port;
490 default = 8080;
491 description = ''
492 GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're
493 service over https.
494 '';
495 };
496
497 https = mkOption {
498 type = types.bool;
499 default = false;
500 description = "Whether gitlab prints URLs with https as scheme.";
501 };
502
503 user = mkOption {
504 type = types.str;
505 default = "gitlab";
506 description = "User to run gitlab and all related services.";
507 };
508
509 group = mkOption {
510 type = types.str;
511 default = "gitlab";
512 description = "Group to run gitlab and all related services.";
513 };
514
515 initialRootEmail = mkOption {
516 type = types.str;
517 default = "admin@local.host";
518 description = ''
519 Initial email address of the root account if this is a new install.
520 '';
521 };
522
523 initialRootPasswordFile = mkOption {
524 type = with types; nullOr path;
525 default = null;
526 description = ''
527 File containing the initial password of the root account if
528 this is a new install.
529
530 This should be a string, not a nix path, since nix paths are
531 copied into the world-readable nix store.
532 '';
533 };
534
535 registry = {
536 enable = mkOption {
537 type = types.bool;
538 default = false;
539 description = "Enable GitLab container registry.";
540 };
541 package = mkOption {
542 type = types.package;
543 default =
544 if versionAtLeast config.system.stateVersion "23.11"
545 then pkgs.gitlab-container-registry
546 else pkgs.docker-distribution;
547 defaultText = literalExpression "pkgs.docker-distribution";
548 description = ''
549 Container registry package to use.
550
551 External container registries such as `pkgs.docker-distribution` are not supported
552 anymore since GitLab 16.0.0.
553 '';
554 };
555 host = mkOption {
556 type = types.str;
557 default = config.services.gitlab.host;
558 defaultText = literalExpression "config.services.gitlab.host";
559 description = "GitLab container registry host name.";
560 };
561 port = mkOption {
562 type = types.port;
563 default = 4567;
564 description = "GitLab container registry port.";
565 };
566 certFile = mkOption {
567 type = types.path;
568 description = "Path to GitLab container registry certificate.";
569 };
570 keyFile = mkOption {
571 type = types.path;
572 description = "Path to GitLab container registry certificate-key.";
573 };
574 defaultForProjects = mkOption {
575 type = types.bool;
576 default = cfg.registry.enable;
577 defaultText = literalExpression "config.${opt.registry.enable}";
578 description = "If GitLab container registry should be enabled by default for projects.";
579 };
580 issuer = mkOption {
581 type = types.str;
582 default = "gitlab-issuer";
583 description = "GitLab container registry issuer.";
584 };
585 serviceName = mkOption {
586 type = types.str;
587 default = "container_registry";
588 description = "GitLab container registry service name.";
589 };
590 externalAddress = mkOption {
591 type = types.str;
592 default = "";
593 description = "External address used to access registry from the internet";
594 };
595 externalPort = mkOption {
596 type = types.int;
597 description = "External port used to access registry from the internet";
598 };
599 };
600
601 smtp = {
602 enable = mkOption {
603 type = types.bool;
604 default = false;
605 description = "Enable gitlab mail delivery over SMTP.";
606 };
607
608 address = mkOption {
609 type = types.str;
610 default = "localhost";
611 description = "Address of the SMTP server for GitLab.";
612 };
613
614 port = mkOption {
615 type = types.port;
616 default = 25;
617 description = "Port of the SMTP server for GitLab.";
618 };
619
620 username = mkOption {
621 type = with types; nullOr str;
622 default = null;
623 description = "Username of the SMTP server for GitLab.";
624 };
625
626 passwordFile = mkOption {
627 type = types.nullOr types.path;
628 default = null;
629 description = ''
630 File containing the password of the SMTP server for GitLab.
631
632 This should be a string, not a nix path, since nix paths
633 are copied into the world-readable nix store.
634 '';
635 };
636
637 domain = mkOption {
638 type = types.str;
639 default = "localhost";
640 description = "HELO domain to use for outgoing mail.";
641 };
642
643 authentication = mkOption {
644 type = with types; nullOr str;
645 default = null;
646 description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
647 };
648
649 enableStartTLSAuto = mkOption {
650 type = types.bool;
651 default = true;
652 description = "Whether to try to use StartTLS.";
653 };
654
655 tls = mkOption {
656 type = types.bool;
657 default = false;
658 description = "Whether to use TLS wrapper-mode.";
659 };
660
661 opensslVerifyMode = mkOption {
662 type = types.str;
663 default = "peer";
664 description = "How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
665 };
666 };
667
668 pages.enable = mkEnableOption "the GitLab Pages service";
669
670 pages.settings = mkOption {
671 example = literalExpression ''
672 {
673 pages-domain = "example.com";
674 auth-client-id = "generated-id-xxxxxxx";
675 auth-client-secret = { _secret = "/var/keys/auth-client-secret"; };
676 auth-redirect-uri = "https://projects.example.com/auth";
677 auth-secret = { _secret = "/var/keys/auth-secret"; };
678 auth-server = "https://gitlab.example.com";
679 }
680 '';
681
682 description = ''
683 Configuration options to set in the GitLab Pages config
684 file.
685
686 Options containing secret data should be set to an attribute
687 set containing the attribute `_secret` - a string pointing
688 to a file containing the value the option should be set
689 to. See the example to get a better picture of this: in the
690 resulting configuration file, the `auth-client-secret` and
691 `auth-secret` keys will be set to the contents of the
692 {file}`/var/keys/auth-client-secret` and
693 {file}`/var/keys/auth-secret` files respectively.
694 '';
695
696 type = types.submodule {
697 freeformType = with types; attrsOf (nullOr (oneOf [ str int bool attrs ]));
698
699 options = {
700 listen-http = mkOption {
701 type = with types; listOf str;
702 apply = x: if x == [] then null else lib.concatStringsSep "," x;
703 default = [];
704 description = ''
705 The address(es) to listen on for HTTP requests.
706 '';
707 };
708
709 listen-https = mkOption {
710 type = with types; listOf str;
711 apply = x: if x == [] then null else lib.concatStringsSep "," x;
712 default = [];
713 description = ''
714 The address(es) to listen on for HTTPS requests.
715 '';
716 };
717
718 listen-proxy = mkOption {
719 type = with types; listOf str;
720 apply = x: if x == [] then null else lib.concatStringsSep "," x;
721 default = [ "127.0.0.1:8090" ];
722 description = ''
723 The address(es) to listen on for proxy requests.
724 '';
725 };
726
727 artifacts-server = mkOption {
728 type = with types; nullOr str;
729 default = "http${optionalString cfg.https "s"}://${cfg.host}/api/v4";
730 defaultText = "http(s)://<services.gitlab.host>/api/v4";
731 example = "https://gitlab.example.com/api/v4";
732 description = ''
733 API URL to proxy artifact requests to.
734 '';
735 };
736
737 gitlab-server = mkOption {
738 type = with types; nullOr str;
739 default = "http${optionalString cfg.https "s"}://${cfg.host}";
740 defaultText = "http(s)://<services.gitlab.host>";
741 example = "https://gitlab.example.com";
742 description = ''
743 Public GitLab server URL.
744 '';
745 };
746
747 internal-gitlab-server = mkOption {
748 type = with types; nullOr str;
749 default = null;
750 defaultText = "http(s)://<services.gitlab.host>";
751 example = "https://gitlab.example.internal";
752 description = ''
753 Internal GitLab server used for API requests, useful
754 if you want to send that traffic over an internal load
755 balancer. By default, the value of
756 `services.gitlab.pages.settings.gitlab-server` is
757 used.
758 '';
759 };
760
761 api-secret-key = mkOption {
762 type = with types; nullOr str;
763 default = "${cfg.statePath}/gitlab_pages_secret";
764 internal = true;
765 description = ''
766 File with secret key used to authenticate with the
767 GitLab API.
768 '';
769 };
770
771 pages-domain = mkOption {
772 type = with types; nullOr str;
773 example = "example.com";
774 description = ''
775 The domain to serve static pages on.
776 '';
777 };
778
779 pages-root = mkOption {
780 type = types.str;
781 default = "${gitlabConfig.production.shared.path}/pages";
782 defaultText = literalExpression ''config.${opt.extraConfig}.production.shared.path + "/pages"'';
783 description = ''
784 The directory where pages are stored.
785 '';
786 };
787 };
788 };
789 };
790
791 secrets.secretFile = mkOption {
792 type = with types; nullOr path;
793 default = null;
794 description = ''
795 A file containing the secret used to encrypt variables in
796 the DB. If you change or lose this key you will be unable to
797 access variables stored in database.
798
799 Make sure the secret is at least 32 characters and all random,
800 no regular words or you'll be exposed to dictionary attacks.
801
802 This should be a string, not a nix path, since nix paths are
803 copied into the world-readable nix store.
804 '';
805 };
806
807 secrets.dbFile = mkOption {
808 type = with types; nullOr path;
809 default = null;
810 description = ''
811 A file containing the secret used to encrypt variables in
812 the DB. If you change or lose this key you will be unable to
813 access variables stored in database.
814
815 Make sure the secret is at least 32 characters and all random,
816 no regular words or you'll be exposed to dictionary attacks.
817
818 This should be a string, not a nix path, since nix paths are
819 copied into the world-readable nix store.
820 '';
821 };
822
823 secrets.otpFile = mkOption {
824 type = with types; nullOr path;
825 default = null;
826 description = ''
827 A file containing the secret used to encrypt secrets for OTP
828 tokens. If you change or lose this key, users which have 2FA
829 enabled for login won't be able to login anymore.
830
831 Make sure the secret is at least 32 characters and all random,
832 no regular words or you'll be exposed to dictionary attacks.
833
834 This should be a string, not a nix path, since nix paths are
835 copied into the world-readable nix store.
836 '';
837 };
838
839 secrets.jwsFile = mkOption {
840 type = with types; nullOr path;
841 default = null;
842 description = ''
843 A file containing the secret used to encrypt session
844 keys. If you change or lose this key, users will be
845 disconnected.
846
847 Make sure the secret is an RSA private key in PEM format. You can
848 generate one with
849
850 openssl genrsa 2048
851
852 This should be a string, not a nix path, since nix paths are
853 copied into the world-readable nix store.
854 '';
855 };
856
857 extraShellConfig = mkOption {
858 type = types.attrs;
859 default = {};
860 description = "Extra configuration to merge into shell-config.yml";
861 };
862
863 puma.workers = mkOption {
864 type = types.int;
865 default = 2;
866 apply = x: builtins.toString x;
867 description = ''
868 The number of worker processes Puma should spawn. This
869 controls the amount of parallel Ruby code can be
870 executed. GitLab recommends `Number of CPU cores - 1`, but at least two.
871
872 ::: {.note}
873 Each worker consumes quite a bit of memory, so
874 be careful when increasing this.
875 :::
876 '';
877 };
878
879 puma.threadsMin = mkOption {
880 type = types.int;
881 default = 0;
882 apply = x: builtins.toString x;
883 description = ''
884 The minimum number of threads Puma should use per
885 worker.
886
887 ::: {.note}
888 Each thread consumes memory and contributes to Global VM
889 Lock contention, so be careful when increasing this.
890 :::
891 '';
892 };
893
894 puma.threadsMax = mkOption {
895 type = types.int;
896 default = 4;
897 apply = x: builtins.toString x;
898 description = ''
899 The maximum number of threads Puma should use per
900 worker. This limits how many threads Puma will automatically
901 spawn in response to requests. In contrast to workers,
902 threads will never be able to run Ruby code in parallel, but
903 give higher IO parallelism.
904
905 ::: {.note}
906 Each thread consumes memory and contributes to Global VM
907 Lock contention, so be careful when increasing this.
908 :::
909 '';
910 };
911
912 sidekiq.concurrency = mkOption {
913 type = with types; nullOr int;
914 default = null;
915 description = ''
916 How many processor threads to use for processing sidekiq background job queues. When null, the GitLab default is used.
917
918 See <https://docs.gitlab.com/ee/administration/sidekiq/extra_sidekiq_processes.html#manage-thread-counts-explicitly> for details.
919 '';
920 };
921
922 sidekiq.memoryKiller.enable = mkOption {
923 type = types.bool;
924 default = true;
925 description = ''
926 Whether the Sidekiq MemoryKiller should be turned
927 on. MemoryKiller kills Sidekiq when its memory consumption
928 exceeds a certain limit.
929
930 See <https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html>
931 for details.
932 '';
933 };
934
935 sidekiq.memoryKiller.maxMemory = mkOption {
936 type = types.int;
937 default = 2000;
938 apply = x: builtins.toString (x * 1024);
939 description = ''
940 The maximum amount of memory, in MiB, a Sidekiq worker is
941 allowed to consume before being killed.
942 '';
943 };
944
945 sidekiq.memoryKiller.graceTime = mkOption {
946 type = types.int;
947 default = 900;
948 apply = x: builtins.toString x;
949 description = ''
950 The time MemoryKiller waits after noticing excessive memory
951 consumption before killing Sidekiq.
952 '';
953 };
954
955 sidekiq.memoryKiller.shutdownWait = mkOption {
956 type = types.int;
957 default = 30;
958 apply = x: builtins.toString x;
959 description = ''
960 The time allowed for all jobs to finish before Sidekiq is
961 killed forcefully.
962 '';
963 };
964
965 logrotate = {
966 enable = mkOption {
967 type = types.bool;
968 default = true;
969 description = ''
970 Enable rotation of log files.
971 '';
972 };
973
974 frequency = mkOption {
975 type = types.str;
976 default = "daily";
977 description = "How often to rotate the logs.";
978 };
979
980 keep = mkOption {
981 type = types.int;
982 default = 30;
983 description = "How many rotations to keep.";
984 };
985 };
986
987 workhorse.config = mkOption {
988 type = toml.type;
989 default = {};
990 example = literalExpression ''
991 {
992 object_storage.provider = "AWS";
993 object_storage.s3 = {
994 aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX";
995 aws_secret_access_key = { _secret = "/var/keys/aws_secret_access_key"; };
996 };
997 };
998 '';
999 description = ''
1000 Configuration options to add to Workhorse's configuration
1001 file.
1002
1003 See
1004 <https://gitlab.com/gitlab-org/gitlab/-/blob/master/workhorse/config.toml.example>
1005 and
1006 <https://docs.gitlab.com/ee/development/workhorse/configuration.html>
1007 for examples and option documentation.
1008
1009 Options containing secret data should be set to an attribute
1010 set containing the attribute `_secret` - a string pointing
1011 to a file containing the value the option should be set
1012 to. See the example to get a better picture of this: in the
1013 resulting configuration file, the
1014 `object_storage.s3.aws_secret_access_key` key will be set to
1015 the contents of the {file}`/var/keys/aws_secret_access_key`
1016 file.
1017 '';
1018 };
1019
1020 extraConfig = mkOption {
1021 type = yaml.type;
1022 default = {};
1023 example = literalExpression ''
1024 {
1025 gitlab = {
1026 default_projects_features = {
1027 builds = false;
1028 };
1029 };
1030 omniauth = {
1031 enabled = true;
1032 auto_sign_in_with_provider = "openid_connect";
1033 allow_single_sign_on = ["openid_connect"];
1034 block_auto_created_users = false;
1035 providers = [
1036 {
1037 name = "openid_connect";
1038 label = "OpenID Connect";
1039 args = {
1040 name = "openid_connect";
1041 scope = ["openid" "profile"];
1042 response_type = "code";
1043 issuer = "https://keycloak.example.com/auth/realms/My%20Realm";
1044 discovery = true;
1045 client_auth_method = "query";
1046 uid_field = "preferred_username";
1047 client_options = {
1048 identifier = "gitlab";
1049 secret = { _secret = "/var/keys/gitlab_oidc_secret"; };
1050 redirect_uri = "https://git.example.com/users/auth/openid_connect/callback";
1051 };
1052 };
1053 }
1054 ];
1055 };
1056 };
1057 '';
1058 description = ''
1059 Extra options to be added under
1060 `production` in
1061 {file}`config/gitlab.yml`, as a nix attribute
1062 set.
1063
1064 Options containing secret data should be set to an attribute
1065 set containing the attribute `_secret` - a
1066 string pointing to a file containing the value the option
1067 should be set to. See the example to get a better picture of
1068 this: in the resulting
1069 {file}`config/gitlab.yml` file, the
1070 `production.omniauth.providers[0].args.client_options.secret`
1071 key will be set to the contents of the
1072 {file}`/var/keys/gitlab_oidc_secret` file.
1073 '';
1074 };
1075 };
1076 };
1077
1078 config = mkIf cfg.enable {
1079 warnings = [
1080 (mkIf
1081 (cfg.registry.enable && versionAtLeast (getVersion cfg.packages.gitlab) "16.0.0" && cfg.registry.package == pkgs.docker-distribution)
1082 ''Support for container registries other than gitlab-container-registry has ended since GitLab 16.0.0 and is scheduled for removal in a future release.
1083 Please back up your data and migrate to the gitlab-container-registry package.''
1084 )
1085 (mkIf
1086 (versionAtLeast (getVersion cfg.packages.gitlab) "16.2.0" && versionOlder (getVersion cfg.packages.gitlab) "16.5.0")
1087 ''GitLab instances created or updated between versions [15.11.0, 15.11.2] have an incorrect database schema.
1088 Check the upstream documentation for a workaround: https://docs.gitlab.com/ee/update/versions/gitlab_16_changes.html#undefined-column-error-upgrading-to-162-or-later''
1089 )
1090 ];
1091
1092 assertions = [
1093 {
1094 assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.databaseUsername);
1095 message = ''For local automatic database provisioning (services.gitlab.databaseCreateLocally == true) with peer authentication (services.gitlab.databaseHost == "") to work services.gitlab.user and services.gitlab.databaseUsername must be identical.'';
1096 }
1097 {
1098 assertion = (cfg.databaseHost != "") -> (cfg.databasePasswordFile != null);
1099 message = "When services.gitlab.databaseHost is customized, services.gitlab.databasePasswordFile must be set!";
1100 }
1101 {
1102 assertion = cfg.initialRootPasswordFile != null;
1103 message = "services.gitlab.initialRootPasswordFile must be set!";
1104 }
1105 {
1106 assertion = cfg.secrets.secretFile != null;
1107 message = "services.gitlab.secrets.secretFile must be set!";
1108 }
1109 {
1110 assertion = cfg.secrets.dbFile != null;
1111 message = "services.gitlab.secrets.dbFile must be set!";
1112 }
1113 {
1114 assertion = cfg.secrets.otpFile != null;
1115 message = "services.gitlab.secrets.otpFile must be set!";
1116 }
1117 {
1118 assertion = cfg.secrets.jwsFile != null;
1119 message = "services.gitlab.secrets.jwsFile must be set!";
1120 }
1121 {
1122 assertion = versionAtLeast postgresqlPackage.version "13.6.0";
1123 message = "PostgreSQL >=13.6 is required to run GitLab 16. Follow the instructions in the manual section for upgrading PostgreSQL here: https://nixos.org/manual/nixos/stable/index.html#module-services-postgres-upgrading";
1124 }
1125 ];
1126
1127 environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ];
1128
1129 systemd.targets.gitlab = {
1130 description = "Common target for all GitLab services.";
1131 wantedBy = [ "multi-user.target" ];
1132 };
1133
1134 # Redis is required for the sidekiq queue runner.
1135 services.redis.servers.gitlab = {
1136 enable = mkDefault true;
1137 user = mkDefault cfg.user;
1138 unixSocket = mkDefault "/run/gitlab/redis.sock";
1139 unixSocketPerm = mkDefault 770;
1140 };
1141
1142 # We use postgres as the main data store.
1143 services.postgresql = optionalAttrs databaseActuallyCreateLocally {
1144 enable = true;
1145 ensureUsers = singleton { name = cfg.databaseUsername; };
1146 };
1147
1148 # Enable rotation of log files
1149 services.logrotate = {
1150 enable = cfg.logrotate.enable;
1151 settings = {
1152 gitlab = {
1153 files = "${cfg.statePath}/log/*.log";
1154 su = "${cfg.user} ${cfg.group}";
1155 frequency = cfg.logrotate.frequency;
1156 rotate = cfg.logrotate.keep;
1157 copytruncate = true;
1158 compress = true;
1159 };
1160 };
1161 };
1162
1163 # The postgresql module doesn't currently support concepts like
1164 # objects owners and extensions; for now we tack on what's needed
1165 # here.
1166 systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally {
1167 after = [ "postgresql.service" ];
1168 bindsTo = [ "postgresql.service" ];
1169 wantedBy = [ "gitlab.target" ];
1170 partOf = [ "gitlab.target" ];
1171 path = [
1172 pgsql.package
1173 pkgs.util-linux
1174 ];
1175 script = ''
1176 set -eu
1177
1178 PSQL() {
1179 psql --port=${toString pgsql.settings.port} "$@"
1180 }
1181
1182 PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
1183 current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
1184 if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
1185 PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
1186 if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then
1187 echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..."
1188 exit 1
1189 fi
1190 touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
1191 PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
1192 rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
1193 fi
1194 PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
1195 PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;"
1196 '';
1197
1198 serviceConfig = {
1199 User = pgsql.superUser;
1200 Type = "oneshot";
1201 RemainAfterExit = true;
1202 };
1203 };
1204
1205 systemd.services.gitlab-registry-cert = optionalAttrs cfg.registry.enable {
1206 path = with pkgs; [ openssl ];
1207
1208 script = ''
1209 mkdir -p $(dirname ${cfg.registry.keyFile})
1210 mkdir -p $(dirname ${cfg.registry.certFile})
1211 openssl req -nodes -newkey rsa:4096 -keyout ${cfg.registry.keyFile} -out /tmp/registry-auth.csr -subj "/CN=${cfg.registry.issuer}"
1212 openssl x509 -in /tmp/registry-auth.csr -out ${cfg.registry.certFile} -req -signkey ${cfg.registry.keyFile} -days 3650
1213 chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.keyFile})
1214 chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.certFile})
1215 chown ${cfg.user}:${cfg.group} ${cfg.registry.keyFile}
1216 chown ${cfg.user}:${cfg.group} ${cfg.registry.certFile}
1217 '';
1218
1219 unitConfig = {
1220 ConditionPathExists = "!${cfg.registry.certFile}";
1221 };
1222 };
1223
1224 # Ensure Docker Registry launches after the certificate generation job
1225 systemd.services.docker-registry = optionalAttrs cfg.registry.enable {
1226 wants = [ "gitlab-registry-cert.service" ];
1227 after = [ "gitlab-registry-cert.service" ];
1228 };
1229
1230 # Enable Docker Registry, if GitLab-Container Registry is enabled
1231 services.dockerRegistry = optionalAttrs cfg.registry.enable {
1232 enable = true;
1233 enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly
1234 package = cfg.registry.package;
1235 extraConfig = {
1236 auth.token = {
1237 realm = "http${optionalString (cfg.https == true) "s"}://${cfg.host}/jwt/auth";
1238 service = cfg.registry.serviceName;
1239 issuer = cfg.registry.issuer;
1240 rootcertbundle = cfg.registry.certFile;
1241 };
1242 };
1243 };
1244
1245 # Use postfix to send out mails.
1246 services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost");
1247
1248 users.users.${cfg.user} =
1249 { group = cfg.group;
1250 home = "${cfg.statePath}/home";
1251 shell = "${pkgs.bash}/bin/bash";
1252 uid = config.ids.uids.gitlab;
1253 };
1254
1255 users.groups.${cfg.group}.gid = config.ids.gids.gitlab;
1256
1257 systemd.tmpfiles.rules = [
1258 "d /run/gitlab 0755 ${cfg.user} ${cfg.group} -"
1259 "d ${gitlabEnv.HOME} 0750 ${cfg.user} ${cfg.group} -"
1260 "z ${gitlabEnv.HOME}/.ssh/authorized_keys 0600 ${cfg.user} ${cfg.group} -"
1261 "d ${cfg.backup.path} 0750 ${cfg.user} ${cfg.group} -"
1262 "d ${cfg.statePath} 0750 ${cfg.user} ${cfg.group} -"
1263 "d ${cfg.statePath}/builds 0750 ${cfg.user} ${cfg.group} -"
1264 "d ${cfg.statePath}/config 0750 ${cfg.user} ${cfg.group} -"
1265 "d ${cfg.statePath}/db 0750 ${cfg.user} ${cfg.group} -"
1266 "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -"
1267 "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -"
1268 "d ${cfg.statePath}/shell 0750 ${cfg.user} ${cfg.group} -"
1269 "d ${cfg.statePath}/tmp 0750 ${cfg.user} ${cfg.group} -"
1270 "d ${cfg.statePath}/tmp/pids 0750 ${cfg.user} ${cfg.group} -"
1271 "d ${cfg.statePath}/tmp/sockets 0750 ${cfg.user} ${cfg.group} -"
1272 "d ${cfg.statePath}/uploads 0700 ${cfg.user} ${cfg.group} -"
1273 "d ${cfg.statePath}/custom_hooks 0700 ${cfg.user} ${cfg.group} -"
1274 "d ${cfg.statePath}/custom_hooks/pre-receive.d 0700 ${cfg.user} ${cfg.group} -"
1275 "d ${cfg.statePath}/custom_hooks/post-receive.d 0700 ${cfg.user} ${cfg.group} -"
1276 "d ${cfg.statePath}/custom_hooks/update.d 0700 ${cfg.user} ${cfg.group} -"
1277 "d ${gitlabConfig.production.shared.path} 0750 ${cfg.user} ${cfg.group} -"
1278 "d ${gitlabConfig.production.shared.path}/artifacts 0750 ${cfg.user} ${cfg.group} -"
1279 "d ${gitlabConfig.production.shared.path}/lfs-objects 0750 ${cfg.user} ${cfg.group} -"
1280 "d ${gitlabConfig.production.shared.path}/packages 0750 ${cfg.user} ${cfg.group} -"
1281 "d ${gitlabConfig.production.shared.path}/pages 0750 ${cfg.user} ${cfg.group} -"
1282 "d ${gitlabConfig.production.shared.path}/registry 0750 ${cfg.user} ${cfg.group} -"
1283 "d ${gitlabConfig.production.shared.path}/terraform_state 0750 ${cfg.user} ${cfg.group} -"
1284 "d ${gitlabConfig.production.shared.path}/ci_secure_files 0750 ${cfg.user} ${cfg.group} -"
1285 "L+ /run/gitlab/config - - - - ${cfg.statePath}/config"
1286 "L+ /run/gitlab/log - - - - ${cfg.statePath}/log"
1287 "L+ /run/gitlab/tmp - - - - ${cfg.statePath}/tmp"
1288 "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads"
1289
1290 "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
1291 ];
1292
1293
1294 systemd.services.gitlab-config = {
1295 wantedBy = [ "gitlab.target" ];
1296 partOf = [ "gitlab.target" ];
1297 path = with pkgs; [
1298 jq
1299 openssl
1300 replace-secret
1301 git
1302 ];
1303 serviceConfig = {
1304 Type = "oneshot";
1305 User = cfg.user;
1306 Group = cfg.group;
1307 TimeoutSec = "infinity";
1308 Restart = "on-failure";
1309 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1310 RemainAfterExit = true;
1311
1312 ExecStartPre = let
1313 preStartFullPrivileges = ''
1314 set -o errexit -o pipefail -o nounset
1315 shopt -s dotglob nullglob inherit_errexit
1316
1317 chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
1318 if [[ -n "$(ls -A '${cfg.statePath}'/config/)" ]]; then
1319 chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/*
1320 fi
1321 '';
1322 in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}";
1323
1324 ExecStart = pkgs.writeShellScript "gitlab-config" ''
1325 set -o errexit -o pipefail -o nounset
1326 shopt -s inherit_errexit
1327
1328 umask u=rwx,g=rx,o=
1329
1330 cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
1331 rm -rf ${cfg.statePath}/db/*
1332 rm -f ${cfg.statePath}/lib
1333 find '${cfg.statePath}/config/' -maxdepth 1 -mindepth 1 -type d -execdir rm -rf {} \;
1334 cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
1335 cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
1336 ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
1337 ln -sf ${cableYml} ${cfg.statePath}/config/cable.yml
1338 ln -sf ${resqueYml} ${cfg.statePath}/config/resque.yml
1339
1340 ${cfg.packages.gitlab-shell}/bin/install
1341
1342 ${optionalString cfg.smtp.enable ''
1343 install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
1344 ${optionalString (cfg.smtp.passwordFile != null) ''
1345 replace-secret '@smtpPassword@' '${cfg.smtp.passwordFile}' '${cfg.statePath}/config/initializers/smtp_settings.rb'
1346 ''}
1347 ''}
1348
1349 (
1350 umask u=rwx,g=,o=
1351
1352 openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
1353 ${optionalString cfg.pages.enable ''
1354 openssl rand -base64 32 > ${cfg.pages.settings.api-secret-key}
1355 ''}
1356
1357 rm -f '${cfg.statePath}/config/database.yml'
1358
1359 ${lib.optionalString (cfg.databasePasswordFile != null) ''
1360 db_password="$(<'${cfg.databasePasswordFile}')"
1361 export db_password
1362
1363 if [[ -z "$db_password" ]]; then
1364 >&2 echo "Database password was an empty string!"
1365 exit 1
1366 fi
1367 ''}
1368
1369 # GitLab expects the `production.main` section to be the first entry in the file.
1370 jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} '{
1371 production: [
1372 ${lib.optionalString (cfg.databasePasswordFile != null) (
1373 builtins.concatStringsSep "\n " (
1374 [ ".production${lib.optionalString (gitlabVersionAtLeast "15.0") ".main"}.password = $ENV.db_password" ]
1375 ++ lib.optional (gitlabVersionAtLeast "15.9") "| .production.ci.password = $ENV.db_password"
1376 ++ [ "|" ]
1377 )
1378 )} .production
1379 | to_entries[]
1380 ]
1381 | sort_by(.key)
1382 | reverse
1383 | from_entries
1384 }' >'${cfg.statePath}/config/database.yml'
1385
1386 ${utils.genJqSecretsReplacementSnippet
1387 gitlabConfig
1388 "${cfg.statePath}/config/gitlab.yml"
1389 }
1390
1391 rm -f '${cfg.statePath}/config/secrets.yml'
1392
1393 secret="$(<'${cfg.secrets.secretFile}')"
1394 db="$(<'${cfg.secrets.dbFile}')"
1395 otp="$(<'${cfg.secrets.otpFile}')"
1396 jws="$(<'${cfg.secrets.jwsFile}')"
1397 export secret db otp jws
1398 jq -n '{production: {secret_key_base: $ENV.secret,
1399 otp_key_base: $ENV.otp,
1400 db_key_base: $ENV.db,
1401 openid_connect_signing_key: $ENV.jws}}' \
1402 > '${cfg.statePath}/config/secrets.yml'
1403 )
1404
1405 # We remove potentially broken links to old gitlab-shell versions
1406 rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
1407
1408 git config --global core.autocrlf "input"
1409 '';
1410 };
1411 };
1412
1413 systemd.services.gitlab-db-config = {
1414 after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ];
1415 wants = optional (cfg.databaseHost == "") "postgresql.service" ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service";
1416 bindsTo = [ "gitlab-config.service" ];
1417 wantedBy = [ "gitlab.target" ];
1418 partOf = [ "gitlab.target" ];
1419 serviceConfig = {
1420 Type = "oneshot";
1421 User = cfg.user;
1422 Group = cfg.group;
1423 TimeoutSec = "infinity";
1424 Restart = "on-failure";
1425 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1426 RemainAfterExit = true;
1427
1428 ExecStart = pkgs.writeShellScript "gitlab-db-config" ''
1429 set -o errexit -o pipefail -o nounset
1430 shopt -s inherit_errexit
1431 umask u=rwx,g=rx,o=
1432
1433 initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
1434 ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
1435 GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null
1436 '';
1437 };
1438 };
1439
1440 systemd.services.gitlab-sidekiq = {
1441 after = [
1442 "network.target"
1443 "redis-gitlab.service"
1444 "postgresql.service"
1445 "gitlab-config.service"
1446 "gitlab-db-config.service"
1447 ];
1448 bindsTo = [
1449 "gitlab-config.service"
1450 "gitlab-db-config.service"
1451 ];
1452 wants = [ "redis-gitlab.service" ] ++ optional (cfg.databaseHost == "") "postgresql.service";
1453 wantedBy = [ "gitlab.target" ];
1454 partOf = [ "gitlab.target" ];
1455 environment = gitlabEnv // (optionalAttrs cfg.sidekiq.memoryKiller.enable {
1456 SIDEKIQ_MEMORY_KILLER_MAX_RSS = cfg.sidekiq.memoryKiller.maxMemory;
1457 SIDEKIQ_MEMORY_KILLER_GRACE_TIME = cfg.sidekiq.memoryKiller.graceTime;
1458 SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT = cfg.sidekiq.memoryKiller.shutdownWait;
1459 });
1460 path = with pkgs; [
1461 postgresqlPackage
1462 git
1463 ruby
1464 openssh
1465 nodejs
1466 gnupg
1467
1468 "${cfg.packages.gitlab}/share/gitlab/vendor/gems/sidekiq-${cfg.packages.gitlab.rubyEnv.gems.sidekiq.version}"
1469
1470 # Needed for GitLab project imports
1471 gnutar
1472 gzip
1473
1474 procps # Sidekiq MemoryKiller
1475 ];
1476 serviceConfig = {
1477 Type = "simple";
1478 User = cfg.user;
1479 Group = cfg.group;
1480 TimeoutSec = "infinity";
1481 Restart = "always";
1482 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1483 ExecStart = utils.escapeSystemdExecArgs (
1484 [
1485 "${cfg.packages.gitlab}/share/gitlab/bin/sidekiq-cluster"
1486 "*" # all queue groups
1487 ] ++ lib.optionals (cfg.sidekiq.concurrency != null) [
1488 "--concurrency" (toString cfg.sidekiq.concurrency)
1489 ] ++ [
1490 "--environment" "production"
1491 "--require" "."
1492 ]
1493 );
1494 };
1495 };
1496
1497 systemd.services.gitaly = {
1498 after = [ "network.target" "gitlab-config.service" ];
1499 bindsTo = [ "gitlab-config.service" ];
1500 wantedBy = [ "gitlab.target" ];
1501 partOf = [ "gitlab.target" ];
1502 path = with pkgs; [
1503 openssh
1504 git
1505 gzip
1506 bzip2
1507 ];
1508 serviceConfig = {
1509 Type = "simple";
1510 User = cfg.user;
1511 Group = cfg.group;
1512 TimeoutSec = "infinity";
1513 Restart = "on-failure";
1514 WorkingDirectory = gitlabEnv.HOME;
1515 RuntimeDirectory = "gitaly";
1516 ExecStart = "${cfg.packages.gitaly}/bin/gitaly ${gitalyToml}";
1517 };
1518 };
1519
1520 services.gitlab.pages.settings = {
1521 api-secret-key = "${cfg.statePath}/gitlab_pages_secret";
1522 };
1523
1524 systemd.services.gitlab-pages =
1525 let
1526 filteredConfig = filterAttrs (_: v: v != null) cfg.pages.settings;
1527 isSecret = v: isAttrs v && v ? _secret && isString v._secret;
1528 mkPagesKeyValue = lib.generators.toKeyValue {
1529 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" rec {
1530 mkValueString = v:
1531 if isInt v then toString v
1532 else if isString v then v
1533 else if true == v then "true"
1534 else if false == v then "false"
1535 else if isSecret v then builtins.hashString "sha256" v._secret
1536 else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}";
1537 };
1538 };
1539 secretPaths = lib.catAttrs "_secret" (lib.collect isSecret filteredConfig);
1540 mkSecretReplacement = file: ''
1541 replace-secret ${lib.escapeShellArgs [ (builtins.hashString "sha256" file) file "/run/gitlab-pages/gitlab-pages.conf" ]}
1542 '';
1543 secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
1544 configFile = pkgs.writeText "gitlab-pages.conf" (mkPagesKeyValue filteredConfig);
1545 in
1546 mkIf cfg.pages.enable {
1547 description = "GitLab static pages daemon";
1548 after = [ "network.target" "gitlab-config.service" "gitlab.service" ];
1549 bindsTo = [ "gitlab-config.service" "gitlab.service" ];
1550 wantedBy = [ "gitlab.target" ];
1551 partOf = [ "gitlab.target" ];
1552
1553 path = with pkgs; [
1554 unzip
1555 replace-secret
1556 ];
1557
1558 serviceConfig = {
1559 Type = "simple";
1560 TimeoutSec = "infinity";
1561 Restart = "on-failure";
1562
1563 User = cfg.user;
1564 Group = cfg.group;
1565
1566 ExecStartPre = pkgs.writeShellScript "gitlab-pages-pre-start" ''
1567 set -o errexit -o pipefail -o nounset
1568 shopt -s dotglob nullglob inherit_errexit
1569
1570 install -m u=rw ${configFile} /run/gitlab-pages/gitlab-pages.conf
1571 ${secretReplacements}
1572 '';
1573 ExecStart = "${cfg.packages.pages}/bin/gitlab-pages -config=/run/gitlab-pages/gitlab-pages.conf";
1574 WorkingDirectory = gitlabEnv.HOME;
1575 RuntimeDirectory = "gitlab-pages";
1576 RuntimeDirectoryMode = "0700";
1577 };
1578 };
1579
1580 systemd.services.gitlab-workhorse = {
1581 after = [ "network.target" ];
1582 wantedBy = [ "gitlab.target" ];
1583 partOf = [ "gitlab.target" ];
1584 path = with pkgs; [
1585 remarshal
1586 exiftool
1587 git
1588 gnutar
1589 gzip
1590 openssh
1591 cfg.packages.gitlab-workhorse
1592 ];
1593 serviceConfig = {
1594 Type = "simple";
1595 User = cfg.user;
1596 Group = cfg.group;
1597 TimeoutSec = "infinity";
1598 Restart = "on-failure";
1599 WorkingDirectory = gitlabEnv.HOME;
1600 ExecStartPre = pkgs.writeShellScript "gitlab-workhorse-pre-start" ''
1601 set -o errexit -o pipefail -o nounset
1602 shopt -s dotglob nullglob inherit_errexit
1603
1604 ${utils.genJqSecretsReplacementSnippet
1605 cfg.workhorse.config
1606 "${cfg.statePath}/config/gitlab-workhorse.json"}
1607
1608 json2toml "${cfg.statePath}/config/gitlab-workhorse.json" "${cfg.statePath}/config/gitlab-workhorse.toml"
1609 rm "${cfg.statePath}/config/gitlab-workhorse.json"
1610 '';
1611 ExecStart =
1612 "${cfg.packages.gitlab-workhorse}/bin/${
1613 optionalString (lib.versionAtLeast (lib.getVersion cfg.packages.gitlab-workhorse) "16.10") "gitlab-"
1614 }workhorse "
1615 + "-listenUmask 0 "
1616 + "-listenNetwork unix "
1617 + "-listenAddr /run/gitlab/gitlab-workhorse.socket "
1618 + "-authSocket ${gitlabSocket} "
1619 + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public "
1620 + "-config ${cfg.statePath}/config/gitlab-workhorse.toml "
1621 + "-secretPath ${cfg.statePath}/.gitlab_workhorse_secret";
1622 };
1623 };
1624
1625 systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
1626 description = "GitLab incoming mail daemon";
1627 after = [ "network.target" "redis-gitlab.service" "gitlab-config.service" ];
1628 bindsTo = [ "gitlab-config.service" ];
1629 wantedBy = [ "gitlab.target" ];
1630 partOf = [ "gitlab.target" ];
1631 environment = gitlabEnv;
1632 serviceConfig = {
1633 Type = "simple";
1634 TimeoutSec = "infinity";
1635 Restart = "on-failure";
1636
1637 User = cfg.user;
1638 Group = cfg.group;
1639 ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.statePath}/config/mail_room.yml";
1640 WorkingDirectory = gitlabEnv.HOME;
1641 };
1642 };
1643
1644 systemd.services.gitlab = {
1645 after = [
1646 "gitlab-workhorse.service"
1647 "network.target"
1648 "redis-gitlab.service"
1649 "gitlab-config.service"
1650 "gitlab-db-config.service"
1651 ];
1652 bindsTo = [
1653 "gitlab-config.service"
1654 "gitlab-db-config.service"
1655 ];
1656 wants = [ "redis-gitlab.service" ] ++ optional (cfg.databaseHost == "") "postgresql.service";
1657 requiredBy = [ "gitlab.target" ];
1658 partOf = [ "gitlab.target" ];
1659 environment = gitlabEnv;
1660 path = with pkgs; [
1661 postgresqlPackage
1662 git
1663 openssh
1664 nodejs
1665 procps
1666 gnupg
1667 gzip
1668 ];
1669 serviceConfig = {
1670 Type = "notify";
1671 User = cfg.user;
1672 Group = cfg.group;
1673 TimeoutSec = "infinity";
1674 Restart = "on-failure";
1675 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1676 ExecStart = concatStringsSep " " [
1677 "${cfg.packages.gitlab.rubyEnv}/bin/bundle" "exec" "puma"
1678 "-e production"
1679 "-C ${cfg.statePath}/config/puma.rb"
1680 "-w ${cfg.puma.workers}"
1681 "-t ${cfg.puma.threadsMin}:${cfg.puma.threadsMax}"
1682 ];
1683 };
1684
1685 };
1686
1687 systemd.services.gitlab-backup = {
1688 after = [ "gitlab.service" ];
1689 bindsTo = [ "gitlab.service" ];
1690 startAt = cfg.backup.startAt;
1691 environment = {
1692 RAILS_ENV = "production";
1693 CRON = "1";
1694 } // optionalAttrs (stringLength cfg.backup.skip > 0) {
1695 SKIP = cfg.backup.skip;
1696 };
1697 serviceConfig = {
1698 User = cfg.user;
1699 Group = cfg.group;
1700 ExecStart = "${gitlab-rake}/bin/gitlab-rake gitlab:backup:create";
1701 };
1702 };
1703
1704 };
1705
1706 meta.doc = ./gitlab.md;
1707 meta.maintainers = teams.gitlab.members;
1708}