1 { config, lib, options, pkgs, utils, ... }:
6 cfg = config.services.gitlab;
7 opt = options.services.gitlab;
9 toml = pkgs.formats.toml {};
10 yaml = pkgs.formats.yaml {};
12 ruby = cfg.packages.gitlab.ruby;
14 postgresqlPackage = if config.services.postgresql.enable then
15 config.services.postgresql.package
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.
27 pkgs.git.overrideAttrs (oldAttrs: rec {
30 url = "https://www.kernel.org/pub/software/scm/git/git-${version}.tar.xz";
31 sha256 = "sha256-mv13OdNkXggeKQkJ+47QcJ6lYmcw6Qjri1ZJ2ETCTOk=";
35 gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
36 gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
37 pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url;
41 adapter = "postgresql";
42 database = cfg.databaseName;
43 host = cfg.databaseHost;
44 username = cfg.databaseUsername;
46 pool = cfg.databasePool;
47 } // cfg.extraDatabaseConfig;
48 in if lib.versionAtLeast (lib.getVersion cfg.packages.gitlab) "15.0" then {
49 production.main = val;
54 # We only want to create a database if we're actually going to connect to it.
55 databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "";
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"
63 bin_path = "${gitPackage}/bin/git"
66 dir = "${cfg.packages.gitaly.ruby}"
69 dir = "${cfg.packages.gitlab-shell}"
72 custom_hooks_dir = "${cfg.statePath}/custom_hooks"
75 secret_file = "${cfg.statePath}/gitlab_shell_secret"
76 url = "http+unix://${pathUrlQuote gitlabSocket}"
78 [gitlab.http-settings]
79 self_signed_cert = false
81 ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: ''
83 name = "${lib.escape ["\""] k}"
84 path = "${lib.escape ["\""] v.path}"
85 '') gitlabConfig.production.repositories.storages))}
88 gitlabShellConfig = flip recursiveUpdate cfg.extraShellConfig {
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";
97 redisConfig.production.url = cfg.redisUrl;
99 cableYml = yaml.generate "cable.yml" {
103 channel_prefix = "gitlab_production";
108 "-pages-domain" gitlabConfig.production.pages.host
109 "-pages-root" "${gitlabConfig.production.shared.path}/pages"
110 ] ++ cfg.pagesExtraArgs;
113 # These are the default settings from config/gitlab.example.yml
114 production = flip recursiveUpdate cfg.extraConfig {
120 email_enabled = true;
121 email_display_name = "GitLab";
122 email_reply_to = "noreply@localhost";
124 default_projects_features = {
126 merge_requests = true;
130 container_registry = true;
133 repositories.storages.default.path = "${cfg.statePath}/repositories";
134 repositories.storages.default.gitaly_address = "unix:${gitalySocket}";
135 artifacts.enabled = true;
137 gravatar.enabled = true;
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";
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;
152 path = "${cfg.packages.gitlab-shell}";
153 hooks_path = "${cfg.statePath}/shell/hooks";
154 secret_file = "${cfg.statePath}/gitlab_shell_secret";
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";
162 ip_whitelist = [ "127.0.0.0/8" "::1/128" ];
165 address = "localhost";
169 registry = lib.optionalAttrs cfg.registry.enable {
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;
178 uploads.storage_path = cfg.statePath;
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";
195 runtimeDeps = with pkgs; [
203 findutils # Needed for gitlab:cleanup:orphan_job_artifact_files
206 gitlab-rake = pkgs.stdenv.mkDerivation {
207 name = "gitlab-rake";
208 nativeBuildInputs = [ pkgs.makeWrapper ];
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'
221 gitlab-rails = pkgs.stdenv.mkDerivation {
222 name = "gitlab-rails";
223 nativeBuildInputs = [ pkgs.makeWrapper ];
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'
235 extraGitlabRb = pkgs.writeText "extra-gitlab.rb" cfg.extraGitlabRb;
237 smtpSettings = pkgs.writeText "gitlab-smtp-settings.rb" ''
238 if Rails.env.production?
239 Rails.application.config.action_mailer.delivery_method = :smtp
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}'
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")
271 description = lib.mdDoc ''
272 Enable the gitlab service.
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";
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";
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";
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";
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";
312 statePath = mkOption {
314 default = "/var/gitlab/state";
315 description = lib.mdDoc ''
316 GitLab state directory. Configuration, repositories and
317 logs, among other things, are stored here.
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`.
326 extraEnv = mkOption {
327 type = types.attrsOf types.str;
329 description = lib.mdDoc ''
330 Additional environment variables for the GitLab environment.
334 backup.startAt = mkOption {
335 type = with types; either str (listOf str);
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)`.
345 backup.path = mkOption {
347 default = cfg.statePath + "/backup";
348 defaultText = literalExpression ''config.${opt.statePath} + "/backup"'';
349 description = lib.mdDoc "GitLab path for backups.";
352 backup.keepTime = mkOption {
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”.
363 backup.skip = mkOption {
377 either value (listOf value);
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
387 Refer to <https://docs.gitlab.com/ee/raketasks/backup_restore.html#excluding-specific-directories-from-the-backup>
388 for more information.
392 backup.uploadOptions = mkOption {
395 example = literalExpression ''
397 # Fog storage connection settings, see http://fog.io/storage/
400 region = "eu-north-1";
401 aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX";
402 aws_secret_access_key = { _secret = config.deployment.keys.aws_access_key.path; };
405 # The remote 'directory' to store your backups in.
406 # For S3, this would be the bucket name.
407 remote_directory = "my-gitlab-backups";
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;
413 # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
414 encryption = "AES256";
416 # Specifies Amazon S3 storage class to use for backups, this is optional
417 storage_class = "STANDARD";
420 description = lib.mdDoc ''
421 GitLab automatic upload specification. Tells GitLab to
422 upload the backup to a remote location when done.
424 Attributes specified here are added under
425 `production -> backup -> upload` in
426 {file}`config/gitlab.yml`.
430 databaseHost = mkOption {
433 description = lib.mdDoc ''
434 GitLab database hostname. An empty string means
435 “use local unix socket connection”.
439 databasePasswordFile = mkOption {
440 type = with types; nullOr path;
442 description = lib.mdDoc ''
443 File containing the GitLab database user password.
445 This should be a string, not a nix path, since nix paths are
446 copied into the world-readable nix store.
450 databaseCreateLocally = mkOption {
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.
461 databaseName = mkOption {
464 description = lib.mdDoc "GitLab database name.";
467 databaseUsername = mkOption {
470 description = lib.mdDoc "GitLab database user.";
473 databasePool = mkOption {
476 description = lib.mdDoc "Database connection pool size.";
479 extraDatabaseConfig = mkOption {
482 description = lib.mdDoc "Extra configuration in config/database.yml.";
485 redisUrl = mkOption {
487 default = "unix:/run/gitlab/redis.sock";
488 example = "redis://localhost:6379/";
489 description = lib.mdDoc "Redis URL for all GitLab services.";
492 extraGitlabRb = mkOption {
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",
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
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.";
522 description = lib.mdDoc ''
523 GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're
531 description = lib.mdDoc "Whether gitlab prints URLs with https as scheme.";
537 description = lib.mdDoc "User to run gitlab and all related services.";
543 description = lib.mdDoc "Group to run gitlab and all related services.";
546 initialRootEmail = mkOption {
548 default = "admin@local.host";
549 description = lib.mdDoc ''
550 Initial email address of the root account if this is a new install.
554 initialRootPasswordFile = mkOption {
555 type = with types; nullOr path;
557 description = lib.mdDoc ''
558 File containing the initial password of the root account if
559 this is a new install.
561 This should be a string, not a nix path, since nix paths are
562 copied into the world-readable nix store.
570 description = lib.mdDoc "Enable GitLab container registry.";
574 default = config.services.gitlab.host;
575 defaultText = literalExpression "config.services.gitlab.host";
576 description = lib.mdDoc "GitLab container registry host name.";
581 description = lib.mdDoc "GitLab container registry port.";
583 certFile = mkOption {
585 description = lib.mdDoc "Path to GitLab container registry certificate.";
589 description = lib.mdDoc "Path to GitLab container registry certificate-key.";
591 defaultForProjects = mkOption {
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.";
599 default = "gitlab-issuer";
600 description = lib.mdDoc "GitLab container registry issuer.";
602 serviceName = mkOption {
604 default = "container_registry";
605 description = lib.mdDoc "GitLab container registry service name.";
607 externalAddress = mkOption {
610 description = lib.mdDoc "External address used to access registry from the internet";
612 externalPort = mkOption {
614 description = lib.mdDoc "External port used to access registry from the internet";
622 description = lib.mdDoc "Enable gitlab mail delivery over SMTP.";
627 default = "localhost";
628 description = lib.mdDoc "Address of the SMTP server for GitLab.";
634 description = lib.mdDoc "Port of the SMTP server for GitLab.";
637 username = mkOption {
638 type = with types; nullOr str;
640 description = lib.mdDoc "Username of the SMTP server for GitLab.";
643 passwordFile = mkOption {
644 type = types.nullOr types.path;
646 description = lib.mdDoc ''
647 File containing the password of the SMTP server for GitLab.
649 This should be a string, not a nix path, since nix paths
650 are copied into the world-readable nix store.
656 default = "localhost";
657 description = lib.mdDoc "HELO domain to use for outgoing mail.";
660 authentication = mkOption {
661 type = with types; nullOr str;
663 description = lib.mdDoc "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
666 enableStartTLSAuto = mkOption {
669 description = lib.mdDoc "Whether to try to use StartTLS.";
675 description = lib.mdDoc "Whether to use TLS wrapper-mode.";
678 opensslVerifyMode = mkOption {
681 description = lib.mdDoc "How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
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";
691 secrets.secretFile = mkOption {
692 type = with types; nullOr path;
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.
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.
702 This should be a string, not a nix path, since nix paths are
703 copied into the world-readable nix store.
707 secrets.dbFile = mkOption {
708 type = with types; nullOr path;
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.
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.
718 This should be a string, not a nix path, since nix paths are
719 copied into the world-readable nix store.
723 secrets.otpFile = mkOption {
724 type = with types; nullOr path;
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.
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.
734 This should be a string, not a nix path, since nix paths are
735 copied into the world-readable nix store.
739 secrets.jwsFile = mkOption {
740 type = with types; nullOr path;
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
747 Make sure the secret is an RSA private key in PEM format. You can
752 This should be a string, not a nix path, since nix paths are
753 copied into the world-readable nix store.
757 extraShellConfig = mkOption {
760 description = lib.mdDoc "Extra configuration to merge into shell-config.yml";
763 puma.workers = mkOption {
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.
773 Each worker consumes quite a bit of memory, so
774 be careful when increasing this.
779 puma.threadsMin = mkOption {
782 apply = x: builtins.toString x;
783 description = lib.mdDoc ''
784 The minimum number of threads Puma should use per
788 Each thread consumes memory and contributes to Global VM
789 Lock contention, so be careful when increasing this.
794 puma.threadsMax = mkOption {
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.
806 Each thread consumes memory and contributes to Global VM
807 Lock contention, so be careful when increasing this.
812 sidekiq.memoryKiller.enable = mkOption {
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.
820 See <https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html>
825 sidekiq.memoryKiller.maxMemory = mkOption {
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.
835 sidekiq.memoryKiller.graceTime = mkOption {
838 apply = x: builtins.toString x;
839 description = lib.mdDoc ''
840 The time MemoryKiller waits after noticing excessive memory
841 consumption before killing Sidekiq.
845 sidekiq.memoryKiller.shutdownWait = mkOption {
848 apply = x: builtins.toString x;
849 description = lib.mdDoc ''
850 The time allowed for all jobs to finish before Sidekiq is
859 description = lib.mdDoc ''
860 Enable rotation of log files.
864 frequency = mkOption {
867 description = lib.mdDoc "How often to rotate the logs.";
873 description = lib.mdDoc "How many rotations to keep.";
877 workhorse.config = mkOption {
880 example = literalExpression ''
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"; };
889 description = lib.mdDoc ''
890 Configuration options to add to Workhorse's configuration
894 <https://gitlab.com/gitlab-org/gitlab/-/blob/master/workhorse/config.toml.example>
896 <https://docs.gitlab.com/ee/development/workhorse/configuration.html>
897 for examples and option documentation.
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`
910 extraConfig = mkOption {
913 example = literalExpression ''
916 default_projects_features = {
922 auto_sign_in_with_provider = "openid_connect";
923 allow_single_sign_on = ["openid_connect"];
924 block_auto_created_users = false;
927 name = "openid_connect";
928 label = "OpenID Connect";
930 name = "openid_connect";
931 scope = ["openid" "profile"];
932 response_type = "code";
933 issuer = "https://keycloak.example.com/auth/realms/My%20Realm";
935 client_auth_method = "query";
936 uid_field = "preferred_username";
938 identifier = "gitlab";
939 secret = { _secret = "/var/keys/gitlab_oidc_secret"; };
940 redirect_uri = "https://git.example.com/users/auth/openid_connect/callback";
948 description = lib.mdDoc ''
949 Extra options to be added under
951 {file}`config/gitlab.yml`, as a nix attribute
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.
968 config = mkIf cfg.enable {
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.'';
976 assertion = (cfg.databaseHost != "") -> (cfg.databasePasswordFile != null);
977 message = "When services.gitlab.databaseHost is customized, services.gitlab.databasePasswordFile must be set!";
980 assertion = cfg.initialRootPasswordFile != null;
981 message = "services.gitlab.initialRootPasswordFile must be set!";
984 assertion = cfg.secrets.secretFile != null;
985 message = "services.gitlab.secrets.secretFile must be set!";
988 assertion = cfg.secrets.dbFile != null;
989 message = "services.gitlab.secrets.dbFile must be set!";
992 assertion = cfg.secrets.otpFile != null;
993 message = "services.gitlab.secrets.otpFile must be set!";
996 assertion = cfg.secrets.jwsFile != null;
997 message = "services.gitlab.secrets.jwsFile must be set!";
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";
1005 environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ];
1007 systemd.targets.gitlab = {
1008 description = "Common target for all GitLab services.";
1009 wantedBy = [ "multi-user.target" ];
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;
1020 # We use postgres as the main data store.
1021 services.postgresql = optionalAttrs databaseActuallyCreateLocally {
1023 ensureUsers = singleton { name = cfg.databaseUsername; };
1026 # Enable rotation of log files
1027 services.logrotate = {
1028 enable = cfg.logrotate.enable;
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;
1041 # The postgresql module doesn't currently support concepts like
1042 # objects owners and extensions; for now we tack on what's needed
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" ];
1057 psql --port=${toString pgsql.port} "$@"
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..."
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}"
1072 PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
1073 PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;"
1077 User = pgsql.superUser;
1079 RemainAfterExit = true;
1083 systemd.services.gitlab-registry-cert = optionalAttrs cfg.registry.enable {
1084 path = with pkgs; [ openssl ];
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}
1098 ConditionPathExists = "!${cfg.registry.certFile}";
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" ];
1108 # Enable Docker Registry, if GitLab-Container Registry is enabled
1109 services.dockerRegistry = optionalAttrs cfg.registry.enable {
1111 enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly
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;
1122 # Use postfix to send out mails.
1123 services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost");
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;
1132 users.groups.${cfg.group}.gid = config.ids.gids.gitlab;
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"
1166 "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
1170 systemd.services.gitlab-config = {
1171 wantedBy = [ "gitlab.target" ];
1172 partOf = [ "gitlab.target" ];
1183 TimeoutSec = "infinity";
1184 Restart = "on-failure";
1185 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1186 RemainAfterExit = true;
1189 preStartFullPrivileges = ''
1190 set -o errexit -o pipefail -o nounset
1191 shopt -s dotglob nullglob inherit_errexit
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/*
1198 in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}";
1200 ExecStart = pkgs.writeShellScript "gitlab-config" ''
1201 set -o errexit -o pipefail -o nounset
1202 shopt -s inherit_errexit
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
1215 ${cfg.packages.gitlab-shell}/bin/install
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'
1227 openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
1229 rm -f '${cfg.statePath}/config/database.yml'
1231 ${if cfg.databasePasswordFile != null then ''
1232 db_password="$(<'${cfg.databasePasswordFile}')"
1235 if [[ -z "$db_password" ]]; then
1236 >&2 echo "Database password was an empty string!"
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'
1245 jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
1246 >'${cfg.statePath}/config/database.yml'
1250 ${utils.genJqSecretsReplacementSnippet
1252 "${cfg.statePath}/config/gitlab.yml"
1255 rm -f '${cfg.statePath}/config/secrets.yml'
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'
1269 # We remove potentially broken links to old gitlab-shell versions
1270 rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
1272 git config --global core.autocrlf "input"
1277 systemd.services.gitlab-db-config = {
1278 after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ];
1280 "gitlab-config.service"
1281 ] ++ optional (cfg.databaseHost == "") "postgresql.service"
1282 ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service";
1283 wantedBy = [ "gitlab.target" ];
1284 partOf = [ "gitlab.target" ];
1289 TimeoutSec = "infinity";
1290 Restart = "on-failure";
1291 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1292 RemainAfterExit = true;
1294 ExecStart = pkgs.writeShellScript "gitlab-db-config" ''
1295 set -o errexit -o pipefail -o nounset
1296 shopt -s inherit_errexit
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
1306 systemd.services.gitlab-sidekiq = {
1309 "redis-gitlab.service"
1310 "postgresql.service"
1311 "gitlab-config.service"
1312 "gitlab-db-config.service"
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;
1334 # Needed for GitLab project imports
1338 procps # Sidekiq MemoryKiller
1344 TimeoutSec = "infinity";
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";
1351 systemd.services.gitaly = {
1352 after = [ "network.target" "gitlab-config.service" ];
1353 bindsTo = [ "gitlab-config.service" ];
1354 wantedBy = [ "gitlab.target" ];
1355 partOf = [ "gitlab.target" ];
1358 procps # See https://gitlab.com/gitlab-org/gitaly/issues/1562
1360 cfg.packages.gitaly.rubyEnv
1361 cfg.packages.gitaly.rubyEnv.wrappedRuby
1369 TimeoutSec = "infinity";
1370 Restart = "on-failure";
1371 WorkingDirectory = gitlabEnv.HOME;
1372 ExecStart = "${cfg.packages.gitaly}/bin/gitaly ${gitalyToml}";
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" ];
1383 path = [ pkgs.unzip ];
1387 TimeoutSec = "infinity";
1388 Restart = "on-failure";
1393 ExecStart = "${cfg.packages.pages}/bin/gitlab-pages ${escapeShellArgs pagesArgs}";
1394 WorkingDirectory = gitlabEnv.HOME;
1398 systemd.services.gitlab-workhorse = {
1399 after = [ "network.target" ];
1400 wantedBy = [ "gitlab.target" ];
1401 partOf = [ "gitlab.target" ];
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
1422 ${utils.genJqSecretsReplacementSnippet
1423 cfg.workhorse.config
1424 "${cfg.statePath}/config/gitlab-workhorse.json"}
1426 json2toml "${cfg.statePath}/config/gitlab-workhorse.json" "${cfg.statePath}/config/gitlab-workhorse.toml"
1427 rm "${cfg.statePath}/config/gitlab-workhorse.json"
1430 "${cfg.packages.gitlab-workhorse}/bin/workhorse "
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";
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;
1450 TimeoutSec = "infinity";
1451 Restart = "on-failure";
1455 ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.statePath}/config/mail_room.yml";
1456 WorkingDirectory = gitlabEnv.HOME;
1460 systemd.services.gitlab = {
1462 "gitlab-workhorse.service"
1464 "redis-gitlab.service"
1465 "gitlab-config.service"
1466 "gitlab-db-config.service"
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;
1488 TimeoutSec = "infinity";
1489 Restart = "on-failure";
1490 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1491 ExecStart = concatStringsSep " " [
1492 "${cfg.packages.gitlab.rubyEnv}/bin/puma"
1494 "-C ${cfg.statePath}/config/puma.rb"
1495 "-w ${cfg.puma.workers}"
1496 "-t ${cfg.puma.threadsMin}:${cfg.puma.threadsMax}"
1502 systemd.services.gitlab-backup = {
1503 after = [ "gitlab.service" ];
1504 bindsTo = [ "gitlab.service" ];
1505 startAt = cfg.backup.startAt;
1507 RAILS_ENV = "production";
1509 } // optionalAttrs (stringLength cfg.backup.skip > 0) {
1510 SKIP = cfg.backup.skip;
1515 ExecStart = "${gitlab-rake}/bin/gitlab-rake gitlab:backup:create";
1521 meta.doc = ./gitlab.xml;