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