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