at 24.11-pre 24 kB view raw
1{ config, lib, pkgs, ... }: 2 3let 4 inherit (lib) 5 filterAttrsRecursive 6 generators 7 literalExpression 8 mkDefault 9 mkIf 10 mkOption 11 mkEnableOption 12 mkPackageOption 13 mkMerge 14 pipe 15 types 16 ; 17 18 cfg = config.services.movim; 19 20 defaultPHPCfg = { 21 "output_buffering" = 0; 22 "error_reporting" = "E_ALL & ~E_DEPRECATED & ~E_STRICT"; 23 "opcache.enable_cli" = 1; 24 "opcache.interned_strings_buffer" = 8; 25 "opcache.max_accelerated_files" = 6144; 26 "opcache.memory_consumption" = 128; 27 "opcache.revalidate_freq" = 2; 28 "opcache.fast_shutdown" = 1; 29 }; 30 31 phpCfg = generators.toKeyValue 32 { mkKeyValue = generators.mkKeyValueDefault { } " = "; } 33 (defaultPHPCfg // cfg.phpCfg); 34 35 podConfigFlags = 36 let 37 bevalue = a: lib.escapeShellArg (generators.mkValueStringDefault { } a); 38 in 39 lib.concatStringsSep " " 40 (lib.attrsets.foldlAttrs 41 (acc: k: v: acc ++ lib.optional (v != null) "--${k}=${bevalue v}") 42 [ ] 43 cfg.podConfig); 44 45 package = 46 let 47 p = cfg.package.override 48 ({ 49 inherit phpCfg; 50 withPgsql = cfg.database.type == "pgsql"; 51 withMysql = cfg.database.type == "mysql"; 52 inherit (cfg) minifyStaticFiles; 53 } // lib.optionalAttrs (lib.isAttrs cfg.minifyStaticFiles) (with cfg.minifyStaticFiles; { 54 esbuild = esbuild.package; 55 lightningcss = lightningcss.package; 56 scour = scour.package; 57 })); 58 in 59 p.overrideAttrs (finalAttrs: prevAttrs: 60 let 61 appDir = "$out/share/php/${finalAttrs.pname}"; 62 63 stateDirectories = '' 64 # Symlinking in our state directories 65 rm -rf $out/.env $out/cache ${appDir}/public/cache 66 ln -s ${cfg.dataDir}/.env ${appDir}/.env 67 ln -s ${cfg.dataDir}/public/cache ${appDir}/public/cache 68 ln -s ${cfg.logDir} ${appDir}/log 69 ln -s ${cfg.runtimeDir}/cache ${appDir}/cache 70 ''; 71 72 exposeComposer = '' 73 # Expose PHP Composer for scripts 74 mkdir -p $out/bin 75 echo "#!${lib.getExe pkgs.dash}" > $out/bin/movim-composer 76 echo "${finalAttrs.php.packages.composer}/bin/composer --working-dir="${appDir}" \"\$@\"" >> $out/bin/movim-composer 77 chmod +x $out/bin/movim-composer 78 ''; 79 80 podConfigInputDisableReplace = lib.optionalString (podConfigFlags != "") 81 (lib.concatStringsSep "\n" 82 (lib.attrsets.foldlAttrs 83 (acc: k: v: 84 acc ++ lib.optional (v != null) 85 # Disable all Admin panel options that were set in the 86 # `cfg.podConfig` to prevent confusing situtions where the 87 # values are rewritten on server reboot 88 '' 89 substituteInPlace ${appDir}/app/widgets/AdminMain/adminmain.tpl \ 90 --replace-warn 'name="${k}"' 'name="${k}" disabled' 91 '') 92 [ ] 93 cfg.podConfig)); 94 95 precompressStaticFilesJobs = 96 let 97 inherit (cfg.precompressStaticFiles) brotli gzip; 98 99 findTextFileNames = lib.concatStringsSep " -o " 100 (builtins.map (n: ''-iname "*.${n}"'') 101 [ "css" "ini" "js" "json" "manifest" "mjs" "svg" "webmanifest" ]); 102 in 103 lib.concatStringsSep "\n" [ 104 (lib.optionalString brotli.enable '' 105 echo -n "Precompressing static files with Brotli " 106 find ${appDir}/public -type f ${findTextFileNames} -print0 \ 107 | xargs -0 -n 1 -P $NIX_BUILD_CORES ${pkgs.writeShellScript "movim_precompress_broti" '' 108 file="$1" 109 ${lib.getExe brotli.package} --keep --quality=${builtins.toString brotli.compressionLevel} --output=$file.br $file 110 ''} 111 echo " done." 112 '') 113 (lib.optionalString gzip.enable '' 114 echo -n "Precompressing static files with Gzip " 115 find ${appDir}/public -type f ${findTextFileNames} -print0 \ 116 | xargs -0 -n 1 -P $NIX_BUILD_CORES ${pkgs.writeShellScript "movim_precompress_broti" '' 117 file="$1" 118 ${lib.getExe gzip.package} -c -${builtins.toString gzip.compressionLevel} $file > $file.gz 119 ''} 120 echo " done." 121 '') 122 ]; 123 in 124 { 125 postInstall = lib.concatStringsSep "\n\n" [ 126 prevAttrs.postInstall 127 stateDirectories 128 exposeComposer 129 podConfigInputDisableReplace 130 precompressStaticFilesJobs 131 ]; 132 }); 133 134 configFile = pipe cfg.settings [ 135 (filterAttrsRecursive (_: v: v != null)) 136 (generators.toKeyValue { }) 137 (pkgs.writeText "movim-env") 138 ]; 139 140 pool = "movim"; 141 fpm = config.services.phpfpm.pools.${pool}; 142 phpExecutionUnit = "phpfpm-${pool}"; 143 144 dbService = { 145 "postgresql" = "postgresql.service"; 146 "mysql" = "mysql.service"; 147 }.${cfg.database.type}; 148in 149{ 150 options.services = { 151 movim = { 152 enable = mkEnableOption "a Movim instance"; 153 package = mkPackageOption pkgs "movim" { }; 154 phpPackage = mkPackageOption pkgs "php" { }; 155 156 phpCfg = mkOption { 157 type = with types; attrsOf (oneOf [ int str bool ]); 158 defaultText = literalExpression (generators.toPretty { } defaultPHPCfg); 159 default = { }; 160 description = "Extra PHP INI options such as `memory_limit`, `max_execution_time`, etc."; 161 }; 162 163 user = mkOption { 164 type = types.nonEmptyStr; 165 default = "movim"; 166 description = "User running Movim service"; 167 }; 168 169 group = mkOption { 170 type = types.nonEmptyStr; 171 default = "movim"; 172 description = "Group running Movim service"; 173 }; 174 175 dataDir = mkOption { 176 type = types.nonEmptyStr; 177 default = "/var/lib/movim"; 178 description = "State directory of the `movim` user which holds the applications state & data."; 179 }; 180 181 logDir = mkOption { 182 type = types.nonEmptyStr; 183 default = "/var/log/movim"; 184 description = "Log directory of the `movim` user which holds the applications logs."; 185 }; 186 187 runtimeDir = mkOption { 188 type = types.nonEmptyStr; 189 default = "/run/movim"; 190 description = "Runtime directory of the `movim` user which holds the applications caches & temporary files."; 191 }; 192 193 domain = mkOption { 194 type = types.nonEmptyStr; 195 description = "Fully-qualified domain name (FQDN) for the Movim instance."; 196 }; 197 198 port = mkOption { 199 type = types.port; 200 default = 8080; 201 description = "Movim daemon port."; 202 }; 203 204 debug = mkOption { 205 type = types.bool; 206 default = false; 207 description = "Debugging logs."; 208 }; 209 210 verbose = mkOption { 211 type = types.bool; 212 default = false; 213 description = "Verbose logs."; 214 }; 215 216 minifyStaticFiles = mkOption { 217 type = with types; either bool (submodule { 218 options = { 219 script = mkOption { 220 type = types.submodule { 221 options = { 222 enable = mkEnableOption "Script minification"; 223 package = mkPackageOption pkgs "esbuild" { }; 224 target = mkOption { 225 type = with types; nullOr nonEmptyStr; 226 default = null; 227 }; 228 }; 229 }; 230 }; 231 style = mkOption { 232 type = types.submodule { 233 options = { 234 enable = mkEnableOption "Script minification"; 235 package = mkPackageOption pkgs "lightningcss" { }; 236 target = mkOption { 237 type = with types; nullOr nonEmptyStr; 238 default = null; 239 }; 240 }; 241 }; 242 }; 243 svg = mkOption { 244 type = types.submodule { 245 options = { 246 enable = mkEnableOption "SVG minification"; 247 package = mkPackageOption pkgs "scour" { }; 248 }; 249 }; 250 }; 251 }; 252 }); 253 default = true; 254 description = "Do minification on public static files"; 255 }; 256 257 precompressStaticFiles = mkOption { 258 type = with types; submodule { 259 options = { 260 brotli = { 261 enable = mkEnableOption "Brotli precompression"; 262 package = mkPackageOption pkgs "brotli" { }; 263 compressionLevel = mkOption { 264 type = types.ints.between 0 11; 265 default = 11; 266 description = "Brotli compression level"; 267 }; 268 }; 269 gzip = { 270 enable = mkEnableOption "Gzip precompression"; 271 package = mkPackageOption pkgs "gzip" { }; 272 compressionLevel = mkOption { 273 type = types.ints.between 1 9; 274 default = 9; 275 description = "Gzip compression level"; 276 }; 277 }; 278 }; 279 }; 280 default = { 281 brotli.enable = true; 282 gzip.enable = false; 283 }; 284 description = "Aggressively precompress static files"; 285 }; 286 287 podConfig = mkOption { 288 type = types.submodule { 289 options = { 290 info = mkOption { 291 type = with types; nullOr str; 292 default = null; 293 description = "Content of the info box on the login page"; 294 }; 295 296 description = mkOption { 297 type = with types; nullOr str; 298 default = null; 299 description = "General description of the instance"; 300 }; 301 302 timezone = mkOption { 303 type = with types; nullOr str; 304 default = null; 305 description = "The server timezone"; 306 }; 307 308 restrictsuggestions = mkOption { 309 type = with types; nullOr bool; 310 default = null; 311 description = "Only suggest chatrooms, Communities and other contents that are available on the user XMPP server and related services"; 312 }; 313 314 chatonly = mkOption { 315 type = with types; nullOr bool; 316 default = null; 317 description = "Disable all the social feature (Communities, Blog) and keep only the chat ones"; 318 }; 319 320 disableregistration = mkOption { 321 type = with types; nullOr bool; 322 default = null; 323 description = "Remove the XMPP registration flow and buttons from the interface"; 324 }; 325 326 loglevel = mkOption { 327 type = with types; nullOr (ints.between 0 3); 328 default = null; 329 description = "The server loglevel"; 330 }; 331 332 locale = mkOption { 333 type = with types; nullOr str; 334 default = null; 335 description = "The server main locale"; 336 }; 337 338 xmppdomain = mkOption { 339 type = with types; nullOr str; 340 default = null; 341 description = "The default XMPP server domain"; 342 }; 343 344 xmppdescription = mkOption { 345 type = with types; nullOr str; 346 default = null; 347 description = "The default XMPP server description"; 348 }; 349 350 xmppwhitelist = mkOption { 351 type = with types; nullOr str; 352 default = null; 353 description = "The allowlisted XMPP servers"; 354 }; 355 }; 356 }; 357 default = { }; 358 description = '' 359 Pod configuration (values from `php daemon.php config --help`). 360 Note that these values will now be disabled in the admin panel. 361 ''; 362 }; 363 364 settings = mkOption { 365 type = with types; attrsOf (nullOr (oneOf [ int str bool ])); 366 default = { }; 367 description = ".env settings for Movim. Secrets should use `secretFile` option instead. `null`s will be culled."; 368 }; 369 370 secretFile = mkOption { 371 type = with types; nullOr path; 372 default = null; 373 description = "The secret file to be sourced for the .env settings."; 374 }; 375 376 database = { 377 type = mkOption { 378 type = types.enum [ "mysql" "postgresql" ]; 379 example = "mysql"; 380 default = "postgresql"; 381 description = "Database engine to use."; 382 }; 383 384 name = mkOption { 385 type = types.str; 386 default = "movim"; 387 description = "Database name."; 388 }; 389 390 user = mkOption { 391 type = types.str; 392 default = "movim"; 393 description = "Database username."; 394 }; 395 396 createLocally = mkOption { 397 type = types.bool; 398 default = true; 399 description = "local database using UNIX socket authentication"; 400 }; 401 }; 402 403 nginx = mkOption { 404 type = with types; nullOr (submodule 405 (import ../web-servers/nginx/vhost-options.nix { 406 inherit config lib; 407 })); 408 default = null; 409 example = lib.literalExpression /* nginx */ '' 410 { 411 serverAliases = [ 412 "pics.''${config.networking.domain}" 413 ]; 414 enableACME = true; 415 forceHttps = true; 416 } 417 ''; 418 description = '' 419 With this option, you can customize an nginx virtual host which already has sensible defaults for Movim. 420 Set to `{ }` if you do not need any customization to the virtual host. 421 If enabled, then by default, the {option}`serverName` is `''${domain}`, 422 If this is set to null (the default), no nginx virtualHost will be configured. 423 ''; 424 }; 425 426 poolConfig = mkOption { 427 type = with types; attrsOf (oneOf [ int str bool ]); 428 default = { }; 429 description = "Options for Movims PHP-FPM pool."; 430 }; 431 }; 432 }; 433 434 config = mkIf cfg.enable { 435 environment.systemPackages = [ cfg.package ]; 436 437 users = { 438 users = { 439 movim = mkIf (cfg.user == "movim") { 440 isSystemUser = true; 441 group = cfg.group; 442 }; 443 "${config.services.nginx.user}".extraGroups = [ cfg.group ]; 444 }; 445 groups = { 446 ${cfg.group} = { }; 447 }; 448 }; 449 450 services = { 451 movim = { 452 settings = mkMerge [ 453 { 454 DAEMON_URL = "//${cfg.domain}"; 455 DAEMON_PORT = cfg.port; 456 DAEMON_INTERFACE = "127.0.0.1"; 457 DAEMON_DEBUG = cfg.debug; 458 DAEMON_VERBOSE = cfg.verbose; 459 } 460 (mkIf cfg.database.createLocally { 461 DB_DRIVER = { 462 "postgresql" = "pgsql"; 463 "mysql" = "mysql"; 464 }.${cfg.database.type}; 465 DB_HOST = "localhost"; 466 DB_PORT = config.services.${cfg.database.type}.settings.port; 467 DB_DATABASE = cfg.database.name; 468 DB_USERNAME = cfg.database.user; 469 DB_PASSWORD = ""; 470 }) 471 ]; 472 473 poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) { 474 "pm" = "dynamic"; 475 "php_admin_value[error_log]" = "stderr"; 476 "php_admin_flag[log_errors]" = true; 477 "catch_workers_output" = true; 478 "pm.max_children" = 32; 479 "pm.start_servers" = 2; 480 "pm.min_spare_servers" = 2; 481 "pm.max_spare_servers" = 8; 482 "pm.max_requests" = 500; 483 }; 484 }; 485 486 nginx = mkIf (cfg.nginx != null) { 487 enable = true; 488 recommendedOptimisation = true; 489 recommendedGzipSettings = true; 490 recommendedBrotliSettings = true; 491 recommendedProxySettings = true; 492 # TODO: recommended cache options already in Nginx⁇ 493 appendHttpConfig = /* nginx */ '' 494 fastcgi_cache_path /tmp/nginx_cache levels=1:2 keys_zone=nginx_cache:100m inactive=60m; 495 fastcgi_cache_key "$scheme$request_method$host$request_uri"; 496 ''; 497 virtualHosts."${cfg.domain}" = mkMerge [ 498 cfg.nginx 499 { 500 root = lib.mkForce "${package}/share/php/movim/public"; 501 locations = { 502 "/favicon.ico" = { 503 priority = 100; 504 extraConfig = /* nginx */ '' 505 access_log off; 506 log_not_found off; 507 ''; 508 }; 509 "/robots.txt" = { 510 priority = 100; 511 extraConfig = /* nginx */ '' 512 access_log off; 513 log_not_found off; 514 ''; 515 }; 516 "~ /\\.(?!well-known).*" = { 517 priority = 210; 518 extraConfig = /* nginx */ '' 519 deny all; 520 ''; 521 }; 522 # Ask nginx to cache every URL starting with "/picture" 523 "/picture" = { 524 priority = 400; 525 tryFiles = "$uri $uri/ /index.php$is_args$args"; 526 extraConfig = /* nginx */ '' 527 set $no_cache 0; # Enable cache only there 528 ''; 529 }; 530 "/" = { 531 priority = 490; 532 tryFiles = "$uri $uri/ /index.php$is_args$args"; 533 extraConfig = /* nginx */ '' 534 # https://github.com/movim/movim/issues/314 535 add_header Content-Security-Policy "default-src 'self'; img-src 'self' aesgcm: https:; media-src 'self' aesgcm: https:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"; 536 set $no_cache 1; 537 ''; 538 }; 539 "~ \\.php$" = { 540 priority = 500; 541 tryFiles = "$uri =404"; 542 extraConfig = /* nginx */ '' 543 include ${config.services.nginx.package}/conf/fastcgi.conf; 544 add_header X-Cache $upstream_cache_status; 545 fastcgi_ignore_headers "Cache-Control" "Expires" "Set-Cookie"; 546 fastcgi_cache nginx_cache; 547 fastcgi_cache_valid any 7d; 548 fastcgi_cache_bypass $no_cache; 549 fastcgi_no_cache $no_cache; 550 fastcgi_split_path_info ^(.+\.php)(/.+)$; 551 fastcgi_index index.php; 552 fastcgi_pass unix:${fpm.socket}; 553 ''; 554 }; 555 "/ws/" = { 556 priority = 900; 557 proxyPass = "http://${cfg.settings.DAEMON_INTERFACE}:${builtins.toString cfg.port}/"; 558 proxyWebsockets = true; 559 recommendedProxySettings = true; 560 extraConfig = /* nginx */ '' 561 proxy_set_header X-Forwarded-Proto $scheme; 562 proxy_redirect off; 563 ''; 564 }; 565 }; 566 extraConfig = /* ngnix */ '' 567 index index.php; 568 ''; 569 } 570 ]; 571 }; 572 573 mysql = mkIf (cfg.database.createLocally && cfg.database.type == "mysql") { 574 enable = mkDefault true; 575 package = mkDefault pkgs.mariadb; 576 ensureDatabases = [ cfg.database.name ]; 577 ensureUsers = [{ 578 name = cfg.user; 579 ensureDBOwnership = true; 580 }]; 581 }; 582 583 postgresql = mkIf (cfg.database.createLocally && cfg.database.type == "postgresql") { 584 enable = mkDefault true; 585 ensureDatabases = [ cfg.database.name ]; 586 ensureUsers = [{ 587 name = cfg.user; 588 ensureDBOwnership = true; 589 }]; 590 authentication = '' 591 host ${cfg.database.name} ${cfg.database.user} localhost trust 592 ''; 593 }; 594 595 phpfpm.pools.${pool} = 596 let 597 socketOwner = 598 if (cfg.nginx != null) 599 then config.services.nginx.user 600 else cfg.user; 601 in 602 { 603 phpPackage = package.php; 604 user = cfg.user; 605 group = cfg.group; 606 607 phpOptions = '' 608 error_log = 'stderr' 609 log_errors = on 610 ''; 611 612 settings = { 613 "listen.owner" = socketOwner; 614 "listen.group" = cfg.group; 615 "listen.mode" = "0660"; 616 "catch_workers_output" = true; 617 } // cfg.poolConfig; 618 }; 619 }; 620 621 systemd = { 622 services.movim-data-setup = { 623 description = "Movim setup: .env file, databases init, cache reload"; 624 wantedBy = [ "multi-user.target" ]; 625 requiredBy = [ "${phpExecutionUnit}.service" ]; 626 before = [ "${phpExecutionUnit}.service" ]; 627 after = lib.optional cfg.database.createLocally dbService; 628 requires = lib.optional cfg.database.createLocally dbService; 629 630 serviceConfig = { 631 Type = "oneshot"; 632 User = cfg.user; 633 Group = cfg.group; 634 UMask = "077"; 635 } // lib.optionalAttrs (cfg.secretFile != null) { 636 LoadCredential = "env-secrets:${cfg.secretFile}"; 637 }; 638 639 script = '' 640 # Env vars 641 rm -f ${cfg.dataDir}/.env 642 cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env 643 echo -e '\n' >> ${cfg.dataDir}/.env 644 if [[ -f "$CREDENTIALS_DIRECTORY/env-secrets" ]]; then 645 cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env 646 echo -e '\n' >> ${cfg.dataDir}/.env 647 fi 648 649 # Caches, logs 650 mkdir -p ${cfg.dataDir}/public/cache ${cfg.logDir} ${cfg.runtimeDir}/cache 651 chmod -R ug+rw ${cfg.dataDir}/public/cache 652 chmod -R ug+rw ${cfg.logDir} 653 chmod -R ug+rwx ${cfg.runtimeDir}/cache 654 655 # Migrations 656 MOVIM_VERSION="${package.version}" 657 if [[ ! -f "${cfg.dataDir}/.migration-version" ]] || [[ "$MOVIM_VERSION" != "$(<${cfg.dataDir}/.migration-version)" ]]; then 658 ${package}/bin/movim-composer movim:migrate && echo $MOVIM_VERSION > ${cfg.dataDir}/.migration-version 659 fi 660 '' 661 + lib.optionalString (podConfigFlags != "") ( 662 let 663 flags = lib.concatStringsSep " " 664 ([ "--no-interaction" ] 665 ++ lib.optional cfg.debug "-vvv" 666 ++ lib.optional (!cfg.debug && cfg.verbose) "-v"); 667 in 668 '' 669 ${lib.getExe package} config ${podConfigFlags} 670 '' 671 ); 672 }; 673 674 services.movim = { 675 description = "Movim daemon"; 676 wantedBy = [ "multi-user.target" ]; 677 after = [ "movim-data-setup.service" ]; 678 requires = [ "movim-data-setup.service" ] 679 ++ lib.optional cfg.database.createLocally dbService; 680 environment = { 681 PUBLIC_URL = "//${cfg.domain}"; 682 WS_PORT = builtins.toString cfg.port; 683 }; 684 685 serviceConfig = { 686 User = cfg.user; 687 Group = cfg.group; 688 WorkingDirectory = "${package}/share/php/movim"; 689 ExecStart = "${lib.getExe package} start"; 690 }; 691 }; 692 693 services.${phpExecutionUnit} = { 694 after = [ "movim-data-setup.service" ]; 695 requires = [ "movim-data-setup.service" ] 696 ++ lib.optional cfg.database.createLocally dbService; 697 }; 698 699 tmpfiles.settings."10-movim" = with cfg; { 700 "${dataDir}".d = { inherit user group; mode = "0710"; }; 701 "${dataDir}/public".d = { inherit user group; mode = "0750"; }; 702 "${dataDir}/public/cache".d = { inherit user group; mode = "0750"; }; 703 "${runtimeDir}".d = { inherit user group; mode = "0700"; }; 704 "${runtimeDir}/cache".d = { inherit user group; mode = "0700"; }; 705 "${logDir}".d = { inherit user group; mode = "0700"; }; 706 }; 707 }; 708 }; 709}