at 25.11-pre 66 kB view raw
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}