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