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