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