diff options
Diffstat (limited to 'nixos/modules/services/web-apps')
25 files changed, 1534 insertions, 685 deletions
diff --git a/nixos/modules/services/web-apps/bookstack.nix b/nixos/modules/services/web-apps/bookstack.nix index d846c98577c8..4999eceb2b60 100644 --- a/nixos/modules/services/web-apps/bookstack.nix +++ b/nixos/modules/services/web-apps/bookstack.nix @@ -412,20 +412,25 @@ in { ''; }; - systemd.tmpfiles.rules = [ - "d ${cfg.dataDir} 0710 ${user} ${group} - -" - "d ${cfg.dataDir}/public 0750 ${user} ${group} - -" - "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -" - "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" - "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" - "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -" - "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" - "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" - "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" - "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" - "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" - "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -" - ]; + systemd.tmpfiles.settings."10-bookstack" = let + defaultConfig = { + inherit user group; + mode = "0700"; + }; + in { + "${cfg.dataDir}".d = defaultConfig // { mode = "0710"; }; + "${cfg.dataDir}/public".d = defaultConfig // { mode = "0750"; }; + "${cfg.dataDir}/public/uploads".d = defaultConfig // { mode = "0750"; }; + "${cfg.dataDir}/storage".d = defaultConfig; + "${cfg.dataDir}/storage/app".d = defaultConfig; + "${cfg.dataDir}/storage/fonts".d = defaultConfig; + "${cfg.dataDir}/storage/framework".d = defaultConfig; + "${cfg.dataDir}/storage/framework/cache".d = defaultConfig; + "${cfg.dataDir}/storage/framework/sessions".d = defaultConfig; + "${cfg.dataDir}/storage/framework/views".d = defaultConfig; + "${cfg.dataDir}/storage/logs".d = defaultConfig; + "${cfg.dataDir}/storage/uploads".d = defaultConfig; + }; users = { users = mkIf (user == "bookstack") { diff --git a/nixos/modules/services/web-apps/engelsystem.nix b/nixos/modules/services/web-apps/engelsystem.nix index 669620debce5..ae7b2b9e7d0c 100644 --- a/nixos/modules/services/web-apps/engelsystem.nix +++ b/nixos/modules/services/web-apps/engelsystem.nix @@ -99,7 +99,6 @@ in { ''; services.phpfpm.pools.engelsystem = { - phpPackage = pkgs.php81; user = "engelsystem"; settings = { "listen.owner" = config.services.nginx.user; diff --git a/nixos/modules/services/web-apps/freshrss.nix b/nixos/modules/services/web-apps/freshrss.nix index c8399143c37b..edec9d547a30 100644 --- a/nixos/modules/services/web-apps/freshrss.nix +++ b/nixos/modules/services/web-apps/freshrss.nix @@ -228,9 +228,10 @@ in }; users.groups."${cfg.user}" = { }; - systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -" - ]; + systemd.tmpfiles.settings."10-freshrss".${cfg.dataDir}.d = { + inherit (cfg) user; + group = config.users.users.${cfg.user}.group; + }; systemd.services.freshrss-config = let diff --git a/nixos/modules/services/web-apps/gotosocial.nix b/nixos/modules/services/web-apps/gotosocial.nix index 45464f646da8..657509c11005 100644 --- a/nixos/modules/services/web-apps/gotosocial.nix +++ b/nixos/modules/services/web-apps/gotosocial.nix @@ -27,7 +27,7 @@ let in { meta.doc = ./gotosocial.md; - meta.maintainers = with lib.maintainers; [ misuzu ]; + meta.maintainers = with lib.maintainers; [ misuzu blakesmith ]; options.services.gotosocial = { enable = lib.mkEnableOption (lib.mdDoc "ActivityPub social network server"); diff --git a/nixos/modules/services/web-apps/komga.nix b/nixos/modules/services/web-apps/komga.nix index 31f475fc7b04..d7ab2a9e612e 100644 --- a/nixos/modules/services/web-apps/komga.nix +++ b/nixos/modules/services/web-apps/komga.nix @@ -1,99 +1,122 @@ -{ config, pkgs, lib, ... }: - -with lib; +{ + config, + pkgs, + lib, + ... +}: let cfg = config.services.komga; - -in { + inherit (lib) mkOption mkEnableOption maintainers; + inherit (lib.types) port str bool; +in +{ options = { services.komga = { - enable = mkEnableOption (lib.mdDoc "Komga, a free and open source comics/mangas media server"); + enable = mkEnableOption "Komga, a free and open source comics/mangas media server"; port = mkOption { - type = types.port; + type = port; default = 8080; - description = lib.mdDoc '' - The port that Komga will listen on. - ''; + description = "The port that Komga will listen on."; }; user = mkOption { - type = types.str; + type = str; default = "komga"; - description = lib.mdDoc '' - User account under which Komga runs. - ''; + description = "User account under which Komga runs."; }; group = mkOption { - type = types.str; + type = str; default = "komga"; - description = lib.mdDoc '' - Group under which Komga runs. - ''; + description = "Group under which Komga runs."; }; stateDir = mkOption { - type = types.str; + type = str; default = "/var/lib/komga"; - description = lib.mdDoc '' - State and configuration directory Komga will use. - ''; + description = "State and configuration directory Komga will use."; }; openFirewall = mkOption { - type = types.bool; + type = bool; default = false; - description = lib.mdDoc '' - Whether to open the firewall for the port in {option}`services.komga.port`. - ''; + description = "Whether to open the firewall for the port in {option}`services.komga.port`."; }; }; }; - config = mkIf cfg.enable { - - networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + config = + let + inherit (lib) mkIf getExe; + in + mkIf cfg.enable { - users.groups = mkIf (cfg.group == "komga") { - komga = {}; - }; + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; - users.users = mkIf (cfg.user == "komga") { - komga = { - group = cfg.group; - home = cfg.stateDir; - description = "Komga Daemon user"; - isSystemUser = true; - }; - }; + users.groups = mkIf (cfg.group == "komga") { komga = { }; }; - systemd.services.komga = { - environment = { - SERVER_PORT = builtins.toString cfg.port; - KOMGA_CONFIGDIR = cfg.stateDir; + users.users = mkIf (cfg.user == "komga") { + komga = { + group = cfg.group; + home = cfg.stateDir; + description = "Komga Daemon user"; + isSystemUser = true; + }; }; - description = "Komga is a free and open source comics/mangas media server"; - - wantedBy = [ "multi-user.target" ]; - wants = [ "network-online.target" ]; - after = [ "network-online.target" ]; - - serviceConfig = { - User = cfg.user; - Group = cfg.group; - - Type = "simple"; - Restart = "on-failure"; - ExecStart = "${pkgs.komga}/bin/komga"; - - StateDirectory = mkIf (cfg.stateDir == "/var/lib/komga") "komga"; + systemd.services.komga = { + environment = { + SERVER_PORT = builtins.toString cfg.port; + KOMGA_CONFIGDIR = cfg.stateDir; + }; + + description = "Komga is a free and open source comics/mangas media server"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + + Type = "simple"; + Restart = "on-failure"; + ExecStart = getExe pkgs.komga; + + StateDirectory = mkIf (cfg.stateDir == "/var/lib/komga") "komga"; + + RemoveIPC = true; + NoNewPrivileges = true; + CapabilityBoundingSet = ""; + SystemCallFilter = [ "@system-service" ]; + ProtectSystem = "full"; + PrivateTmp = true; + ProtectProc = "invisible"; + ProtectClock = true; + ProcSubset = "pid"; + PrivateUsers = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + LockPersonality = true; + RestrictNamespaces = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + SystemCallArchitectures = "native"; + RestrictSUIDSGID = true; + RestrictRealtime = true; + }; }; - }; - }; meta.maintainers = with maintainers; [ govanify ]; } diff --git a/nixos/modules/services/web-apps/lemmy.nix b/nixos/modules/services/web-apps/lemmy.nix index bde9051a7033..968dcac93fab 100644 --- a/nixos/modules/services/web-apps/lemmy.nix +++ b/nixos/modules/services/web-apps/lemmy.nix @@ -204,7 +204,6 @@ in }; "/" = { # mixed frontend and backend requests, based on the request headers - proxyPass = "$proxpass"; recommendedProxySettings = true; extraConfig = '' set $proxpass "${ui}"; @@ -220,6 +219,8 @@ in # Cuts off the trailing slash on URLs to make them valid rewrite ^(.+)/+$ $1 permanent; + + proxy_pass $proxpass; ''; }; }; diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix index 538e728fcc72..7fc710c6fcec 100644 --- a/nixos/modules/services/web-apps/mastodon.nix +++ b/nixos/modules/services/web-apps/mastodon.nix @@ -4,7 +4,8 @@ let cfg = config.services.mastodon; opt = options.services.mastodon; - # We only want to create a database if we're actually going to connect to it. + # We only want to create a Redis and PostgreSQL databases if we're actually going to connect to it local. + redisActuallyCreateLocally = cfg.redis.createLocally && (cfg.redis.host == "127.0.0.1" || cfg.redis.enableUnixSocket); databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql"; env = { @@ -33,6 +34,7 @@ let TRUSTED_PROXY_IP = cfg.trustedProxy; } + // lib.optionalAttrs (cfg.redis.createLocally && cfg.redis.enableUnixSocket) { REDIS_URL = "unix://${config.services.redis.servers.mastodon.unixSocket}"; } // lib.optionalAttrs (cfg.database.host != "/run/postgresql" && cfg.database.port != null) { DB_PORT = toString cfg.database.port; } // lib.optionalAttrs cfg.smtp.authenticate { SMTP_LOGIN = cfg.smtp.user; } // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_HOST = cfg.elasticsearch.host; } @@ -116,9 +118,11 @@ let threads = toString (if processCfg.threads == null then cfg.sidekiqThreads else processCfg.threads); in { after = [ "network.target" "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; requires = [ "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; description = "Mastodon sidekiq${jobClassLabel}"; @@ -133,6 +137,7 @@ let RestartSec = 20; EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles; WorkingDirectory = cfg.package; + LimitNOFILE = "1024000"; # System Call Filtering SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ]; } // cfgService; @@ -145,9 +150,11 @@ let name = "mastodon-streaming-${toString i}"; value = { after = [ "network.target" "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; requires = [ "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; wantedBy = [ "mastodon.target" "mastodon-streaming.target" ]; @@ -403,6 +410,19 @@ in { type = lib.types.port; default = 31637; }; + + passwordFile = lib.mkOption { + description = lib.mdDoc "A file containing the password for Redis database."; + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/keys/mastodon-redis-password"; + }; + + enableUnixSocket = lib.mkOption { + description = lib.mdDoc "Use Unix socket"; + type = lib.types.bool; + default = true; + }; }; database = { @@ -612,6 +632,13 @@ in { config = lib.mkIf cfg.enable (lib.mkMerge [{ assertions = [ { + assertion = redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || cfg.redis.passwordFile == null); + message = '' + <option>services.mastodon.redis.enableUnixSocket</option> needs to be disabled if + <option>services.mastodon.redis.passwordFile</option> is used. + ''; + } + { assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user && cfg.database.user == cfg.database.name); message = '' For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer @@ -688,6 +715,8 @@ in { OTP_SECRET="$(cat ${cfg.otpSecretFile})" VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})" VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})" + '' + lib.optionalString (cfg.redis.passwordFile != null)'' + REDIS_PASSWORD="$(cat ${cfg.redis.passwordFile})" '' + lib.optionalString (cfg.database.passwordFile != null) '' DB_PASS="$(cat ${cfg.database.passwordFile})" '' + lib.optionalString cfg.smtp.authenticate '' @@ -750,9 +779,11 @@ in { systemd.services.mastodon-web = { after = [ "network.target" "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; requires = [ "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; wantedBy = [ "mastodon.target" ]; @@ -833,11 +864,14 @@ in { enable = true; hostname = lib.mkDefault "${cfg.localDomain}"; }; - services.redis.servers.mastodon = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") { - enable = true; - port = cfg.redis.port; - bind = "127.0.0.1"; - }; + services.redis.servers.mastodon = lib.mkIf redisActuallyCreateLocally (lib.mkMerge [ + { + enable = true; + } + (lib.mkIf (!cfg.redis.enableUnixSocket) { + port = cfg.redis.port; + }) + ]); services.postgresql = lib.mkIf databaseActuallyCreateLocally { enable = true; ensureUsers = [ @@ -858,6 +892,7 @@ in { }; }) (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package pkgs.imagemagick ]) + (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {${config.services.mastodon.user}.extraGroups = [ "redis-mastodon" ];}) ]; users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user; diff --git a/nixos/modules/services/web-apps/mattermost.nix b/nixos/modules/services/web-apps/mattermost.nix index 503559432374..3d03c96d1c19 100644 --- a/nixos/modules/services/web-apps/mattermost.nix +++ b/nixos/modules/services/web-apps/mattermost.nix @@ -277,9 +277,7 @@ in # The systemd service will fail to execute the preStart hook # if the WorkingDirectory does not exist - systemd.tmpfiles.rules = [ - ''d "${cfg.statePath}" -'' - ]; + systemd.tmpfiles.settings."10-mattermost".${cfg.statePath}.d = { }; systemd.services.mattermost = { description = "Mattermost chat service"; diff --git a/nixos/modules/services/web-apps/mealie.nix b/nixos/modules/services/web-apps/mealie.nix new file mode 100644 index 000000000000..8bb7542c6b56 --- /dev/null +++ b/nixos/modules/services/web-apps/mealie.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ...}: +let + cfg = config.services.mealie; + pkg = cfg.package; +in +{ + options.services.mealie = { + enable = lib.mkEnableOption "Mealie, a recipe manager and meal planner"; + + package = lib.mkPackageOption pkgs "mealie" { }; + + listenAddress = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "Address on which the service should listen."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 9000; + description = "Port on which to serve the Mealie service."; + }; + + settings = lib.mkOption { + type = with lib.types; attrsOf anything; + default = {}; + description = lib.mdDoc '' + Configuration of the Mealie service. + + See [the mealie documentation](https://nightly.mealie.io/documentation/getting-started/installation/backend-config/) for available options and default values. + + In addition to the official documentation, you can set {env}`MEALIE_LOG_FILE`. + ''; + example = { + ALLOW_SIGNUP = "false"; + }; + }; + + credentialsFile = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + example = "/run/secrets/mealie-credentials.env"; + description = '' + File containing credentials used in mealie such as {env}`POSTGRES_PASSWORD` + or sensitive LDAP options. + + Expects the format of an `EnvironmentFile=`, as described by {manpage}`systemd.exec(5)`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.mealie = { + description = "Mealie, a self hosted recipe manager and meal planner"; + + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + PRODUCTION = "true"; + ALEMBIC_CONFIG_FILE="${pkg}/config/alembic.ini"; + API_PORT = toString cfg.port; + DATA_DIR = "/var/lib/mealie"; + CRF_MODEL_PATH = "/var/lib/mealie/model.crfmodel"; + } // (builtins.mapAttrs (_: val: toString val) cfg.settings); + + serviceConfig = { + DynamicUser = true; + User = "mealie"; + ExecStartPre = "${pkg}/libexec/init_db"; + ExecStart = "${lib.getExe pkg} -b ${cfg.listenAddress}:${builtins.toString cfg.port}"; + EnvironmentFile = lib.mkIf (cfg.credentialsFile != null) cfg.credentialsFile; + StateDirectory = "mealie"; + StandardOutput="journal"; + }; + }; + }; +} diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix index 1a5b7d0c24e9..16b6fb0d655d 100644 --- a/nixos/modules/services/web-apps/miniflux.nix +++ b/nixos/modules/services/web-apps/miniflux.nix @@ -16,10 +16,20 @@ in { options = { services.miniflux = { - enable = mkEnableOption (lib.mdDoc "miniflux and creates a local postgres database for it"); + enable = mkEnableOption (lib.mdDoc "miniflux"); package = mkPackageOption pkgs "miniflux" { }; + createDatabaseLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether a PostgreSQL database should be automatically created and + configured on the local host. If set to `false`, you need provision a + database yourself and make sure to create the hstore extension in it. + ''; + }; + config = mkOption { type = with types; attrsOf (oneOf [ str int ]); example = literalExpression '' @@ -38,7 +48,7 @@ in ''; }; - adminCredentialsFile = mkOption { + adminCredentialsFile = mkOption { type = types.path; description = lib.mdDoc '' File containing the ADMIN_USERNAME and @@ -51,14 +61,14 @@ in }; config = mkIf cfg.enable { - services.miniflux.config = { + services.miniflux.config = { LISTEN_ADDR = mkDefault defaultAddress; - DATABASE_URL = "user=miniflux host=/run/postgresql dbname=miniflux"; + DATABASE_URL = lib.mkIf cfg.createDatabaseLocally "user=miniflux host=/run/postgresql dbname=miniflux"; RUN_MIGRATIONS = 1; CREATE_ADMIN = 1; }; - services.postgresql = { + services.postgresql = lib.mkIf cfg.createDatabaseLocally { enable = true; ensureUsers = [ { name = "miniflux"; @@ -67,7 +77,7 @@ in ensureDatabases = [ "miniflux" ]; }; - systemd.services.miniflux-dbsetup = { + systemd.services.miniflux-dbsetup = lib.mkIf cfg.createDatabaseLocally { description = "Miniflux database setup"; requires = [ "postgresql.service" ]; after = [ "network.target" "postgresql.service" ]; @@ -81,8 +91,9 @@ in systemd.services.miniflux = { description = "Miniflux service"; wantedBy = [ "multi-user.target" ]; - requires = [ "miniflux-dbsetup.service" ]; - after = [ "network.target" "postgresql.service" "miniflux-dbsetup.service" ]; + requires = lib.optional cfg.createDatabaseLocally "miniflux-dbsetup.service"; + after = [ "network.target" ] + ++ lib.optionals cfg.createDatabaseLocally [ "postgresql.service" "miniflux-dbsetup.service" ]; serviceConfig = { ExecStart = "${cfg.package}/bin/miniflux"; @@ -129,6 +140,7 @@ in include "${pkgs.apparmorRulesFromClosure { name = "miniflux"; } cfg.package}" r ${cfg.package}/bin/miniflux, r @{sys}/kernel/mm/transparent_hugepage/hpage_pmd_size, + rw /run/miniflux/**, } ''; }; diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix index ce6a80054725..496a0e32436f 100644 --- a/nixos/modules/services/web-apps/moodle.nix +++ b/nixos/modules/services/web-apps/moodle.nix @@ -255,9 +255,10 @@ in } ]; }; - systemd.tmpfiles.rules = [ - "d '${stateDir}' 0750 ${user} ${group} - -" - ]; + systemd.tmpfiles.settings."10-moodle".${stateDir}.d = { + inherit user group; + mode = "0750"; + }; systemd.services.moodle-init = { wantedBy = [ "multi-user.target" ]; diff --git a/nixos/modules/services/web-apps/nextcloud-notify_push.nix b/nixos/modules/services/web-apps/nextcloud-notify_push.nix index 759daa0c50dc..7b90e0bbaa9b 100644 --- a/nixos/modules/services/web-apps/nextcloud-notify_push.nix +++ b/nixos/modules/services/web-apps/nextcloud-notify_push.nix @@ -116,7 +116,7 @@ in } (lib.mkIf cfg.bendDomainToLocalhost { - nextcloud.extraOptions.trusted_proxies = [ "127.0.0.1" "::1" ]; + nextcloud.settings.trusted_proxies = [ "127.0.0.1" "::1" ]; }) ]; }; diff --git a/nixos/modules/services/web-apps/nextcloud.md b/nixos/modules/services/web-apps/nextcloud.md index ce8f96a6a389..5db83d7e4463 100644 --- a/nixos/modules/services/web-apps/nextcloud.md +++ b/nixos/modules/services/web-apps/nextcloud.md @@ -51,7 +51,7 @@ to ensure that changes can be applied by changing the module's options. In case the application serves multiple domains (those are checked with [`$_SERVER['HTTP_HOST']`](https://www.php.net/manual/en/reserved.variables.server.php)) it's needed to add them to -[`services.nextcloud.extraOptions.trusted_domains`](#opt-services.nextcloud.extraOptions.trusted_domains). +[`services.nextcloud.settings.trusted_domains`](#opt-services.nextcloud.settings.trusted_domains). Auto updates for Nextcloud apps can be enabled using [`services.nextcloud.autoUpdateApps`](#opt-services.nextcloud.autoUpdateApps.enable). diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index 0b19265942c0..7f998207c434 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -14,7 +14,6 @@ let expose_php = "Off"; error_reporting = "E_ALL & ~E_DEPRECATED & ~E_STRICT"; display_errors = "stderr"; - "opcache.enable_cli" = "1"; "opcache.interned_strings_buffer" = "8"; "opcache.max_accelerated_files" = "10000"; "opcache.memory_consumption" = "128"; @@ -45,7 +44,7 @@ let }; }; - webroot = pkgs.runCommand + webroot = pkgs.runCommandLocal "${cfg.package.name or "nextcloud"}-with-apps" { } '' @@ -183,8 +182,8 @@ let ]; $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file( - "${jsonFormat.generate "nextcloud-extraOptions.json" cfg.extraOptions}", - "impossible: this should never happen (decoding generated extraOptions file %s failed)" + "${jsonFormat.generate "nextcloud-settings.json" cfg.settings}", + "impossible: this should never happen (decoding generated settings file %s failed)" )); ${optionalString (cfg.secretFile != null) '' @@ -205,21 +204,22 @@ in { Add port to services.nextcloud.config.dbhost instead. '') (mkRenamedOptionModule - [ "services" "nextcloud" "logLevel" ] [ "services" "nextcloud" "extraOptions" "loglevel" ]) + [ "services" "nextcloud" "logLevel" ] [ "services" "nextcloud" "settings" "loglevel" ]) (mkRenamedOptionModule - [ "services" "nextcloud" "logType" ] [ "services" "nextcloud" "extraOptions" "log_type" ]) + [ "services" "nextcloud" "logType" ] [ "services" "nextcloud" "settings" "log_type" ]) (mkRenamedOptionModule - [ "services" "nextcloud" "config" "defaultPhoneRegion" ] [ "services" "nextcloud" "extraOptions" "default_phone_region" ]) + [ "services" "nextcloud" "config" "defaultPhoneRegion" ] [ "services" "nextcloud" "settings" "default_phone_region" ]) (mkRenamedOptionModule - [ "services" "nextcloud" "config" "overwriteProtocol" ] [ "services" "nextcloud" "extraOptions" "overwriteprotocol" ]) + [ "services" "nextcloud" "config" "overwriteProtocol" ] [ "services" "nextcloud" "settings" "overwriteprotocol" ]) (mkRenamedOptionModule - [ "services" "nextcloud" "skeletonDirectory" ] [ "services" "nextcloud" "extraOptions" "skeletondirectory" ]) + [ "services" "nextcloud" "skeletonDirectory" ] [ "services" "nextcloud" "settings" "skeletondirectory" ]) (mkRenamedOptionModule - [ "services" "nextcloud" "globalProfiles" ] [ "services" "nextcloud" "extraOptions" "profile.enabled" ]) + [ "services" "nextcloud" "globalProfiles" ] [ "services" "nextcloud" "settings" "profile.enabled" ]) (mkRenamedOptionModule - [ "services" "nextcloud" "config" "extraTrustedDomains" ] [ "services" "nextcloud" "extraOptions" "trusted_domains" ]) + [ "services" "nextcloud" "config" "extraTrustedDomains" ] [ "services" "nextcloud" "settings" "trusted_domains" ]) (mkRenamedOptionModule - [ "services" "nextcloud" "config" "trustedProxies" ] [ "services" "nextcloud" "extraOptions" "trusted_proxies" ]) + [ "services" "nextcloud" "config" "trustedProxies" ] [ "services" "nextcloud" "settings" "trusted_proxies" ]) + (mkRenamedOptionModule ["services" "nextcloud" "extraOptions" ] [ "services" "nextcloud" "settings" ]) ]; options.services.nextcloud = { @@ -648,7 +648,7 @@ in { ''; }; - extraOptions = mkOption { + settings = mkOption { type = types.submodule { freeformType = jsonFormat.type; options = { @@ -770,7 +770,7 @@ in { default = null; description = lib.mdDoc '' Secret options which will be appended to Nextcloud's config.php file (written as JSON, in the same - form as the [](#opt-services.nextcloud.extraOptions) option), for example + form as the [](#opt-services.nextcloud.settings) option), for example `{"redis":{"password":"secret"}}`. ''; }; @@ -872,9 +872,11 @@ in { { systemd.timers.nextcloud-cron = { wantedBy = [ "timers.target" ]; after = [ "nextcloud-setup.service" ]; - timerConfig.OnBootSec = "5m"; - timerConfig.OnUnitActiveSec = "5m"; - timerConfig.Unit = "nextcloud-cron.service"; + timerConfig = { + OnBootSec = "5m"; + OnUnitActiveSec = "5m"; + Unit = "nextcloud-cron.service"; + }; }; systemd.tmpfiles.rules = map (dir: "d ${dir} 0750 nextcloud nextcloud - -") [ @@ -930,7 +932,7 @@ in { (i: v: '' ${occ}/bin/nextcloud-occ config:system:set trusted_domains \ ${toString i} --value="${toString v}" - '') ([ cfg.hostName ] ++ cfg.extraOptions.trusted_domains)); + '') ([ cfg.hostName ] ++ cfg.settings.trusted_domains)); in { wantedBy = [ "multi-user.target" ]; @@ -991,15 +993,21 @@ in { nextcloud-cron = { after = [ "nextcloud-setup.service" ]; environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; - serviceConfig.Type = "oneshot"; - serviceConfig.User = "nextcloud"; - serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${webroot}/cron.php"; + serviceConfig = { + Type = "oneshot"; + User = "nextcloud"; + ExecCondition = "${lib.getExe phpPackage} -f ${webroot}/occ status -e"; + ExecStart = "${lib.getExe phpPackage} -f ${webroot}/cron.php"; + KillMode = "process"; + }; }; nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable { after = [ "nextcloud-setup.service" ]; - serviceConfig.Type = "oneshot"; - serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all"; - serviceConfig.User = "nextcloud"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${occ}/bin/nextcloud-occ app:update --all"; + User = "nextcloud"; + }; startAt = cfg.autoUpdateApps.startAt; }; }; @@ -1056,7 +1064,7 @@ in { services.nextcloud = { caching.redis = lib.mkIf cfg.configureRedis true; - extraOptions = mkMerge [({ + settings = mkMerge [({ datadirectory = lib.mkDefault "${datadir}/data"; trusted_domains = [ cfg.hostName ]; }) (lib.mkIf cfg.configureRedis { diff --git a/nixos/modules/services/web-apps/nifi.nix b/nixos/modules/services/web-apps/nifi.nix index 5ce561077836..c0fc443f0df7 100644 --- a/nixos/modules/services/web-apps/nifi.nix +++ b/nixos/modules/services/web-apps/nifi.nix @@ -163,10 +163,15 @@ in { Please do not disable HTTPS mode in production. In this mode, access to the nifi is opened without authentication. ''; - systemd.tmpfiles.rules = [ - "d '/var/lib/nifi/conf' 0750 ${cfg.user} ${cfg.group}" - "L+ '/var/lib/nifi/lib' - - - - ${cfg.package}/lib" - ]; + systemd.tmpfiles.settings."10-nifi" = { + "/var/lib/nifi/conf".d = { + inherit (cfg) user group; + mode = "0750"; + }; + "/var/lib/nifi/lib"."L+" = { + argument = "${cfg.package}/lib"; + }; + }; systemd.services.nifi = { diff --git a/nixos/modules/services/web-apps/photoprism.nix b/nixos/modules/services/web-apps/photoprism.nix index e25b03484424..39eb7c65c635 100644 --- a/nixos/modules/services/web-apps/photoprism.nix +++ b/nixos/modules/services/web-apps/photoprism.nix @@ -12,14 +12,14 @@ let lib.mapAttrs (_: toString) cfg.settings ); - manage = - let - setupEnv = lib.concatStringsSep "\n" (lib.mapAttrsToList (name: val: "export ${name}=${lib.escapeShellArg val}") env); - in - pkgs.writeShellScript "manage" '' - ${setupEnv} - exec ${cfg.package}/bin/photoprism "$@" - ''; + manage = pkgs.writeShellScript "manage" '' + set -o allexport # Export the following env vars + ${lib.toShellVars env} + eval "$(${config.systemd.package}/bin/systemctl show -pUID,MainPID photoprism.service | ${pkgs.gnused}/bin/sed "s/UID/ServiceUID/")" + exec ${pkgs.util-linux}/bin/nsenter \ + -t $MainPID -m -S $ServiceUID -G $ServiceUID --wdns=${cfg.storagePath} \ + ${cfg.package}/bin/photoprism "$@" + ''; in { meta.maintainers = with lib.maintainers; [ stunkymonkey ]; @@ -104,6 +104,7 @@ in StateDirectory = "photoprism"; WorkingDirectory = "/var/lib/photoprism"; RuntimeDirectory = "photoprism"; + ReadWritePaths = [ cfg.originalsPath cfg.importPath cfg.storagePath ]; LoadCredential = lib.optionalString (cfg.passwordFile != null) "PHOTOPRISM_ADMIN_PASSWORD:${cfg.passwordFile}"; diff --git a/nixos/modules/services/web-apps/pretalx.nix b/nixos/modules/services/web-apps/pretalx.nix new file mode 100644 index 000000000000..ff6218112d2f --- /dev/null +++ b/nixos/modules/services/web-apps/pretalx.nix @@ -0,0 +1,415 @@ +{ config +, lib +, pkgs +, utils +, ... +}: + +let + cfg = config.services.pretalx; + format = pkgs.formats.ini { }; + + configFile = format.generate "pretalx.cfg" cfg.settings; + + extras = cfg.package.optional-dependencies.redis + ++ lib.optionals (cfg.settings.database.backend == "mysql") cfg.package.optional-dependencies.mysql + ++ lib.optionals (cfg.settings.database.backend == "postgresql") cfg.package.optional-dependencies.postgres; + + pythonEnv = cfg.package.python.buildEnv.override { + extraLibs = [ (cfg.package.python.pkgs.toPythonModule cfg.package) ] + ++ (with cfg.package.python.pkgs; [ gunicorn ] + ++ lib.optional cfg.celery.enable celery) ++ extras; + }; +in + +{ + meta = with lib; { + maintainers = teams.c3d2.members; + }; + + options.services.pretalx = { + enable = lib.mkEnableOption (lib.mdDoc "pretalx"); + + package = lib.mkPackageOptionMD pkgs "pretalx" {}; + + group = lib.mkOption { + type = lib.types.str; + default = "pretalx"; + description = "Group under which pretalx should run."; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "pretalx"; + description = "User under which pretalx should run."; + }; + + gunicorn.extraArgs = lib.mkOption { + type = with lib.types; listOf str; + default = [ + "--name=pretalx" + ]; + example = [ + "--name=pretalx" + "--workers=4" + "--max-requests=1200" + "--max-requests-jitter=50" + "--log-level=info" + ]; + description = lib.mdDoc '' + Extra arguments to pass to gunicorn. + See <https://docs.pretalx.org/administrator/installation.html#step-6-starting-pretalx-as-a-service> for details. + ''; + apply = lib.escapeShellArgs; + }; + + celery = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = lib.mdDoc '' + Whether to set up celery as an asynchronous task runner. + ''; + }; + + extraArgs = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + description = lib.mdDoc '' + Extra arguments to pass to celery. + + See <https://docs.celeryq.dev/en/stable/reference/cli.html#celery-worker> for more info. + ''; + apply = utils.escapeSystemdExecArgs; + }; + }; + + nginx = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = lib.mdDoc '' + Whether to set up an nginx virtual host. + ''; + }; + + domain = lib.mkOption { + type = lib.types.str; + example = "talks.example.com"; + description = lib.mdDoc '' + The domain name under which to set up the virtual host. + ''; + }; + }; + + database.createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = lib.mdDoc '' + Whether to automatically set up the database on the local DBMS instance. + + Currently only supported for PostgreSQL. Not required for sqlite. + ''; + }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = format.type; + options = { + database = { + backend = lib.mkOption { + type = lib.types.enum [ + "postgresql" + ]; + default = "postgresql"; + description = lib.mdDoc '' + Database backend to use. + + Currently only PostgreSQL gets tested, and as such we don't support any other DBMS. + ''; + readOnly = true; # only postgres supported right now + }; + + host = lib.mkOption { + type = with lib.types; nullOr types.path; + default = if cfg.settings.database.backend == "postgresql" then "/run/postgresql" + else if cfg.settings.database.backend == "mysql" then "/run/mysqld/mysqld.sock" + else null; + defaultText = lib.literalExpression '' + if config.services.pretalx.settings..database.backend == "postgresql" then "/run/postgresql" + else if config.services.pretalx.settings.database.backend == "mysql" then "/run/mysqld/mysqld.sock" + else null + ''; + description = lib.mdDoc '' + Database host or socket path. + ''; + }; + + name = lib.mkOption { + type = lib.types.str; + default = "pretalx"; + description = lib.mdDoc '' + Database name. + ''; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "pretalx"; + description = lib.mdDoc '' + Database username. + ''; + }; + }; + + filesystem = { + data = lib.mkOption { + type = lib.types.path; + default = "/var/lib/pretalx"; + description = lib.mdDoc '' + Base path for all other storage paths. + ''; + }; + logs = lib.mkOption { + type = lib.types.path; + default = "/var/log/pretalx"; + description = lib.mdDoc '' + Path to the log directory, that pretalx logs message to. + ''; + }; + static = lib.mkOption { + type = lib.types.path; + default = "${cfg.package.static}/"; + defaultText = lib.literalExpression "\${config.services.pretalx.package}.static}/"; + readOnly = true; + description = lib.mdDoc '' + Path to the directory that contains static files. + ''; + }; + }; + + celery = { + backend = lib.mkOption { + type = with lib.types; nullOr str; + default = lib.optionalString cfg.celery.enable "redis+socket://${config.services.redis.servers.pretalx.unixSocket}?virtual_host=1"; + defaultText = lib.literalExpression '' + optionalString config.services.pretalx.celery.enable "redis+socket://''${config.services.redis.servers.pretalx.unixSocket}?virtual_host=1" + ''; + description = lib.mdDoc '' + URI to the celery backend used for the asynchronous job queue. + ''; + }; + + broker = lib.mkOption { + type = with lib.types; nullOr str; + default = lib.optionalString cfg.celery.enable "redis+socket://${config.services.redis.servers.pretalx.unixSocket}?virtual_host=2"; + defaultText = lib.literalExpression '' + optionalString config.services.pretalx.celery.enable "redis+socket://''${config.services.redis.servers.pretalx.unixSocket}?virtual_host=2" + ''; + description = lib.mdDoc '' + URI to the celery broker used for the asynchronous job queue. + ''; + }; + }; + + redis = { + location = lib.mkOption { + type = with lib.types; nullOr str; + default = "unix://${config.services.redis.servers.pretalx.unixSocket}?db=0"; + defaultText = lib.literalExpression '' + "unix://''${config.services.redis.servers.pretalx.unixSocket}?db=0" + ''; + description = lib.mdDoc '' + URI to the redis server, used to speed up locking, caching and session storage. + ''; + }; + + session = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = lib.mdDoc '' + Whether to use redis as the session storage. + ''; + }; + }; + + site = { + url = lib.mkOption { + type = lib.types.str; + default = "https://${cfg.nginx.domain}"; + defaultText = lib.literalExpression "https://\${config.services.pretalx.nginx.domain}"; + example = "https://talks.example.com"; + description = lib.mdDoc '' + The base URI below which your pretalx instance will be reachable. + ''; + }; + }; + }; + }; + default = { }; + description = lib.mdDoc '' + pretalx configuration as a Nix attribute set. All settings can also be passed + from the environment. + + See <https://docs.pretalx.org/administrator/configure.html> for possible options. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + # https://docs.pretalx.org/administrator/installation.html + + environment.systemPackages = [ + (pkgs.writeScriptBin "pretalx-manage" '' + cd ${cfg.settings.filesystem.data} + sudo=exec + if [[ "$USER" != ${cfg.user} ]]; then + sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env=PRETALX_CONFIG_FILE' + fi + export PRETALX_CONFIG_FILE=${configFile} + $sudo ${lib.getExe' pythonEnv "pretalx-manage"} "$@" + '') + ]; + + services = { + nginx = lib.mkIf cfg.nginx.enable { + enable = true; + recommendedGzipSettings = lib.mkDefault true; + recommendedOptimisation = lib.mkDefault true; + recommendedProxySettings = lib.mkDefault true; + recommendedTlsSettings = lib.mkDefault true; + upstreams.pretalx.servers."unix:/run/pretalx/pretalx.sock" = { }; + virtualHosts.${cfg.nginx.domain} = { + # https://docs.pretalx.org/administrator/installation.html#step-7-ssl + extraConfig = '' + more_set_headers Referrer-Policy same-origin; + more_set_headers X-Content-Type-Options nosniff; + ''; + locations = { + "/".proxyPass = "http://pretalx"; + "/media/" = { + alias = "${cfg.settings.filesystem.data}/data/media/"; + extraConfig = '' + access_log off; + more_set_headers Content-Disposition 'attachment; filename="$1"'; + expires 7d; + ''; + }; + "/static/" = { + alias = cfg.settings.filesystem.static; + extraConfig = '' + access_log off; + more_set_headers Cache-Control "public"; + expires 365d; + ''; + }; + }; + }; + }; + + postgresql = lib.mkIf (cfg.database.createLocally && cfg.settings.database.backend == "postgresql") { + enable = true; + ensureUsers = [ { + name = cfg.settings.database.user; + ensureDBOwnership = true; + } ]; + ensureDatabases = [ cfg.settings.database.name ]; + }; + + redis.servers.pretalx.enable = true; + }; + + systemd.services = let + commonUnitConfig = { + environment.PRETALX_CONFIG_FILE = configFile; + serviceConfig = { + User = "pretalx"; + Group = "pretalx"; + StateDirectory = [ "pretalx" "pretalx/media" ]; + LogsDirectory = "pretalx"; + WorkingDirectory = cfg.settings.filesystem.data; + SupplementaryGroups = [ "redis-pretalx" ]; + }; + }; + in { + pretalx-web = lib.recursiveUpdate commonUnitConfig { + description = "pretalx web service"; + after = [ + "network.target" + "redis-pretalx.service" + ] ++ lib.optionals (cfg.settings.database.backend == "postgresql") [ + "postgresql.service" + ] ++ lib.optionals (cfg.settings.database.backend == "mysql") [ + "mysql.service" + ]; + wantedBy = [ "multi-user.target" ]; + preStart = '' + versionFile="${cfg.settings.filesystem.data}/.version" + version=$(cat "$versionFile" 2>/dev/null || echo 0) + + if [[ $version != ${cfg.package.version} ]]; then + ${lib.getExe' pythonEnv "pretalx-manage"} migrate + + echo "${cfg.package.version}" > "$versionFile" + fi + ''; + serviceConfig = { + ExecStart = "${lib.getExe' pythonEnv "gunicorn"} --bind unix:/run/pretalx/pretalx.sock ${cfg.gunicorn.extraArgs} pretalx.wsgi"; + RuntimeDirectory = "pretalx"; + }; + }; + + pretalx-periodic = lib.recursiveUpdate commonUnitConfig { + description = "pretalx periodic task runner"; + # every 15 minutes + startAt = [ "*:3,18,33,48" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${lib.getExe' pythonEnv "pretalx-manage"} runperiodic"; + }; + }; + + pretalx-clear-sessions = lib.recursiveUpdate commonUnitConfig { + description = "pretalx session pruning"; + startAt = [ "monthly" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${lib.getExe' pythonEnv "pretalx-manage"} clearsessions"; + }; + }; + + pretalx-worker = lib.mkIf cfg.celery.enable (lib.recursiveUpdate commonUnitConfig { + description = "pretalx asynchronous job runner"; + after = [ + "network.target" + "redis-pretalx.service" + ] ++ lib.optionals (cfg.settings.database.backend == "postgresql") [ + "postgresql.service" + ] ++ lib.optionals (cfg.settings.database.backend == "mysql") [ + "mysql.service" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.ExecStart = "${lib.getExe' pythonEnv "celery"} -A pretalx.celery_app worker ${cfg.celery.extraArgs}"; + }); + }; + + systemd.sockets.pretalx-web.socketConfig = { + ListenStream = "/run/pretalx/pretalx.sock"; + SocketUser = "nginx"; + }; + + users = { + groups."${cfg.group}" = {}; + users."${cfg.user}" = { + isSystemUser = true; + createHome = true; + home = cfg.settings.filesystem.data; + inherit (cfg) group; + }; + }; + }; +} diff --git a/nixos/modules/services/web-apps/pretix.nix b/nixos/modules/services/web-apps/pretix.nix new file mode 100644 index 000000000000..500b2eb5416b --- /dev/null +++ b/nixos/modules/services/web-apps/pretix.nix @@ -0,0 +1,580 @@ +{ config +, lib +, pkgs +, utils +, ... +}: + +let + inherit (lib) + concatMapStringsSep + escapeShellArgs + filter + filterAttrs + getExe + getExe' + isAttrs + isList + literalExpression + mapAttrs + mkDefault + mkEnableOption + mkIf + mkOption + mkPackageOption + optionals + optionalString + recursiveUpdate + types + ; + + filterRecursiveNull = o: + if isAttrs o then + mapAttrs (_: v: filterRecursiveNull v) (filterAttrs (_: v: v != null) o) + else if isList o then + map filterRecursiveNull (filter (v: v != null) o) + else + o; + + cfg = config.services.pretix; + format = pkgs.formats.ini { }; + + configFile = format.generate "pretix.cfg" (filterRecursiveNull cfg.settings); + + finalPackage = cfg.package.override { + inherit (cfg) plugins; + }; + + pythonEnv = cfg.package.python.buildEnv.override { + extraLibs = with cfg.package.python.pkgs; [ + (toPythonModule finalPackage) + gunicorn + ] + ++ lib.optionals (cfg.settings.memcached.location != null) + cfg.package.optional-dependencies.memcached + ; + }; + + withRedis = cfg.settings.redis.location != null; +in +{ + meta = with lib; { + maintainers = with maintainers; [ hexa ]; + }; + + options.services.pretix = { + enable = mkEnableOption "pretix"; + + package = mkPackageOption pkgs "pretix" { }; + + group = mkOption { + type = types.str; + default = "pretix"; + description = '' + Group under which pretix should run. + ''; + }; + + user = mkOption { + type = types.str; + default = "pretix"; + description = '' + User under which pretix should run. + ''; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/pretix-secrets.env"; + description = '' + Environment file to pass secret configuration values. + + Each line must follow the `PRETIX_SECTION_KEY=value` pattern. + ''; + }; + + plugins = mkOption { + type = types.listOf types.package; + default = []; + example = literalExpression '' + with config.services.pretix.package.plugins; [ + passbook + pages + ]; + ''; + description = '' + Pretix plugins to install into the Python environment. + ''; + }; + + gunicorn.extraArgs = mkOption { + type = with types; listOf str; + default = [ + "--name=pretix" + ]; + example = [ + "--name=pretix" + "--workers=4" + "--max-requests=1200" + "--max-requests-jitter=50" + "--log-level=info" + ]; + description = '' + Extra arguments to pass to gunicorn. + See <https://docs.pretix.eu/en/latest/admin/installation/manual_smallscale.html#start-pretix-as-a-service> for details. + ''; + apply = escapeShellArgs; + }; + + celery = { + extraArgs = mkOption { + type = with types; listOf str; + default = [ ]; + description = '' + Extra arguments to pass to celery. + + See <https://docs.celeryq.dev/en/stable/reference/cli.html#celery-worker> for more info. + ''; + apply = utils.escapeSystemdExecArgs; + }; + }; + + nginx = { + enable = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to set up an nginx virtual host. + ''; + }; + + domain = mkOption { + type = types.str; + example = "talks.example.com"; + description = '' + The domain name under which to set up the virtual host. + ''; + }; + }; + + database.createLocally = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to automatically set up the database on the local DBMS instance. + + Only supported for PostgreSQL. Not required for sqlite. + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = format.type; + options = { + pretix = { + instance_name = mkOption { + type = types.str; + example = "tickets.example.com"; + description = '' + The name of this installation. + ''; + }; + + url = mkOption { + type = types.str; + example = "https://tickets.example.com"; + description = '' + The installation’s full URL, without a trailing slash. + ''; + }; + + cachedir = mkOption { + type = types.path; + default = "/var/cache/pretix"; + description = '' + Directory for storing temporary files. + ''; + }; + + datadir = mkOption { + type = types.path; + default = "/var/lib/pretix"; + description = '' + Directory for storing user uploads and similar data. + ''; + }; + + logdir = mkOption { + type = types.path; + default = "/var/log/pretix"; + description = '' + Directory for storing log files. + ''; + }; + + currency = mkOption { + type = types.str; + default = "EUR"; + example = "USD"; + description = '' + Default currency for events in its ISO 4217 three-letter code. + ''; + }; + + registration = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to allow registration of new admin users. + ''; + }; + }; + + database = { + backend = mkOption { + type = types.enum [ + "sqlite3" + "postgresql" + ]; + default = "postgresql"; + description = '' + Database backend to use. + + Only postgresql is recommended for production setups. + ''; + }; + + host = mkOption { + type = with types; nullOr types.path; + default = if cfg.settings.database.backend == "postgresql" then "/run/postgresql" else null; + defaultText = literalExpression '' + if config.services.pretix.settings..database.backend == "postgresql" then "/run/postgresql" + else null + ''; + description = '' + Database host or socket path. + ''; + }; + + name = mkOption { + type = types.str; + default = "pretix"; + description = '' + Database name. + ''; + }; + + user = mkOption { + type = types.str; + default = "pretix"; + description = '' + Database username. + ''; + }; + }; + + mail = { + from = mkOption { + type = types.str; + example = "tickets@example.com"; + description = '' + E-Mail address used in the `FROM` header of outgoing mails. + ''; + }; + + host = mkOption { + type = types.str; + default = "localhost"; + example = "mail.example.com"; + description = '' + Hostname of the SMTP server use for mail delivery. + ''; + }; + + port = mkOption { + type = types.port; + default = 25; + example = 587; + description = '' + Port of the SMTP server to use for mail delivery. + ''; + }; + }; + + celery = { + backend = mkOption { + type = types.str; + default = "redis+socket://${config.services.redis.servers.pretix.unixSocket}?virtual_host=1"; + defaultText = literalExpression '' + optionalString config.services.pretix.celery.enable "redis+socket://''${config.services.redis.servers.pretix.unixSocket}?virtual_host=1" + ''; + description = '' + URI to the celery backend used for the asynchronous job queue. + ''; + }; + + broker = mkOption { + type = types.str; + default = "redis+socket://${config.services.redis.servers.pretix.unixSocket}?virtual_host=2"; + defaultText = literalExpression '' + optionalString config.services.pretix.celery.enable "redis+socket://''${config.services.redis.servers.pretix.unixSocket}?virtual_host=2" + ''; + description = '' + URI to the celery broker used for the asynchronous job queue. + ''; + }; + }; + + redis = { + location = mkOption { + type = with types; nullOr str; + default = "unix://${config.services.redis.servers.pretix.unixSocket}?db=0"; + defaultText = literalExpression '' + "unix://''${config.services.redis.servers.pretix.unixSocket}?db=0" + ''; + description = '' + URI to the redis server, used to speed up locking, caching and session storage. + ''; + }; + + sessions = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to use redis as the session storage. + ''; + }; + }; + + memcached = { + location = mkOption { + type = with types; nullOr str; + default = null; + example = "127.0.0.1:11211"; + description = '' + The `host:port` combination or the path to the UNIX socket of a memcached instance. + + Can be used instead of Redis for caching. + ''; + }; + }; + + tools = { + pdftk = mkOption { + type = types.path; + default = getExe pkgs.pdftk; + defaultText = literalExpression '' + lib.getExe pkgs.pdftk + ''; + description = '' + Path to the pdftk executable. + ''; + }; + }; + }; + }; + default = { }; + description = '' + pretix configuration as a Nix attribute set. All settings can also be passed + from the environment. + + See <https://docs.pretix.eu/en/latest/admin/config.html> for possible options. + ''; + }; + }; + + config = mkIf cfg.enable { + # https://docs.pretix.eu/en/latest/admin/installation/index.html + + environment.systemPackages = [ + (pkgs.writeScriptBin "pretix-manage" '' + cd ${cfg.settings.pretix.datadir} + sudo=exec + if [[ "$USER" != ${cfg.user} ]]; then + sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} ${optionalString withRedis "-g redis-pretix"} --preserve-env=PRETIX_CONFIG_FILE' + fi + export PRETIX_CONFIG_FILE=${configFile} + $sudo ${getExe' pythonEnv "pretix-manage"} "$@" + '') + ]; + + services = { + nginx = mkIf cfg.nginx.enable { + enable = true; + recommendedGzipSettings = mkDefault true; + recommendedOptimisation = mkDefault true; + recommendedProxySettings = mkDefault true; + recommendedTlsSettings = mkDefault true; + upstreams.pretix.servers."unix:/run/pretix/pretix.sock" = { }; + virtualHosts.${cfg.nginx.domain} = { + # https://docs.pretix.eu/en/latest/admin/installation/manual_smallscale.html#ssl + extraConfig = '' + more_set_headers Referrer-Policy same-origin; + more_set_headers X-Content-Type-Options nosniff; + ''; + locations = { + "/".proxyPass = "http://pretix"; + "/media/" = { + alias = "${cfg.settings.pretix.datadir}/media/"; + extraConfig = '' + access_log off; + expires 7d; + ''; + }; + "^~ /media/(cachedfiles|invoices)" = { + extraConfig = '' + deny all; + return 404; + ''; + }; + "/static/" = { + alias = "${finalPackage}/${cfg.package.python.sitePackages}/pretix/static.dist/"; + extraConfig = '' + access_log off; + more_set_headers Cache-Control "public"; + expires 365d; + ''; + }; + }; + }; + }; + + postgresql = mkIf (cfg.database.createLocally && cfg.settings.database.backend == "postgresql") { + enable = true; + ensureUsers = [ { + name = cfg.settings.database.user; + ensureDBOwnership = true; + } ]; + ensureDatabases = [ cfg.settings.database.name ]; + }; + + redis.servers.pretix.enable = withRedis; + }; + + systemd.services = let + commonUnitConfig = { + environment.PRETIX_CONFIG_FILE = configFile; + serviceConfig = { + User = "pretix"; + Group = "pretix"; + EnvironmentFile = optionals (cfg.environmentFile != null) [ + cfg.environmentFile + ]; + StateDirectory = [ + "pretix" + ]; + StateDirectoryMode = "0755"; + CacheDirectory = "pretix"; + LogsDirectory = "pretix"; + WorkingDirectory = cfg.settings.pretix.datadir; + SupplementaryGroups = optionals withRedis [ + "redis-pretix" + ]; + AmbientCapabilities = ""; + CapabilityBoundingSet = [ "" ]; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "@chown" + ]; + UMask = "0022"; + }; + }; + in { + pretix-web = recursiveUpdate commonUnitConfig { + description = "pretix web service"; + after = [ + "network.target" + "redis-pretix.service" + "postgresql.service" + ]; + wantedBy = [ "multi-user.target" ]; + preStart = '' + versionFile="${cfg.settings.pretix.datadir}/.version" + version=$(cat "$versionFile" 2>/dev/null || echo 0) + + pluginsFile="${cfg.settings.pretix.datadir}/.plugins" + plugins=$(cat "$pluginsFile" 2>/dev/null || echo "") + configuredPlugins="${concatMapStringsSep "|" (package: package.name) cfg.plugins}" + + if [[ $version != ${cfg.package.version} || $plugins != $configuredPlugins ]]; then + ${getExe' pythonEnv "pretix-manage"} migrate + + echo "${cfg.package.version}" > "$versionFile" + echo "$configuredPlugins" > "$pluginsFile" + fi + ''; + serviceConfig = { + ExecStart = "${getExe' pythonEnv "gunicorn"} --bind unix:/run/pretix/pretix.sock ${cfg.gunicorn.extraArgs} pretix.wsgi"; + RuntimeDirectory = "pretix"; + }; + }; + + pretix-periodic = recursiveUpdate commonUnitConfig { + description = "pretix periodic task runner"; + # every 15 minutes + startAt = [ "*:3,18,33,48" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${getExe' pythonEnv "pretix-manage"} runperiodic"; + }; + }; + + pretix-worker = recursiveUpdate commonUnitConfig { + description = "pretix asynchronous job runner"; + after = [ + "network.target" + "redis-pretix.service" + "postgresql.service" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.ExecStart = "${getExe' pythonEnv "celery"} -A pretix.celery_app worker ${cfg.celery.extraArgs}"; + }; + }; + + systemd.sockets.pretix-web.socketConfig = { + ListenStream = "/run/pretix/pretix.sock"; + SocketUser = "nginx"; + }; + + users = { + groups."${cfg.group}" = {}; + users."${cfg.user}" = { + isSystemUser = true; + createHome = true; + home = cfg.settings.pretix.datadir; + inherit (cfg) group; + }; + }; + }; +} diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix deleted file mode 100644 index 959bcbc5c9f1..000000000000 --- a/nixos/modules/services/web-apps/restya-board.nix +++ /dev/null @@ -1,380 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -# TODO: are these php-packages needed? -#imagick -#php-geoip -> php.ini: extension = geoip.so -#expat - -let - cfg = config.services.restya-board; - fpm = config.services.phpfpm.pools.${poolName}; - - runDir = "/run/restya-board"; - - poolName = "restya-board"; - -in - -{ - - ###### interface - - options = { - - services.restya-board = { - - enable = mkEnableOption (lib.mdDoc "restya-board"); - - dataDir = mkOption { - type = types.path; - default = "/var/lib/restya-board"; - description = lib.mdDoc '' - Data of the application. - ''; - }; - - user = mkOption { - type = types.str; - default = "restya-board"; - description = lib.mdDoc '' - User account under which the web-application runs. - ''; - }; - - group = mkOption { - type = types.str; - default = "nginx"; - description = lib.mdDoc '' - Group account under which the web-application runs. - ''; - }; - - virtualHost = { - serverName = mkOption { - type = types.str; - default = "restya.board"; - description = lib.mdDoc '' - Name of the nginx virtualhost to use. - ''; - }; - - listenHost = mkOption { - type = types.str; - default = "localhost"; - description = lib.mdDoc '' - Listen address for the virtualhost to use. - ''; - }; - - listenPort = mkOption { - type = types.port; - default = 3000; - description = lib.mdDoc '' - Listen port for the virtualhost to use. - ''; - }; - }; - - database = { - host = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc '' - Host of the database. Leave 'null' to use a local PostgreSQL database. - A local PostgreSQL database is initialized automatically. - ''; - }; - - port = mkOption { - type = types.nullOr types.int; - default = 5432; - description = lib.mdDoc '' - The database's port. - ''; - }; - - name = mkOption { - type = types.str; - default = "restya_board"; - description = lib.mdDoc '' - Name of the database. The database must exist. - ''; - }; - - user = mkOption { - type = types.str; - default = "restya_board"; - description = lib.mdDoc '' - The database user. The user must exist and have access to - the specified database. - ''; - }; - - passwordFile = mkOption { - type = types.nullOr types.path; - default = null; - description = lib.mdDoc '' - The database user's password. 'null' if no password is set. - ''; - }; - }; - - email = { - server = mkOption { - type = types.nullOr types.str; - default = null; - example = "localhost"; - description = lib.mdDoc '' - Hostname to send outgoing mail. Null to use the system MTA. - ''; - }; - - port = mkOption { - type = types.port; - default = 25; - description = lib.mdDoc '' - Port used to connect to SMTP server. - ''; - }; - - login = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - SMTP authentication login used when sending outgoing mail. - ''; - }; - - password = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - SMTP authentication password used when sending outgoing mail. - - ATTENTION: The password is stored world-readable in the nix-store! - ''; - }; - }; - - timezone = mkOption { - type = types.lines; - default = "GMT"; - description = lib.mdDoc '' - Timezone the web-app runs in. - ''; - }; - - }; - - }; - - - ###### implementation - - config = mkIf cfg.enable { - - services.phpfpm.pools = { - ${poolName} = { - inherit (cfg) user group; - - phpOptions = '' - date.timezone = "CET" - - ${optionalString (cfg.email.server != null) '' - SMTP = ${cfg.email.server} - smtp_port = ${toString cfg.email.port} - auth_username = ${cfg.email.login} - auth_password = ${cfg.email.password} - ''} - ''; - settings = mapAttrs (name: mkDefault) { - "listen.owner" = "nginx"; - "listen.group" = "nginx"; - "listen.mode" = "0600"; - "pm" = "dynamic"; - "pm.max_children" = 75; - "pm.start_servers" = 10; - "pm.min_spare_servers" = 5; - "pm.max_spare_servers" = 20; - "pm.max_requests" = 500; - "catch_workers_output" = 1; - }; - }; - }; - - services.nginx.enable = true; - services.nginx.virtualHosts.${cfg.virtualHost.serverName} = { - listen = [ { addr = cfg.virtualHost.listenHost; port = cfg.virtualHost.listenPort; } ]; - serverName = cfg.virtualHost.serverName; - root = runDir; - extraConfig = '' - index index.html index.php; - - gzip on; - - gzip_comp_level 6; - gzip_min_length 1100; - gzip_buffers 16 8k; - gzip_proxied any; - gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss; - - client_max_body_size 300M; - - rewrite ^/oauth/authorize$ /server/php/authorize.php last; - rewrite ^/oauth_callback/([a-zA-Z0-9_\.]*)/([a-zA-Z0-9_\.]*)$ /server/php/oauth_callback.php?plugin=$1&code=$2 last; - rewrite ^/download/([0-9]*)/([a-zA-Z0-9_\.]*)$ /server/php/download.php?id=$1&hash=$2 last; - rewrite ^/ical/([0-9]*)/([0-9]*)/([a-z0-9]*).ics$ /server/php/ical.php?board_id=$1&user_id=$2&hash=$3 last; - rewrite ^/api/(.*)$ /server/php/R/r.php?_url=$1&$args last; - rewrite ^/api_explorer/api-docs/$ /client/api_explorer/api-docs/index.php last; - ''; - - locations."/".root = "${runDir}/client"; - - locations."~ \\.php$" = { - tryFiles = "$uri =404"; - extraConfig = '' - include ${config.services.nginx.package}/conf/fastcgi_params; - fastcgi_pass unix:${fpm.socket}; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PHP_VALUE "upload_max_filesize=9G \n post_max_size=9G \n max_execution_time=200 \n max_input_time=200 \n memory_limit=256M"; - ''; - }; - - locations."~* \\.(css|js|less|html|ttf|woff|jpg|jpeg|gif|png|bmp|ico)" = { - root = "${runDir}/client"; - extraConfig = '' - if (-f $request_filename) { - break; - } - rewrite ^/img/([a-zA-Z_]*)/([a-zA-Z_]*)/([a-zA-Z0-9_\.]*)$ /server/php/image.php?size=$1&model=$2&filename=$3 last; - add_header Cache-Control public; - add_header Cache-Control must-revalidate; - expires 7d; - ''; - }; - }; - - systemd.services.restya-board-init = { - description = "Restya board initialization"; - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - - wantedBy = [ "multi-user.target" ]; - requires = lib.optional (cfg.database.host != null) "postgresql.service"; - after = [ "network.target" ] ++ (lib.optional (cfg.database.host != null) "postgresql.service"); - - script = '' - rm -rf "${runDir}" - mkdir -m 750 -p "${runDir}" - cp -r "${pkgs.restya-board}/"* "${runDir}" - sed -i "s/@restya.com/@${cfg.virtualHost.serverName}/g" "${runDir}/sql/restyaboard_with_empty_data.sql" - rm -rf "${runDir}/media" - rm -rf "${runDir}/client/img" - chmod -R 0750 "${runDir}" - - sed -i "s@^php@${config.services.phpfpm.phpPackage}/bin/php@" "${runDir}/server/php/shell/"*.sh - - ${if (cfg.database.host == null) then '' - sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', 'localhost');/g" "${runDir}/server/php/config.inc.php" - sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php" - '' else '' - sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php" - sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', ${if cfg.database.passwordFile == null then "''" else "'$(cat ${cfg.database.passwordFile})');/g"}" "${runDir}/server/php/config.inc.php" - ''} - sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php" - sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php" - sed -i "s/^.*'R_DB_USER'.*$/define('R_DB_USER', '${cfg.database.user}');/g" "${runDir}/server/php/config.inc.php" - - chmod 0400 "${runDir}/server/php/config.inc.php" - - ln -sf "${cfg.dataDir}/media" "${runDir}/media" - ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img" - - chmod g+w "${runDir}/tmp/cache" - chown -R "${cfg.user}":"${cfg.group}" "${runDir}" - - - mkdir -m 0750 -p "${cfg.dataDir}" - mkdir -m 0750 -p "${cfg.dataDir}/media" - mkdir -m 0750 -p "${cfg.dataDir}/client/img" - cp -r "${pkgs.restya-board}/media/"* "${cfg.dataDir}/media" - cp -r "${pkgs.restya-board}/client/img/"* "${cfg.dataDir}/client/img" - chown "${cfg.user}":"${cfg.group}" "${cfg.dataDir}" - chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/media" - chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/client/img" - - ${optionalString (cfg.database.host == null) '' - if ! [ -e "${cfg.dataDir}/.db-initialized" ]; then - ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \ - ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \ - -c "CREATE USER ${cfg.database.user} WITH ENCRYPTED PASSWORD 'restya'" - - ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \ - ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \ - -c "CREATE DATABASE ${cfg.database.name} OWNER ${cfg.database.user} ENCODING 'UTF8' TEMPLATE template0" - - ${pkgs.sudo}/bin/sudo -u ${cfg.user} \ - ${config.services.postgresql.package}/bin/psql -U ${cfg.database.user} \ - -d ${cfg.database.name} -f "${runDir}/sql/restyaboard_with_empty_data.sql" - - touch "${cfg.dataDir}/.db-initialized" - fi - ''} - ''; - }; - - systemd.timers.restya-board = { - description = "restya-board scripts for e.g. email notification"; - wantedBy = [ "timers.target" ]; - after = [ "restya-board-init.service" ]; - requires = [ "restya-board-init.service" ]; - timerConfig = { - OnUnitInactiveSec = "60s"; - Unit = "restya-board-timers.service"; - }; - }; - - systemd.services.restya-board-timers = { - description = "restya-board scripts for e.g. email notification"; - serviceConfig.Type = "oneshot"; - serviceConfig.User = cfg.user; - - after = [ "restya-board-init.service" ]; - requires = [ "restya-board-init.service" ]; - - script = '' - /bin/sh ${runDir}/server/php/shell/instant_email_notification.sh 2> /dev/null || true - /bin/sh ${runDir}/server/php/shell/periodic_email_notification.sh 2> /dev/null || true - /bin/sh ${runDir}/server/php/shell/imap.sh 2> /dev/null || true - /bin/sh ${runDir}/server/php/shell/webhook.sh 2> /dev/null || true - /bin/sh ${runDir}/server/php/shell/card_due_notification.sh 2> /dev/null || true - ''; - }; - - users.users.restya-board = { - isSystemUser = true; - createHome = false; - home = runDir; - group = "restya-board"; - }; - users.groups.restya-board = {}; - - services.postgresql.enable = mkIf (cfg.database.host == null) true; - - services.postgresql.identMap = optionalString (cfg.database.host == null) - '' - restya-board-users restya-board restya_board - ''; - - services.postgresql.authentication = optionalString (cfg.database.host == null) - '' - local restya_board all ident map=restya-board-users - ''; - - }; - -} - diff --git a/nixos/modules/services/web-apps/suwayomi-server.nix b/nixos/modules/services/web-apps/suwayomi-server.nix index c4c1540edbee..94dbe6f99356 100644 --- a/nixos/modules/services/web-apps/suwayomi-server.nix +++ b/nixos/modules/services/web-apps/suwayomi-server.nix @@ -3,6 +3,8 @@ let cfg = config.services.suwayomi-server; inherit (lib) mkOption mdDoc mkEnableOption mkIf types; + + format = pkgs.formats.hocon { }; in { options = { @@ -48,19 +50,7 @@ in settings = mkOption { type = types.submodule { - freeformType = - let - recursiveAttrsType = with types; attrsOf (nullOr (oneOf [ - str - path - int - float - bool - (listOf str) - (recursiveAttrsType // { description = "instances of this type recursively"; }) - ])); - in - recursiveAttrsType; + freeformType = format.type; options = { server = { ip = mkOption { @@ -180,38 +170,7 @@ in systemd.services.suwayomi-server = let - flattenConfig = prefix: config: - lib.foldl' - lib.mergeAttrs - { } - (lib.attrValues - (lib.mapAttrs - (k: v: - if !(lib.isAttrs v) - then { "${prefix}${k}" = v; } - else flattenConfig "${prefix}${k}." v - ) - config - ) - ); - - # HOCON is a JSON superset that suwayomi-server use for configuration - toHOCON = attr: - let - attrType = builtins.typeOf attr; - in - if builtins.elem attrType [ "string" "path" "int" "float" ] - then ''"${toString attr}"'' - else if attrType == "bool" - then lib.boolToString attr - else if attrType == "list" - then "[\n${lib.concatMapStringsSep ",\n" toHOCON attr}\n]" - else # attrs, lambda, null - throw '' - [suwayomi-server]: invalid config value type '${attrType}'. - ''; - - configFile = pkgs.writeText "server.conf" (lib.pipe cfg.settings [ + configFile = format.generate "server.conf" (lib.pipe cfg.settings [ (settings: lib.recursiveUpdate settings { server.basicAuthPasswordFile = null; server.basicAuthPassword = @@ -219,12 +178,8 @@ in then "$TACHIDESK_SERVER_BASIC_AUTH_PASSWORD" else null; }) - (flattenConfig "") - (lib.filterAttrs (_: x: x != null)) - (lib.mapAttrsToList (name: value: ''${name} = ${toHOCON value}'')) - lib.concatLines + (lib.filterAttrsRecursive (_: x: x != null)) ]); - in { description = "A free and open source manga reader server that runs extensions built for Tachiyomi."; diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix index a8fb37d2c5ec..84342165c9c0 100644 --- a/nixos/modules/services/web-apps/tt-rss.nix +++ b/nixos/modules/services/web-apps/tt-rss.nix @@ -4,6 +4,8 @@ with lib; let cfg = config.services.tt-rss; + inherit (cfg) phpPackage; + configVersion = 26; dbPort = if cfg.database.port == null @@ -26,7 +28,7 @@ let ; in pkgs.writeText "config.php" '' <?php - putenv('TTRSS_PHP_EXECUTABLE=${pkgs.php}/bin/php'); + putenv('TTRSS_PHP_EXECUTABLE=${phpPackage}/bin/php'); putenv('TTRSS_LOCK_DIRECTORY=${cfg.root}/lock'); putenv('TTRSS_CACHE_DIR=${cfg.root}/cache'); @@ -456,6 +458,15 @@ let ''; }; + phpPackage = lib.mkOption { + type = lib.types.package; + default = pkgs.php; + defaultText = "pkgs.php"; + description = lib.mdDoc '' + php package to use for php fpm and update daemon. + ''; + }; + plugins = mkOption { type = types.listOf types.str; default = ["auth_internal" "note"]; @@ -543,7 +554,7 @@ let services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") { ${poolName} = { inherit (cfg) user; - phpPackage = pkgs.php81; + inherit phpPackage; settings = mapAttrs (name: mkDefault) { "listen.owner" = "nginx"; "listen.group" = "nginx"; @@ -605,13 +616,13 @@ let description = "Tiny Tiny RSS feeds update daemon"; preStart = '' - ${pkgs.php81}/bin/php ${cfg.root}/www/update.php --update-schema + ${phpPackage}/bin/php ${cfg.root}/www/update.php --update-schema --force-yes ''; serviceConfig = { User = "${cfg.user}"; Group = "tt_rss"; - ExecStart = "${pkgs.php}/bin/php ${cfg.root}/www/update.php --daemon --quiet"; + ExecStart = "${phpPackage}/bin/php ${cfg.root}/www/update.php --daemon --quiet"; Restart = "on-failure"; RestartSec = "60"; SyslogIdentifier = "tt-rss"; diff --git a/nixos/modules/services/web-apps/vikunja.nix b/nixos/modules/services/web-apps/vikunja.nix index b893f2c1f33c..efa9c676d9a5 100644 --- a/nixos/modules/services/web-apps/vikunja.nix +++ b/nixos/modules/services/web-apps/vikunja.nix @@ -9,10 +9,13 @@ let useMysql = cfg.database.type == "mysql"; usePostgresql = cfg.database.type == "postgres"; in { + imports = [ + (mkRemovedOptionModule [ "services" "vikunja" "setupNginx" ] "services.vikunja no longer supports the automatic set up of a nginx virtual host. Set up your own webserver config with a proxy pass to the vikunja service.") + ]; + options.services.vikunja = with lib; { enable = mkEnableOption (lib.mdDoc "vikunja service"); - package-api = mkPackageOption pkgs "vikunja-api" { }; - package-frontend = mkPackageOption pkgs "vikunja-frontend" { }; + package = mkPackageOption pkgs "vikunja" { }; environmentFiles = mkOption { type = types.listOf types.path; default = [ ]; @@ -21,25 +24,10 @@ in { For example passwords should be set in one of these files. ''; }; - setupNginx = mkOption { - type = types.bool; - default = config.services.nginx.enable; - defaultText = literalExpression "config.services.nginx.enable"; - description = lib.mdDoc '' - Whether to setup NGINX. - Further nginx configuration can be done by changing - {option}`services.nginx.virtualHosts.<frontendHostname>`. - This does not enable TLS or ACME by default. To enable this, set the - {option}`services.nginx.virtualHosts.<frontendHostname>.enableACME` to - `true` and if appropriate do the same for - {option}`services.nginx.virtualHosts.<frontendHostname>.forceSSL`. - ''; - }; frontendScheme = mkOption { type = types.enum [ "http" "https" ]; description = lib.mdDoc '' Whether the site is available via http or https. - This does not configure https or ACME in nginx! ''; }; frontendHostname = mkOption { @@ -104,42 +92,27 @@ in { }; }; - systemd.services.vikunja-api = { - description = "vikunja-api"; + systemd.services.vikunja = { + description = "vikunja"; after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service"; wantedBy = [ "multi-user.target" ]; - path = [ cfg.package-api ]; + path = [ cfg.package ]; restartTriggers = [ configFile ]; serviceConfig = { Type = "simple"; DynamicUser = true; StateDirectory = "vikunja"; - ExecStart = "${cfg.package-api}/bin/vikunja"; + ExecStart = "${cfg.package}/bin/vikunja"; Restart = "always"; EnvironmentFile = cfg.environmentFiles; }; }; - services.nginx.virtualHosts."${cfg.frontendHostname}" = mkIf cfg.setupNginx { - locations = { - "/" = { - root = cfg.package-frontend; - tryFiles = "try_files $uri $uri/ /"; - }; - "~* ^/(api|dav|\\.well-known)/" = { - proxyPass = "http://localhost:${toString cfg.port}"; - extraConfig = '' - client_max_body_size 20M; - ''; - }; - }; - }; - environment.etc."vikunja/config.yaml".source = configFile; environment.systemPackages = [ - cfg.package-api # for admin `vikunja` CLI + cfg.package # for admin `vikunja` CLI ]; }; } diff --git a/nixos/modules/services/web-apps/writefreely.nix b/nixos/modules/services/web-apps/writefreely.nix index f92afa9276e3..2e9a34897909 100644 --- a/nixos/modules/services/web-apps/writefreely.nix +++ b/nixos/modules/services/web-apps/writefreely.nix @@ -334,8 +334,10 @@ in { optionalAttrs (cfg.group == "writefreely") { writefreely = { }; }; }; - systemd.tmpfiles.rules = - [ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ]; + systemd.tmpfiles.settings."10-writefreely".${cfg.stateDir}.d = { + inherit (cfg) user group; + mode = "0750"; + }; systemd.services.writefreely = { after = [ "network.target" ] diff --git a/nixos/modules/services/web-apps/youtrack.md b/nixos/modules/services/web-apps/youtrack.md new file mode 100644 index 000000000000..f33f482ff970 --- /dev/null +++ b/nixos/modules/services/web-apps/youtrack.md @@ -0,0 +1,30 @@ +# YouTrack {#module-services-youtrack} + +YouTrack is a browser-based bug tracker, issue tracking system and project management software. + +## Installation {#module-services-youtrack-installation} + +YouTrack exposes a web GUI installer on first login. +You need a token to access it. +You can find this token in the log of the `youtrack` service. The log line looks like +``` +* JetBrains YouTrack 2023.3 Configuration Wizard will be available on [http://127.0.0.1:8090/?wizard_token=somelongtoken] after start +``` + +## Upgrade from 2022.3 to 2023.x {#module-services-youtrack-upgrade-2022_3-2023_1} + +Starting with YouTrack 2023.1, JetBrains no longer distributes it as as JAR. +The new distribution with the JetBrains Launcher as a ZIP changed the basic data structure and also some configuration parameters. +Check out https://www.jetbrains.com/help/youtrack/server/YouTrack-Java-Start-Parameters.html for more information on the new configuration options. +When upgrading to YouTrack 2023.1 or higher, a migration script will move the old state directory to `/var/lib/youtrack/2022_3` as a backup. +A one-time manual update is required: + +1. Before you update take a backup of your YouTrack instance! +2. Migrate the options you set in `services.youtrack.extraParams` and `services.youtrack.jvmOpts` to `services.youtrack.generalParameters` and `services.youtrack.environmentalParameters` (see the examples and [the YouTrack docs](https://www.jetbrains.com/help/youtrack/server/2023.3/YouTrack-Java-Start-Parameters.html)) +2. To start the upgrade set `services.youtrack.package = pkgs.youtrack` +3. YouTrack then starts in upgrade mode, meaning you need to obtain the wizard token as above +4. Select you want to **Upgrade** YouTrack +5. As source you select `/var/lib/youtrack/2022_3/teamsysdata/` (adopt if you have a different state path) +6. Change the data directory location to `/var/lib/youtrack/data/`. The other paths should already be right. + +If you migrate a larger YouTrack instance, it might be useful to set `-Dexodus.entityStore.refactoring.forceAll=true` in `services.youtrack.generalParameters` for the first startup of YouTrack 2023.x. diff --git a/nixos/modules/services/web-apps/youtrack.nix b/nixos/modules/services/web-apps/youtrack.nix index 79e1d12e0abb..08e180b520f0 100644 --- a/nixos/modules/services/web-apps/youtrack.nix +++ b/nixos/modules/services/web-apps/youtrack.nix @@ -1,130 +1,224 @@ { config, lib, pkgs, ... }: -with lib; - let cfg = config.services.youtrack; - - extraAttr = concatStringsSep " " (mapAttrsToList (k: v: "-D${k}=${v}") (stdParams // cfg.extraParams)); - mergeAttrList = lib.foldl' lib.mergeAttrs {}; - - stdParams = mergeAttrList [ - (optionalAttrs (cfg.baseUrl != null) { - "jetbrains.youtrack.baseUrl" = cfg.baseUrl; - }) - { - "java.aws.headless" = "true"; - "jetbrains.youtrack.disableBrowser" = "true"; - } - ]; in { - options.services.youtrack = { + imports = [ + (lib.mkRenamedOptionModule [ "services" "youtrack" "baseUrl" ] [ "services" "youtrack" "environmentalParameters" "base-url" ]) + (lib.mkRenamedOptionModule [ "services" "youtrack" "port" ] [ "services" "youtrack" "environmentalParameters" "listen-port" ]) + (lib.mkRemovedOptionModule [ "services" "youtrack" "maxMemory" ] "Please instead use `services.youtrack.generalParameters`.") + (lib.mkRemovedOptionModule [ "services" "youtrack" "maxMetaspaceSize" ] "Please instead use `services.youtrack.generalParameters`.") + ]; - enable = mkEnableOption (lib.mdDoc "YouTrack service"); + options.services.youtrack = { + enable = lib.mkEnableOption (lib.mdDoc "YouTrack service"); - address = mkOption { + address = lib.mkOption { description = lib.mdDoc '' The interface youtrack will listen on. ''; default = "127.0.0.1"; - type = types.str; + type = lib.types.str; }; - baseUrl = mkOption { - description = lib.mdDoc '' - Base URL for youtrack. Will be auto-detected and stored in database. - ''; - type = types.nullOr types.str; - default = null; - }; - - extraParams = mkOption { + extraParams = lib.mkOption { default = {}; description = lib.mdDoc '' - Extra parameters to pass to youtrack. See + Extra parameters to pass to youtrack. + Use to configure YouTrack 2022.x, deprecated with YouTrack 2023.x. Use `services.youtrack.generalParameters`. https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Java-Start-Parameters.html for more information. ''; - example = literalExpression '' + example = lib.literalExpression '' { "jetbrains.youtrack.overrideRootPassword" = "tortuga"; } ''; - type = types.attrsOf types.str; + type = lib.types.attrsOf lib.types.str; + visible = false; }; - package = mkPackageOption pkgs "youtrack" { }; - - port = mkOption { + package = lib.mkOption { description = lib.mdDoc '' - The port youtrack will listen on. + Package to use. ''; - default = 8080; - type = types.port; + type = lib.types.package; + default = null; + relatedPackages = [ "youtrack_2022_3" "youtrack" ]; }; - statePath = mkOption { + + statePath = lib.mkOption { description = lib.mdDoc '' - Where to keep the youtrack database. + Path were the YouTrack state is stored. + To this path the base version (e.g. 2023_1) of the used package will be appended. ''; - type = types.path; + type = lib.types.path; default = "/var/lib/youtrack"; }; - virtualHost = mkOption { + virtualHost = lib.mkOption { description = lib.mdDoc '' Name of the nginx virtual host to use and setup. If null, do not setup anything. ''; default = null; - type = types.nullOr types.str; + type = lib.types.nullOr lib.types.str; }; - jvmOpts = mkOption { + jvmOpts = lib.mkOption { description = lib.mdDoc '' Extra options to pass to the JVM. + Only has a use with YouTrack 2022.x, deprecated with YouTrack 2023.x. Use `serivces.youtrack.generalParameters`. See https://www.jetbrains.com/help/youtrack/standalone/Configure-JVM-Options.html for more information. ''; - type = types.separatedString " "; - example = "-XX:MetaspaceSize=250m"; + type = lib.types.separatedString " "; + example = "--J-XX:MetaspaceSize=250m"; default = ""; + visible = false; }; - maxMemory = mkOption { + autoUpgrade = lib.mkOption { + type = lib.types.bool; + default = true; + description = lib.mdDoc "Whether YouTrack should auto upgrade it without showing the upgrade dialog."; + }; + + generalParameters = lib.mkOption { + type = with lib.types; listOf str; description = lib.mdDoc '' - Maximum Java heap size + General configuration parameters and other JVM options. + Only has an effect for YouTrack 2023.x. + See https://www.jetbrains.com/help/youtrack/server/2023.3/youtrack-java-start-parameters.html#general-parameters + for more information. ''; - type = types.str; - default = "1g"; + example = lib.literalExpression '' + [ + "-Djetbrains.youtrack.admin.restore=true" + "-Xmx1024m" + ]; + ''; + default = []; }; - maxMetaspaceSize = mkOption { + environmentalParameters = lib.mkOption { + type = lib.types.submodule { + freeformType = with lib.types; attrsOf (oneOf [ int str port ]); + options = { + listen-address = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = lib.mdDoc "The interface YouTrack will listen on."; + }; + listen-port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = lib.mdDoc "The port YouTrack will listen on."; + }; + }; + }; description = lib.mdDoc '' - Maximum java Metaspace memory. + Environmental configuration parameters, set imperatively. The values doesn't get removed, when removed in Nix. + Only has an effect for YouTrack 2023.x. + See https://www.jetbrains.com/help/youtrack/server/2023.3/youtrack-java-start-parameters.html#environmental-parameters + for more information. + ''; + example = lib.literalExpression '' + { + secure-mode = "tls"; + } ''; - type = types.str; - default = "350m"; + default = {}; }; }; - config = mkIf cfg.enable { - - systemd.services.youtrack = { - environment.HOME = cfg.statePath; - environment.YOUTRACK_JVM_OPTS = "${extraAttr}"; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - path = with pkgs; [ unixtools.hostname ]; - serviceConfig = { - Type = "simple"; - User = "youtrack"; - Group = "youtrack"; - Restart = "on-failure"; - ExecStart = ''${cfg.package}/bin/youtrack --J-Xmx${cfg.maxMemory} --J-XX:MaxMetaspaceSize=${cfg.maxMetaspaceSize} ${cfg.jvmOpts} ${cfg.address}:${toString cfg.port}''; + config = lib.mkIf cfg.enable { + warnings = lib.optional (lib.versions.major cfg.package.version <= "2022") + "YouTrack 2022.x is deprecated. See https://nixos.org/manual/nixos/unstable/index.html#module-services-youtrack for details on how to upgrade." + ++ lib.optional (cfg.extraParams != {} && (lib.versions.major cfg.package.version >= "2023")) + "'services.youtrack.extraParams' is deprecated and has no effect on YouTrack 2023.x and newer. Please migrate to 'services.youtrack.generalParameters'" + ++ lib.optional (cfg.jvmOpts != "" && (lib.versions.major cfg.package.version >= "2023")) + "'services.youtrack.jvmOpts' is deprecated and has no effect on YouTrack 2023.x and newer. Please migrate to 'services.youtrack.generalParameters'"; + + # XXX: Drop all version feature switches at the point when we consider YT 2022.3 as outdated. + services.youtrack.package = lib.mkDefault ( + if lib.versionAtLeast config.system.stateVersion "24.11" then pkgs.youtrack + else pkgs.youtrack_2022_3 + ); + + services.youtrack.generalParameters = lib.optional (lib.versions.major cfg.package.version >= "2023") + "-Ddisable.configuration.wizard.on.upgrade=${lib.boolToString cfg.autoUpgrade}" + ++ (lib.mapAttrsToList (k: v: "-D${k}=${v}") cfg.extraParams); + + systemd.services.youtrack = let + service_jar = let + mergeAttrList = lib.foldl' lib.mergeAttrs {}; + stdParams = mergeAttrList [ + (lib.optionalAttrs (cfg.environmentalParameters ? base-url && cfg.environmentalParameters.base-url != null) { + "jetbrains.youtrack.baseUrl" = cfg.environmentalParameters.base-url; + }) + { + "java.aws.headless" = "true"; + "jetbrains.youtrack.disableBrowser" = "true"; + } + ]; + extraAttr = lib.concatStringsSep " " (lib.mapAttrsToList (k: v: "-D${k}=${v}") (stdParams // cfg.extraParams)); + in { + environment.HOME = cfg.statePath; + environment.YOUTRACK_JVM_OPTS = "${extraAttr}"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ unixtools.hostname ]; + serviceConfig = { + Type = "simple"; + User = "youtrack"; + Group = "youtrack"; + Restart = "on-failure"; + ExecStart = ''${cfg.package}/bin/youtrack ${cfg.jvmOpts} ${cfg.environmentalParameters.listen-address}:${toString cfg.environmentalParameters.listen-port}''; + }; }; - }; + service_zip = let + jvmoptions = pkgs.writeTextFile { + name = "youtrack.jvmoptions"; + text = (lib.concatStringsSep "\n" cfg.generalParameters); + }; + + package = cfg.package.override { + statePath = cfg.statePath; + }; + in { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ unixtools.hostname ]; + preStart = '' + # This detects old (i.e. <= 2022.3) installations that were not migrated yet + # and migrates them to the new state directory style + if [[ -d ${cfg.statePath}/teamsysdata ]] && [[ ! -d ${cfg.statePath}/2022_3 ]] + then + mkdir -p ${cfg.statePath}/2022_3 + mv ${cfg.statePath}/teamsysdata ${cfg.statePath}/2022_3 + mv ${cfg.statePath}/.youtrack ${cfg.statePath}/2022_3 + fi + mkdir -p ${cfg.statePath}/{backups,conf,data,logs,temp} + ${pkgs.coreutils}/bin/ln -fs ${jvmoptions} ${cfg.statePath}/conf/youtrack.jvmoptions + ${package}/bin/youtrack configure ${lib.concatStringsSep " " (lib.mapAttrsToList (name: value: "--${name}=${toString value}") cfg.environmentalParameters )} + ''; + serviceConfig = lib.mkMerge [ + { + Type = "simple"; + User = "youtrack"; + Group = "youtrack"; + Restart = "on-failure"; + ExecStart = "${package}/bin/youtrack run"; + } + (lib.mkIf (cfg.statePath == "/var/lib/youtrack") { + StateDirectory = "youtrack"; + }) + ]; + }; + in if (lib.versions.major cfg.package.version >= "2023") then service_zip else service_jar; users.users.youtrack = { description = "Youtrack service user"; @@ -136,8 +230,8 @@ in users.groups.youtrack = {}; - services.nginx = mkIf (cfg.virtualHost != null) { - upstreams.youtrack.servers."${cfg.address}:${toString cfg.port}" = {}; + services.nginx = lib.mkIf (cfg.virtualHost != null) { + upstreams.youtrack.servers."${cfg.address}:${toString cfg.environmentalParameters.listen-port}" = {}; virtualHosts.${cfg.virtualHost}.locations = { "/" = { proxyPass = "http://youtrack"; @@ -166,9 +260,10 @@ in proxy_set_header X-Forwarded-Proto $scheme; ''; }; - }; }; - }; + + meta.doc = ./youtrack.md; + meta.maintainers = [ lib.maintainers.leona ]; } |