about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/web-apps
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2022-12-06 19:57:55 +0000
committerAlyssa Ross <hi@alyssa.is>2023-02-08 13:48:30 +0000
commitbf3aadfdd39aa197e18bade671fab6726349ffa4 (patch)
tree698567af766ed441d757b57a7b21e68d4a342a2b /nixpkgs/nixos/modules/services/web-apps
parentf4afc5a01d9539ce09e47494e679c51f80723d07 (diff)
parent99665eb45f58d959d2cb9e49ddb960c79d596f33 (diff)
downloadnixlib-bf3aadfdd39aa197e18bade671fab6726349ffa4.tar
nixlib-bf3aadfdd39aa197e18bade671fab6726349ffa4.tar.gz
nixlib-bf3aadfdd39aa197e18bade671fab6726349ffa4.tar.bz2
nixlib-bf3aadfdd39aa197e18bade671fab6726349ffa4.tar.lz
nixlib-bf3aadfdd39aa197e18bade671fab6726349ffa4.tar.xz
nixlib-bf3aadfdd39aa197e18bade671fab6726349ffa4.tar.zst
nixlib-bf3aadfdd39aa197e18bade671fab6726349ffa4.zip
Merge commit '99665eb45f58d959d2cb9e49ddb960c79d596f33'
Diffstat (limited to 'nixpkgs/nixos/modules/services/web-apps')
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/atlassian/confluence.nix89
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/atlassian/crowd.nix59
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/atlassian/jira.nix79
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/baget.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/bookstack.nix70
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/calibre-web.nix26
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/code-server.nix26
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/convos.nix10
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/cryptpad.nix54
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/dex.nix32
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/discourse.nix118
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/documize.nix26
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/dokuwiki.nix50
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/engelsystem.nix12
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/ethercalc.nix10
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/fluidd.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/galene.nix55
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/gerrit.nix30
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/gotify-server.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/grocy.nix24
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/healthchecks.nix249
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/hedgedoc.nix322
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/hledger-web.nix20
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix20
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix34
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/ihatemoney/default.nix20
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/invidious.nix30
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/invoiceplane.nix33
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/jirafeau.nix16
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/jitsi-meet.nix75
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/keycloak.nix915
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/keycloak.xml142
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/komga.nix99
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/lemmy.nix16
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/limesurvey.nix32
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/mastodon.nix110
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/matomo.nix16
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/mattermost.nix34
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/mediawiki.nix54
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/miniflux.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/moodle.nix36
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/netbox.nix265
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/nextcloud.nix307
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/nextcloud.xml2
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/nexus.nix12
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/nifi.nix318
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/node-red.nix23
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/onlyoffice.nix288
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/openwebrx.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/peertube.nix66
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/phylactery.nix51
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/pict-rs.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/plantuml-server.nix26
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/plausible.nix44
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/powerdns-admin.nix10
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/prosody-filer.nix4
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/restya-board.nix46
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/rss-bridge.nix12
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/selfoss.nix18
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/shiori.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/snipe-it.nix494
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/sogo.nix12
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/timetagger.nix80
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/trilium.nix25
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/tt-rss.nix87
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/vikunja.nix34
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/virtlyst.nix73
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/whitebophir.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/wiki-js.nix21
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/wordpress.nix48
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/youtrack.nix20
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/zabbix.nix36
72 files changed, 3611 insertions, 1902 deletions
diff --git a/nixpkgs/nixos/modules/services/web-apps/atlassian/confluence.nix b/nixpkgs/nixos/modules/services/web-apps/atlassian/confluence.nix
index 2d809c17ff09..6c5de3fbe4b8 100644
--- a/nixpkgs/nixos/modules/services/web-apps/atlassian/confluence.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/atlassian/confluence.nix
@@ -8,21 +8,22 @@ let
 
   pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
     enableSSO = cfg.sso.enable;
-    crowdProperties = ''
-      application.name                        ${cfg.sso.applicationName}
-      application.password                    ${cfg.sso.applicationPassword}
-      application.login.url                   ${cfg.sso.crowd}/console/
-
-      crowd.server.url                        ${cfg.sso.crowd}/services/
-      crowd.base.url                          ${cfg.sso.crowd}/
-
-      session.isauthenticated                 session.isauthenticated
-      session.tokenkey                        session.tokenkey
-      session.validationinterval              ${toString cfg.sso.validationInterval}
-      session.lastvalidation                  session.lastvalidation
-    '';
   });
 
+  crowdProperties = pkgs.writeText "crowd.properties" ''
+    application.name                        ${cfg.sso.applicationName}
+    application.password                    ${if cfg.sso.applicationPassword != null then cfg.sso.applicationPassword else "@NIXOS_CONFLUENCE_CROWD_SSO_PWD@"}
+    application.login.url                   ${cfg.sso.crowd}/console/
+
+    crowd.server.url                        ${cfg.sso.crowd}/services/
+    crowd.base.url                          ${cfg.sso.crowd}/
+
+    session.isauthenticated                 session.isauthenticated
+    session.tokenkey                        session.tokenkey
+    session.validationinterval              ${toString cfg.sso.validationInterval}
+    session.lastvalidation                  session.lastvalidation
+  '';
+
 in
 
 {
@@ -33,38 +34,38 @@ in
       user = mkOption {
         type = types.str;
         default = "confluence";
-        description = "User which runs confluence.";
+        description = lib.mdDoc "User which runs confluence.";
       };
 
       group = mkOption {
         type = types.str;
         default = "confluence";
-        description = "Group which runs confluence.";
+        description = lib.mdDoc "Group which runs confluence.";
       };
 
       home = mkOption {
         type = types.str;
         default = "/var/lib/confluence";
-        description = "Home directory of the confluence instance.";
+        description = lib.mdDoc "Home directory of the confluence instance.";
       };
 
       listenAddress = mkOption {
         type = types.str;
         default = "127.0.0.1";
-        description = "Address to listen on.";
+        description = lib.mdDoc "Address to listen on.";
       };
 
       listenPort = mkOption {
         type = types.int;
         default = 8090;
-        description = "Port to listen on.";
+        description = lib.mdDoc "Port to listen on.";
       };
 
       catalinaOptions = mkOption {
         type = types.listOf types.str;
         default = [];
         example = [ "-Xms1024m" "-Xmx2048m" "-Dconfluence.disable.peopledirectory.all=true" ];
-        description = "Java options to pass to catalina/tomcat.";
+        description = lib.mdDoc "Java options to pass to catalina/tomcat.";
       };
 
       proxy = {
@@ -73,21 +74,21 @@ in
         name = mkOption {
           type = types.str;
           example = "confluence.example.com";
-          description = "Virtual hostname at the proxy";
+          description = lib.mdDoc "Virtual hostname at the proxy";
         };
 
         port = mkOption {
           type = types.int;
           default = 443;
           example = 80;
-          description = "Port used at the proxy";
+          description = lib.mdDoc "Port used at the proxy";
         };
 
         scheme = mkOption {
           type = types.str;
           default = "https";
           example = "http";
-          description = "Protocol used at the proxy.";
+          description = lib.mdDoc "Protocol used at the proxy.";
         };
       };
 
@@ -97,25 +98,32 @@ in
         crowd = mkOption {
           type = types.str;
           example = "http://localhost:8095/crowd";
-          description = "Crowd Base URL without trailing slash";
+          description = lib.mdDoc "Crowd Base URL without trailing slash";
         };
 
         applicationName = mkOption {
           type = types.str;
           example = "jira";
-          description = "Exact name of this Confluence instance in Crowd";
+          description = lib.mdDoc "Exact name of this Confluence instance in Crowd";
         };
 
         applicationPassword = mkOption {
-          type = types.str;
-          description = "Application password of this Confluence instance in Crowd";
+          type = types.nullOr types.str;
+          default = null;
+          description = lib.mdDoc "Application password of this Confluence instance in Crowd";
+        };
+
+        applicationPasswordFile = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = lib.mdDoc "Path to the application password for Crowd of Confluence.";
         };
 
         validationInterval = mkOption {
           type = types.int;
           default = 2;
           example = 0;
-          description = ''
+          description = lib.mdDoc ''
             Set to 0, if you want authentication checks to occur on each
             request. Otherwise set to the number of minutes between request
             to validate if the user is logged in or out of the Crowd SSO
@@ -129,14 +137,14 @@ in
         type = types.package;
         default = pkgs.atlassian-confluence;
         defaultText = literalExpression "pkgs.atlassian-confluence";
-        description = "Atlassian Confluence package to use.";
+        description = lib.mdDoc "Atlassian Confluence package to use.";
       };
 
       jrePackage = mkOption {
         type = types.package;
         default = pkgs.oraclejre8;
         defaultText = literalExpression "pkgs.oraclejre8";
-        description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
+        description = lib.mdDoc "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
       };
     };
   };
@@ -147,6 +155,16 @@ in
       group = cfg.group;
     };
 
+    assertions = [
+      { assertion = cfg.sso.enable -> ((cfg.sso.applicationPassword == null) != (cfg.sso.applicationPasswordFile));
+        message = "Please set either applicationPassword or applicationPasswordFile";
+      }
+    ];
+
+    warnings = mkIf (cfg.sso.enable && cfg.sso.applicationPassword != null) [
+      "Using `services.confluence.sso.applicationPassword` is deprecated! Use `applicationPasswordFile` instead!"
+    ];
+
     users.groups.${cfg.group} = {};
 
     systemd.tmpfiles.rules = [
@@ -173,6 +191,7 @@ in
         CONF_USER = cfg.user;
         JAVA_HOME = "${cfg.jrePackage}";
         CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
+        JAVA_OPTS = mkIf cfg.sso.enable "-Dcrowd.properties=${cfg.home}/crowd.properties";
       };
 
       preStart = ''
@@ -183,12 +202,24 @@ in
           -e 's,protocol="org.apache.coyote.http11.Http11NioProtocol",protocol="org.apache.coyote.http11.Http11NioProtocol" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}",' \
         '') + ''
           ${pkg}/conf/server.xml.dist > ${cfg.home}/server.xml
+
+        ${optionalString cfg.sso.enable ''
+          install -m660 ${crowdProperties} ${cfg.home}/crowd.properties
+          ${optionalString (cfg.sso.applicationPasswordFile != null) ''
+            ${pkgs.replace-secret}/bin/replace-secret \
+              '@NIXOS_CONFLUENCE_CROWD_SSO_PWD@' \
+              ${cfg.sso.applicationPasswordFile} \
+              ${cfg.home}/crowd.properties
+          ''}
+        ''}
       '';
 
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
         PrivateTmp = true;
+        Restart = "on-failure";
+        RestartSec = "10";
         ExecStart = "${pkg}/bin/start-confluence.sh -fg";
         ExecStop = "${pkg}/bin/stop-confluence.sh";
       };
diff --git a/nixpkgs/nixos/modules/services/web-apps/atlassian/crowd.nix b/nixpkgs/nixos/modules/services/web-apps/atlassian/crowd.nix
index a8b2482d5a9c..abe3a8bdb225 100644
--- a/nixpkgs/nixos/modules/services/web-apps/atlassian/crowd.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/atlassian/crowd.nix
@@ -14,6 +14,21 @@ let
     proxyUrl = "${cfg.proxy.scheme}://${cfg.proxy.name}:${toString cfg.proxy.port}";
   });
 
+  crowdPropertiesFile = pkgs.writeText "crowd.properties" ''
+    application.name                        crowd-openid-server
+    application.password @NIXOS_CROWD_OPENID_PW@
+    application.base.url                    http://localhost:${toString cfg.listenPort}/openidserver
+    application.login.url                   http://localhost:${toString cfg.listenPort}/openidserver
+    application.login.url.template          http://localhost:${toString cfg.listenPort}/openidserver?returnToUrl=''${RETURN_TO_URL}
+
+    crowd.server.url                        http://localhost:${toString cfg.listenPort}/crowd/services/
+
+    session.isauthenticated                 session.isauthenticated
+    session.tokenkey                        session.tokenkey
+    session.validationinterval              0
+    session.lastvalidation                  session.lastvalidation
+  '';
+
 in
 
 {
@@ -24,43 +39,50 @@ in
       user = mkOption {
         type = types.str;
         default = "crowd";
-        description = "User which runs Crowd.";
+        description = lib.mdDoc "User which runs Crowd.";
       };
 
       group = mkOption {
         type = types.str;
         default = "crowd";
-        description = "Group which runs Crowd.";
+        description = lib.mdDoc "Group which runs Crowd.";
       };
 
       home = mkOption {
         type = types.str;
         default = "/var/lib/crowd";
-        description = "Home directory of the Crowd instance.";
+        description = lib.mdDoc "Home directory of the Crowd instance.";
       };
 
       listenAddress = mkOption {
         type = types.str;
         default = "127.0.0.1";
-        description = "Address to listen on.";
+        description = lib.mdDoc "Address to listen on.";
       };
 
       listenPort = mkOption {
         type = types.int;
         default = 8092;
-        description = "Port to listen on.";
+        description = lib.mdDoc "Port to listen on.";
       };
 
       openidPassword = mkOption {
         type = types.str;
-        description = "Application password for OpenID server.";
+        default = "WILL_NEVER_BE_SET";
+        description = lib.mdDoc "Application password for OpenID server.";
+      };
+
+      openidPasswordFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = lib.mdDoc "Path to the file containing the application password for OpenID server.";
       };
 
       catalinaOptions = mkOption {
         type = types.listOf types.str;
         default = [];
         example = [ "-Xms1024m" "-Xmx2048m" ];
-        description = "Java options to pass to catalina/tomcat.";
+        description = lib.mdDoc "Java options to pass to catalina/tomcat.";
       };
 
       proxy = {
@@ -69,27 +91,27 @@ in
         name = mkOption {
           type = types.str;
           example = "crowd.example.com";
-          description = "Virtual hostname at the proxy";
+          description = lib.mdDoc "Virtual hostname at the proxy";
         };
 
         port = mkOption {
           type = types.int;
           default = 443;
           example = 80;
-          description = "Port used at the proxy";
+          description = lib.mdDoc "Port used at the proxy";
         };
 
         scheme = mkOption {
           type = types.str;
           default = "https";
           example = "http";
-          description = "Protocol used at the proxy.";
+          description = lib.mdDoc "Protocol used at the proxy.";
         };
 
         secure = mkOption {
           type = types.bool;
           default = true;
-          description = "Whether the connections to the proxy should be considered secure.";
+          description = lib.mdDoc "Whether the connections to the proxy should be considered secure.";
         };
       };
 
@@ -97,14 +119,14 @@ in
         type = types.package;
         default = pkgs.atlassian-crowd;
         defaultText = literalExpression "pkgs.atlassian-crowd";
-        description = "Atlassian Crowd package to use.";
+        description = lib.mdDoc "Atlassian Crowd package to use.";
       };
 
       jrePackage = mkOption {
         type = types.package;
         default = pkgs.oraclejre8;
         defaultText = literalExpression "pkgs.oraclejre8";
-        description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
+        description = lib.mdDoc "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
       };
     };
   };
@@ -140,6 +162,7 @@ in
         JAVA_HOME = "${cfg.jrePackage}";
         CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
         CATALINA_TMPDIR = "/tmp";
+        JAVA_OPTS = mkIf (cfg.openidPasswordFile != null) "-Dcrowd.properties=${cfg.home}/crowd.properties";
       };
 
       preStart = ''
@@ -151,12 +174,22 @@ in
           -e 's,compression="on",compression="off" protocol="HTTP/1.1" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}" secure="${boolToString cfg.proxy.secure}",' \
         '') + ''
           ${pkg}/apache-tomcat/conf/server.xml.dist > ${cfg.home}/server.xml
+
+        ${optionalString (cfg.openidPasswordFile != null) ''
+          install -m660 ${crowdPropertiesFile} ${cfg.home}/crowd.properties
+          ${pkgs.replace-secret}/bin/replace-secret \
+            '@NIXOS_CROWD_OPENID_PW@' \
+            ${cfg.openidPasswordFile} \
+            ${cfg.home}/crowd.properties
+        ''}
       '';
 
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
         PrivateTmp = true;
+        Restart = "on-failure";
+        RestartSec = "10";
         ExecStart = "${pkg}/start_crowd.sh -fg";
       };
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/atlassian/jira.nix b/nixpkgs/nixos/modules/services/web-apps/atlassian/jira.nix
index d7a26838d6f8..5d62160ffb13 100644
--- a/nixpkgs/nixos/modules/services/web-apps/atlassian/jira.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/atlassian/jira.nix
@@ -8,21 +8,22 @@ let
 
   pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
     enableSSO = cfg.sso.enable;
-    crowdProperties = ''
-      application.name                        ${cfg.sso.applicationName}
-      application.password                    ${cfg.sso.applicationPassword}
-      application.login.url                   ${cfg.sso.crowd}/console/
-
-      crowd.server.url                        ${cfg.sso.crowd}/services/
-      crowd.base.url                          ${cfg.sso.crowd}/
-
-      session.isauthenticated                 session.isauthenticated
-      session.tokenkey                        session.tokenkey
-      session.validationinterval              ${toString cfg.sso.validationInterval}
-      session.lastvalidation                  session.lastvalidation
-    '';
   });
 
+  crowdProperties = pkgs.writeText "crowd.properties" ''
+    application.name                        ${cfg.sso.applicationName}
+    application.password                    @NIXOS_JIRA_CROWD_SSO_PWD@
+    application.login.url                   ${cfg.sso.crowd}/console/
+
+    crowd.server.url                        ${cfg.sso.crowd}/services/
+    crowd.base.url                          ${cfg.sso.crowd}/
+
+    session.isauthenticated                 session.isauthenticated
+    session.tokenkey                        session.tokenkey
+    session.validationinterval              ${toString cfg.sso.validationInterval}
+    session.lastvalidation                  session.lastvalidation
+  '';
+
 in
 
 {
@@ -33,38 +34,38 @@ in
       user = mkOption {
         type = types.str;
         default = "jira";
-        description = "User which runs JIRA.";
+        description = lib.mdDoc "User which runs JIRA.";
       };
 
       group = mkOption {
         type = types.str;
         default = "jira";
-        description = "Group which runs JIRA.";
+        description = lib.mdDoc "Group which runs JIRA.";
       };
 
       home = mkOption {
         type = types.str;
         default = "/var/lib/jira";
-        description = "Home directory of the JIRA instance.";
+        description = lib.mdDoc "Home directory of the JIRA instance.";
       };
 
       listenAddress = mkOption {
         type = types.str;
         default = "127.0.0.1";
-        description = "Address to listen on.";
+        description = lib.mdDoc "Address to listen on.";
       };
 
       listenPort = mkOption {
         type = types.int;
         default = 8091;
-        description = "Port to listen on.";
+        description = lib.mdDoc "Port to listen on.";
       };
 
       catalinaOptions = mkOption {
         type = types.listOf types.str;
         default = [];
         example = [ "-Xms1024m" "-Xmx2048m" ];
-        description = "Java options to pass to catalina/tomcat.";
+        description = lib.mdDoc "Java options to pass to catalina/tomcat.";
       };
 
       proxy = {
@@ -73,27 +74,27 @@ in
         name = mkOption {
           type = types.str;
           example = "jira.example.com";
-          description = "Virtual hostname at the proxy";
+          description = lib.mdDoc "Virtual hostname at the proxy";
         };
 
         port = mkOption {
           type = types.int;
           default = 443;
           example = 80;
-          description = "Port used at the proxy";
+          description = lib.mdDoc "Port used at the proxy";
         };
 
         scheme = mkOption {
           type = types.str;
           default = "https";
           example = "http";
-          description = "Protocol used at the proxy.";
+          description = lib.mdDoc "Protocol used at the proxy.";
         };
 
         secure = mkOption {
           type = types.bool;
           default = true;
-          description = "Whether the connections to the proxy should be considered secure.";
+          description = lib.mdDoc "Whether the connections to the proxy should be considered secure.";
         };
       };
 
@@ -103,25 +104,25 @@ in
         crowd = mkOption {
           type = types.str;
           example = "http://localhost:8095/crowd";
-          description = "Crowd Base URL without trailing slash";
+          description = lib.mdDoc "Crowd Base URL without trailing slash";
         };
 
         applicationName = mkOption {
           type = types.str;
           example = "jira";
-          description = "Exact name of this JIRA instance in Crowd";
+          description = lib.mdDoc "Exact name of this JIRA instance in Crowd";
         };
 
-        applicationPassword = mkOption {
+        applicationPasswordFile = mkOption {
           type = types.str;
-          description = "Application password of this JIRA instance in Crowd";
+          description = lib.mdDoc "Path to the file containing the application password of this JIRA instance in Crowd";
         };
 
         validationInterval = mkOption {
           type = types.int;
           default = 2;
           example = 0;
-          description = ''
+          description = lib.mdDoc ''
             Set to 0, if you want authentication checks to occur on each
             request. Otherwise set to the number of minutes between request
             to validate if the user is logged in or out of the Crowd SSO
@@ -135,14 +136,14 @@ in
         type = types.package;
         default = pkgs.atlassian-jira;
         defaultText = literalExpression "pkgs.atlassian-jira";
-        description = "Atlassian JIRA package to use.";
+        description = lib.mdDoc "Atlassian JIRA package to use.";
       };
 
       jrePackage = mkOption {
         type = types.package;
         default = pkgs.oraclejre8;
         defaultText = literalExpression "pkgs.oraclejre8";
-        description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
+        description = lib.mdDoc "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
       };
     };
   };
@@ -151,6 +152,7 @@ in
     users.users.${cfg.user} = {
       isSystemUser = true;
       group = cfg.group;
+      home = cfg.home;
     };
 
     users.groups.${cfg.group} = {};
@@ -180,6 +182,7 @@ in
         JIRA_HOME = cfg.home;
         JAVA_HOME = "${cfg.jrePackage}";
         CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
+        JAVA_OPTS = mkIf cfg.sso.enable "-Dcrowd.properties=${cfg.home}/crowd.properties";
       };
 
       preStart = ''
@@ -190,15 +193,31 @@ in
           -e 's,protocol="HTTP/1.1",protocol="HTTP/1.1" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}" secure="${toString cfg.proxy.secure}",' \
         '') + ''
           ${pkg}/conf/server.xml.dist > ${cfg.home}/server.xml
+
+        ${optionalString cfg.sso.enable ''
+          install -m660 ${crowdProperties} ${cfg.home}/crowd.properties
+          ${pkgs.replace-secret}/bin/replace-secret \
+            '@NIXOS_JIRA_CROWD_SSO_PWD@' \
+            ${cfg.sso.applicationPasswordFile} \
+            ${cfg.home}/crowd.properties
+        ''}
       '';
 
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
         PrivateTmp = true;
+        Restart = "on-failure";
+        RestartSec = "10";
         ExecStart = "${pkg}/bin/start-jira.sh -fg";
         ExecStop = "${pkg}/bin/stop-jira.sh";
       };
     };
   };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "jira" "sso" "applicationPassword" ] ''
+      Use `applicationPasswordFile` instead!
+    '')
+  ];
 }
diff --git a/nixpkgs/nixos/modules/services/web-apps/baget.nix b/nixpkgs/nixos/modules/services/web-apps/baget.nix
index 3007dd4fbb26..dd70d462d57d 100644
--- a/nixpkgs/nixos/modules/services/web-apps/baget.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/baget.nix
@@ -58,7 +58,7 @@ in
     apiKeyFile = mkOption {
       type = types.path;
       example = "/root/baget.key";
-      description = ''
+      description = lib.mdDoc ''
         Private API key for BaGet.
       '';
     };
@@ -112,8 +112,8 @@ in
           };
         }
       '';
-      description = ''
-        Extra configuration options for BaGet. Refer to <link xlink:href="https://loic-sharma.github.io/BaGet/configuration/"/> for details.
+      description = lib.mdDoc ''
+        Extra configuration options for BaGet. Refer to <https://loic-sharma.github.io/BaGet/configuration/> for details.
         Default value is merged with values from here.
       '';
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/bookstack.nix b/nixpkgs/nixos/modules/services/web-apps/bookstack.nix
index 64a2767fab6e..b939adc50fa3 100644
--- a/nixpkgs/nixos/modules/services/web-apps/bookstack.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/bookstack.nix
@@ -38,21 +38,21 @@ in {
 
     user = mkOption {
       default = "bookstack";
-      description = "User bookstack runs as.";
+      description = lib.mdDoc "User bookstack runs as.";
       type = types.str;
     };
 
     group = mkOption {
       default = "bookstack";
-      description = "Group bookstack runs as.";
+      description = lib.mdDoc "Group bookstack runs as.";
       type = types.str;
     };
 
     appKeyFile = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         A file containing the Laravel APP_KEY - a 32 character long,
         base64 encoded key used for encryption where needed. Can be
-        generated with <code>head -c 32 /dev/urandom | base64</code>.
+        generated with `head -c 32 /dev/urandom | base64`.
       '';
       example = "/run/keys/bookstack-appkey";
       type = types.path;
@@ -66,15 +66,15 @@ in {
                   config.networking.hostName;
       defaultText = lib.literalExpression "config.networking.fqdn";
       example = "bookstack.example.com";
-      description = ''
+      description = lib.mdDoc ''
         The hostname to serve BookStack on.
       '';
     };
 
     appURL = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         The root URL that you want to host BookStack on. All URLs in BookStack will be generated using this value.
-        If you change this in the future you may need to run a command to update stored URLs in the database. Command example: <code>php artisan bookstack:update-url https://old.example.com https://new.example.com</code>
+        If you change this in the future you may need to run a command to update stored URLs in the database. Command example: `php artisan bookstack:update-url https://old.example.com https://new.example.com`
       '';
       default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
       defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}'';
@@ -83,7 +83,7 @@ in {
     };
 
     dataDir = mkOption {
-      description = "BookStack data directory";
+      description = lib.mdDoc "BookStack data directory";
       default = "/var/lib/bookstack";
       type = types.path;
     };
@@ -92,37 +92,37 @@ in {
       host = mkOption {
         type = types.str;
         default = "localhost";
-        description = "Database host address.";
+        description = lib.mdDoc "Database host address.";
       };
       port = mkOption {
         type = types.port;
         default = 3306;
-        description = "Database host port.";
+        description = lib.mdDoc "Database host port.";
       };
       name = mkOption {
         type = types.str;
         default = "bookstack";
-        description = "Database name.";
+        description = lib.mdDoc "Database name.";
       };
       user = mkOption {
         type = types.str;
         default = user;
         defaultText = literalExpression "user";
-        description = "Database username.";
+        description = lib.mdDoc "Database username.";
       };
       passwordFile = mkOption {
         type = with types; nullOr path;
         default = null;
         example = "/run/keys/bookstack-dbpassword";
-        description = ''
+        description = lib.mdDoc ''
           A file containing the password corresponding to
-          <option>database.user</option>.
+          {option}`database.user`.
         '';
       };
       createLocally = mkOption {
         type = types.bool;
         default = false;
-        description = "Create the database and database user locally.";
+        description = lib.mdDoc "Create the database and database user locally.";
       };
     };
 
@@ -130,47 +130,47 @@ in {
       driver = mkOption {
         type = types.enum [ "smtp" "sendmail" ];
         default = "smtp";
-        description = "Mail driver to use.";
+        description = lib.mdDoc "Mail driver to use.";
       };
       host = mkOption {
         type = types.str;
         default = "localhost";
-        description = "Mail host address.";
+        description = lib.mdDoc "Mail host address.";
       };
       port = mkOption {
         type = types.port;
         default = 1025;
-        description = "Mail host port.";
+        description = lib.mdDoc "Mail host port.";
       };
       fromName = mkOption {
         type = types.str;
         default = "BookStack";
-        description = "Mail \"from\" name.";
+        description = lib.mdDoc "Mail \"from\" name.";
       };
       from = mkOption {
         type = types.str;
         default = "mail@bookstackapp.com";
-        description = "Mail \"from\" email.";
+        description = lib.mdDoc "Mail \"from\" email.";
       };
       user = mkOption {
         type = with types; nullOr str;
         default = null;
         example = "bookstack";
-        description = "Mail username.";
+        description = lib.mdDoc "Mail username.";
       };
       passwordFile = mkOption {
         type = with types; nullOr path;
         default = null;
         example = "/run/keys/bookstack-mailpassword";
-        description = ''
+        description = lib.mdDoc ''
           A file containing the password corresponding to
-          <option>mail.user</option>.
+          {option}`mail.user`.
         '';
       };
       encryption = mkOption {
         type = with types; nullOr (enum [ "tls" ]);
         default = null;
-        description = "SMTP encryption mechanism to use.";
+        description = lib.mdDoc "SMTP encryption mechanism to use.";
       };
     };
 
@@ -178,7 +178,7 @@ in {
       type = types.str;
       default = "18M";
       example = "1G";
-      description = "The maximum size for uploads (e.g. images).";
+      description = lib.mdDoc "The maximum size for uploads (e.g. images).";
     };
 
     poolConfig = mkOption {
@@ -191,8 +191,8 @@ in {
         "pm.max_spare_servers" = 4;
         "pm.max_requests" = 500;
       };
-      description = ''
-        Options for the bookstack PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+      description = lib.mdDoc ''
+        Options for the bookstack PHP pool. See the documentation on `php-fpm.conf`
         for details on configuration directives.
       '';
     };
@@ -213,7 +213,7 @@ in {
           enableACME = true;
         }
       '';
-      description = ''
+      description = lib.mdDoc ''
         With this option, you can customize the nginx virtualHost settings.
       '';
     };
@@ -256,20 +256,20 @@ in {
           OIDC_ISSUER_DISCOVER = true;
         }
       '';
-      description = ''
+      description = lib.mdDoc ''
         BookStack configuration options to set in the
-        <filename>.env</filename> file.
+        {file}`.env` file.
 
-        Refer to <link xlink:href="https://www.bookstackapp.com/docs/"/>
+        Refer to <https://www.bookstackapp.com/docs/>
         for details on supported values.
 
         Settings containing secret data should be set to an attribute
-        set containing the attribute <literal>_secret</literal> - a
+        set containing the attribute `_secret` - a
         string pointing to a file containing the value the option
         should be set to. See the example to get a better picture of
-        this: in the resulting <filename>.env</filename> file, the
-        <literal>OIDC_CLIENT_SECRET</literal> key will be set to the
-        contents of the <filename>/run/keys/oidc_secret</filename>
+        this: in the resulting {file}`.env` file, the
+        `OIDC_CLIENT_SECRET` key will be set to the
+        contents of the {file}`/run/keys/oidc_secret`
         file.
       '';
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/calibre-web.nix b/nixpkgs/nixos/modules/services/web-apps/calibre-web.nix
index 704cd2cfa8a7..6bcf733452b9 100644
--- a/nixpkgs/nixos/modules/services/web-apps/calibre-web.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/calibre-web.nix
@@ -14,7 +14,7 @@ in
         ip = mkOption {
           type = types.str;
           default = "::1";
-          description = ''
+          description = lib.mdDoc ''
             IP address that Calibre-Web should listen on.
           '';
         };
@@ -22,7 +22,7 @@ in
         port = mkOption {
           type = types.port;
           default = 8083;
-          description = ''
+          description = lib.mdDoc ''
             Listen port for Calibre-Web.
           '';
         };
@@ -31,27 +31,27 @@ in
       dataDir = mkOption {
         type = types.str;
         default = "calibre-web";
-        description = ''
-          The directory below <filename>/var/lib</filename> where Calibre-Web stores its data.
+        description = lib.mdDoc ''
+          The directory below {file}`/var/lib` where Calibre-Web stores its data.
         '';
       };
 
       user = mkOption {
         type = types.str;
         default = "calibre-web";
-        description = "User account under which Calibre-Web runs.";
+        description = lib.mdDoc "User account under which Calibre-Web runs.";
       };
 
       group = mkOption {
         type = types.str;
         default = "calibre-web";
-        description = "Group account under which Calibre-Web runs.";
+        description = lib.mdDoc "Group account under which Calibre-Web runs.";
       };
 
       openFirewall = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Open ports in the firewall for the server.
         '';
       };
@@ -60,7 +60,7 @@ in
         calibreLibrary = mkOption {
           type = types.nullOr types.path;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             Path to Calibre library.
           '';
         };
@@ -68,7 +68,7 @@ in
         enableBookConversion = mkOption {
           type = types.bool;
           default = false;
-          description = ''
+          description = lib.mdDoc ''
             Configure path to the Calibre's ebook-convert in the DB.
           '';
         };
@@ -76,7 +76,7 @@ in
         enableBookUploading = mkOption {
           type = types.bool;
           default = false;
-          description = ''
+          description = lib.mdDoc ''
             Allow books to be uploaded via Calibre-Web UI.
           '';
         };
@@ -85,7 +85,7 @@ in
           enable = mkOption {
             type = types.bool;
             default = false;
-            description = ''
+            description = lib.mdDoc ''
               Enable authorization using auth proxy.
             '';
           };
@@ -93,7 +93,7 @@ in
           header = mkOption {
             type = types.str;
             default = "";
-            description = ''
+            description = lib.mdDoc ''
               Auth proxy header name.
             '';
           };
@@ -136,7 +136,7 @@ in
 
               ${pkgs.sqlite}/bin/sqlite3 ${appDb} "update settings set ${settings}"
             '' + optionalString (cfg.options.calibreLibrary != null) ''
-              test -f ${cfg.options.calibreLibrary}/metadata.db || { echo "Invalid Calibre library"; exit 1; }
+              test -f "${cfg.options.calibreLibrary}/metadata.db" || { echo "Invalid Calibre library"; exit 1; }
             ''
           );
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/code-server.nix b/nixpkgs/nixos/modules/services/web-apps/code-server.nix
index 474e9140ae87..84fc03deabff 100644
--- a/nixpkgs/nixos/modules/services/web-apps/code-server.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/code-server.nix
@@ -16,13 +16,13 @@ in {
       package = mkOption {
         default = pkgs.code-server;
         defaultText = "pkgs.code-server";
-        description = "Which code-server derivation to use.";
+        description = lib.mdDoc "Which code-server derivation to use.";
         type = types.package;
       };
 
       extraPackages = mkOption {
         default = [ ];
-        description = "Packages that are available in the PATH of code-server.";
+        description = lib.mdDoc "Packages that are available in the PATH of code-server.";
         example = "[ pkgs.go ]";
         type = types.listOf types.package;
       };
@@ -30,49 +30,49 @@ in {
       extraEnvironment = mkOption {
         type = types.attrsOf types.str;
         description =
-          "Additional environment variables to passed to code-server.";
+          lib.mdDoc "Additional environment variables to passed to code-server.";
         default = { };
         example = { PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig"; };
       };
 
       extraArguments = mkOption {
         default = [ "--disable-telemetry" ];
-        description = "Additional arguments that passed to code-server";
+        description = lib.mdDoc "Additional arguments that passed to code-server";
         example = ''[ "--verbose" ]'';
         type = types.listOf types.str;
       };
 
       host = mkOption {
         default = "127.0.0.1";
-        description = "The host-ip to bind to.";
+        description = lib.mdDoc "The host-ip to bind to.";
         type = types.str;
       };
 
       port = mkOption {
         default = 4444;
-        description = "The port where code-server runs.";
+        description = lib.mdDoc "The port where code-server runs.";
         type = types.port;
       };
 
       auth = mkOption {
         default = "password";
-        description = "The type of authentication to use.";
+        description = lib.mdDoc "The type of authentication to use.";
         type = types.enum [ "none" "password" ];
       };
 
       hashedPassword = mkOption {
         default = "";
         description =
-          "Create the password with: 'echo -n 'thisismypassword' | npx argon2-cli -e'.";
+          lib.mdDoc "Create the password with: 'echo -n 'thisismypassword' | npx argon2-cli -e'.";
         type = types.str;
       };
 
       user = mkOption {
         default = defaultUser;
         example = "yourUser";
-        description = ''
+        description = lib.mdDoc ''
           The user to run code-server as.
-          By default, a user named <literal>${defaultUser}</literal> will be created.
+          By default, a user named `${defaultUser}` will be created.
         '';
         type = types.str;
       };
@@ -80,9 +80,9 @@ in {
       group = mkOption {
         default = defaultGroup;
         example = "yourGroup";
-        description = ''
+        description = lib.mdDoc ''
           The group to run code-server under.
-          By default, a group named <literal>${defaultGroup}</literal> will be created.
+          By default, a group named `${defaultGroup}` will be created.
         '';
         type = types.str;
       };
@@ -90,7 +90,7 @@ in {
       extraGroups = mkOption {
         default = [ ];
         description =
-          "An array of additional groups for the <literal>${defaultUser}</literal> user.";
+          lib.mdDoc "An array of additional groups for the `${defaultUser}` user.";
         example = [ "docker" ];
         type = types.listOf types.str;
       };
diff --git a/nixpkgs/nixos/modules/services/web-apps/convos.nix b/nixpkgs/nixos/modules/services/web-apps/convos.nix
index 8be11eec9f31..120481c64017 100644
--- a/nixpkgs/nixos/modules/services/web-apps/convos.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/convos.nix
@@ -12,21 +12,21 @@ in
       type = types.port;
       default = 3000;
       example = 8080;
-      description = "Port the web interface should listen on";
+      description = lib.mdDoc "Port the web interface should listen on";
     };
     listenAddress = mkOption {
       type = types.str;
       default = "*";
       example = "127.0.0.1";
-      description = "Address or host the web interface should listen on";
+      description = lib.mdDoc "Address or host the web interface should listen on";
     };
     reverseProxy = mkOption {
       type = types.bool;
       default = false;
-      description = ''
+      description = lib.mdDoc ''
         Enables reverse proxy support. This will allow Convos to automatically
-        pick up the <literal>X-Forwarded-For</literal> and
-        <literal>X-Request-Base</literal> HTTP headers set in your reverse proxy
+        pick up the `X-Forwarded-For` and
+        `X-Request-Base` HTTP headers set in your reverse proxy
         web server. Note that enabling this option without a reverse proxy in
         front will be a security issue.
       '';
diff --git a/nixpkgs/nixos/modules/services/web-apps/cryptpad.nix b/nixpkgs/nixos/modules/services/web-apps/cryptpad.nix
deleted file mode 100644
index e6772de768e0..000000000000
--- a/nixpkgs/nixos/modules/services/web-apps/cryptpad.nix
+++ /dev/null
@@ -1,54 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.cryptpad;
-in
-{
-  options.services.cryptpad = {
-    enable = mkEnableOption "the Cryptpad service";
-
-    package = mkOption {
-      default = pkgs.cryptpad;
-      defaultText = literalExpression "pkgs.cryptpad";
-      type = types.package;
-      description = "
-        Cryptpad package to use.
-      ";
-    };
-
-    configFile = mkOption {
-      type = types.path;
-      default = "${cfg.package}/lib/node_modules/cryptpad/config/config.example.js";
-      defaultText = literalExpression ''"''${package}/lib/node_modules/cryptpad/config/config.example.js"'';
-      description = ''
-        Path to the JavaScript configuration file.
-
-        See <link
-        xlink:href="https://github.com/xwiki-labs/cryptpad/blob/master/config/config.example.js"/>
-        for a configuration example.
-      '';
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services.cryptpad = {
-      description = "Cryptpad Service";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "networking.target" ];
-      serviceConfig = {
-        DynamicUser = true;
-        Environment = [
-          "CRYPTPAD_CONFIG=${cfg.configFile}"
-          "HOME=%S/cryptpad"
-        ];
-        ExecStart = "${cfg.package}/bin/cryptpad";
-        PrivateTmp = true;
-        Restart = "always";
-        StateDirectory = "cryptpad";
-        WorkingDirectory = "%S/cryptpad";
-      };
-    };
-  };
-}
diff --git a/nixpkgs/nixos/modules/services/web-apps/dex.nix b/nixpkgs/nixos/modules/services/web-apps/dex.nix
index 4d4689a4cf24..82fdcd212f96 100644
--- a/nixpkgs/nixos/modules/services/web-apps/dex.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/dex.nix
@@ -11,15 +11,26 @@ let
   settingsFormat = pkgs.formats.yaml {};
   configFile = settingsFormat.generate "config.yaml" filteredSettings;
 
-  startPreScript = pkgs.writeShellScript "dex-start-pre" (''
-  '' + (concatStringsSep "\n" (builtins.map (file: ''
-    ${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' /run/dex/config.yaml
-  '') secretFiles)));
+  startPreScript = pkgs.writeShellScript "dex-start-pre"
+    (concatStringsSep "\n" (map (file: ''
+      replace-secret '${file}' '${file}' /run/dex/config.yaml
+    '')
+    secretFiles));
 in
 {
   options.services.dex = {
     enable = mkEnableOption "the OpenID Connect and OAuth2 identity provider";
 
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Environment file (see <literal>systemd.exec(5)</literal>
+        "EnvironmentFile=" section for the syntax) to define variables for dex.
+        This option can be used to safely include secret keys into the dex configuration.
+      '';
+    };
+
     settings = mkOption {
       type = settingsFormat.type;
       default = {};
@@ -45,9 +56,12 @@ in
           ];
         }
       '';
-      description = ''
+      description = lib.mdDoc ''
         The available options can be found in
-        <link xlink:href="https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist">the example configuration</link>.
+        [the example configuration](https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist).
+
+        It's also possible to refer to environment variables (defined in [services.dex.environmentFile](#opt-services.dex.environmentFile))
+        using the syntax `$VARIABLE_NAME`.
       '';
     };
   };
@@ -57,15 +71,15 @@ in
       description = "dex identity provider";
       wantedBy = [ "multi-user.target" ];
       after = [ "networking.target" ] ++ (optional (cfg.settings.storage.type == "postgres") "postgresql.service");
-
+      path = with pkgs; [ replace-secret ];
       serviceConfig = {
         ExecStart = "${pkgs.dex-oidc}/bin/dex serve /run/dex/config.yaml";
         ExecStartPre = [
           "${pkgs.coreutils}/bin/install -m 600 ${configFile} /run/dex/config.yaml"
           "+${startPreScript}"
         ];
-        RuntimeDirectory = "dex";
 
+        RuntimeDirectory = "dex";
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
         BindReadOnlyPaths = [
           "/nix/store"
@@ -109,6 +123,8 @@ in
         TemporaryFileSystem = "/:ro";
         # Does not work well with the temporary root
         #UMask = "0066";
+      } // optionalAttrs (cfg.environmentFile != null) {
+        EnvironmentFile = cfg.environmentFile;
       };
     };
   };
diff --git a/nixpkgs/nixos/modules/services/web-apps/discourse.nix b/nixpkgs/nixos/modules/services/web-apps/discourse.nix
index 2c2911aada3f..1e2326d81801 100644
--- a/nixpkgs/nixos/modules/services/web-apps/discourse.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/discourse.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.discourse;
   opt = options.services.discourse;
 
-  # Keep in sync with https://github.com/discourse/discourse_docker/blob/master/image/base/Dockerfile#L5
+  # Keep in sync with https://github.com/discourse/discourse_docker/blob/main/image/base/slim.Dockerfile#L5
   upstreamPostgresqlVersion = lib.getVersion pkgs.postgresql_13;
 
   postgresqlPackage = if config.services.postgresql.enable then
@@ -35,7 +35,7 @@ in
           plugins = lib.unique (p.enabledPlugins ++ cfg.plugins);
         };
         defaultText = lib.literalExpression "pkgs.discourse";
-        description = ''
+        description = lib.mdDoc ''
           The discourse package to use.
         '';
       };
@@ -48,7 +48,7 @@ in
                     config.networking.hostName;
         defaultText = lib.literalExpression "config.networking.fqdn";
         example = "discourse.example.com";
-        description = ''
+        description = lib.mdDoc ''
           The hostname to serve Discourse on.
         '';
       };
@@ -81,7 +81,7 @@ in
         type = with lib.types; nullOr path;
         default = null;
         example = "/run/keys/ssl.cert";
-        description = ''
+        description = lib.mdDoc ''
           The path to the server SSL certificate. Set this to enable
           SSL.
         '';
@@ -91,7 +91,7 @@ in
         type = with lib.types; nullOr path;
         default = null;
         example = "/run/keys/ssl.key";
-        description = ''
+        description = lib.mdDoc ''
           The path to the server SSL certificate key. Set this to
           enable SSL.
         '';
@@ -104,7 +104,7 @@ in
           <literal>true</literal>, unless <option>services.discourse.sslCertificate</option>
           and <option>services.discourse.sslCertificateKey</option> are set.
         '';
-        description = ''
+        description = lib.mdDoc ''
           Whether an ACME certificate should be used to secure
           connections to the server.
         '';
@@ -151,26 +151,26 @@ in
             };
           };
         '';
-        description = ''
+        description = lib.mdDoc ''
           Discourse site settings. These are the settings that can be
           changed from the UI. This only defines their default values:
           they can still be overridden from the UI.
 
           Available settings can be found by looking in the
-          <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">site_settings.yml</link>
+          [site_settings.yml](https://github.com/discourse/discourse/blob/master/config/site_settings.yml)
           file of the upstream distribution. To find a setting's path,
           you only need to care about the first two levels; i.e. its
           category and name. See the example.
 
           Settings containing secret data should be set to an
           attribute set containing the attribute
-          <literal>_secret</literal> - a string pointing to a file
+          `_secret` - a string pointing to a file
           containing the value the option should be set to. See the
           example to get a better picture of this: in the resulting
-          <filename>config/nixos_site_settings.json</filename> file,
-          the <literal>login.github_client_secret</literal> key will
+          {file}`config/nixos_site_settings.json` file,
+          the `login.github_client_secret` key will
           be set to the contents of the
-          <filename>/run/keys/discourse_github_client_secret</filename>
+          {file}`/run/keys/discourse_github_client_secret`
           file.
         '';
       };
@@ -179,7 +179,7 @@ in
         skipCreate = lib.mkOption {
           type = lib.types.bool;
           default = false;
-          description = ''
+          description = lib.mdDoc ''
             Do not create the admin account, instead rely on other
             existing admin accounts.
           '';
@@ -188,7 +188,7 @@ in
         email = lib.mkOption {
           type = lib.types.str;
           example = "admin@example.com";
-          description = ''
+          description = lib.mdDoc ''
             The admin user email address.
           '';
         };
@@ -196,21 +196,21 @@ in
         username = lib.mkOption {
           type = lib.types.str;
           example = "admin";
-          description = ''
+          description = lib.mdDoc ''
             The admin user username.
           '';
         };
 
         fullName = lib.mkOption {
           type = lib.types.str;
-          description = ''
+          description = lib.mdDoc ''
             The admin user's full name.
           '';
         };
 
         passwordFile = lib.mkOption {
           type = lib.types.path;
-          description = ''
+          description = lib.mdDoc ''
             A path to a file containing the admin user's password.
 
             This should be a string, not a nix path, since nix paths are
@@ -222,8 +222,8 @@ in
       nginx.enable = lib.mkOption {
         type = lib.types.bool;
         default = true;
-        description = ''
-          Whether an <literal>nginx</literal> virtual host should be
+        description = lib.mdDoc ''
+          Whether an `nginx` virtual host should be
           set up to serve Discourse. Only disable if you're planning
           to use a different web server, which is not recommended.
         '';
@@ -233,7 +233,7 @@ in
         pool = lib.mkOption {
           type = lib.types.int;
           default = 8;
-          description = ''
+          description = lib.mdDoc ''
             Database connection pool size.
           '';
         };
@@ -250,7 +250,7 @@ in
         passwordFile = lib.mkOption {
           type = with lib.types; nullOr path;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             File containing the Discourse database user password.
 
             This should be a string, not a nix path, since nix paths are
@@ -261,18 +261,18 @@ in
         createLocally = lib.mkOption {
           type = lib.types.bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             Whether a database should be automatically created on the
-            local host. Set this to <literal>false</literal> if you plan
+            local host. Set this to `false` if you plan
             on provisioning a local database yourself. This has no effect
-            if <option>services.discourse.database.host</option> is customized.
+            if {option}`services.discourse.database.host` is customized.
           '';
         };
 
         name = lib.mkOption {
           type = lib.types.str;
           default = "discourse";
-          description = ''
+          description = lib.mdDoc ''
             Discourse database name.
           '';
         };
@@ -280,7 +280,7 @@ in
         username = lib.mkOption {
           type = lib.types.str;
           default = "discourse";
-          description = ''
+          description = lib.mdDoc ''
             Discourse database user.
           '';
         };
@@ -288,10 +288,10 @@ in
         ignorePostgresqlVersion = lib.mkOption {
           type = lib.types.bool;
           default = false;
-          description = ''
+          description = lib.mdDoc ''
             Whether to allow other versions of PostgreSQL than the
             recommended one. Only effective when
-            <option>services.discourse.database.createLocally</option>
+            {option}`services.discourse.database.createLocally`
             is enabled.
           '';
         };
@@ -301,7 +301,7 @@ in
         host = lib.mkOption {
           type = lib.types.str;
           default = "localhost";
-          description = ''
+          description = lib.mdDoc ''
             Redis server hostname.
           '';
         };
@@ -309,7 +309,7 @@ in
         passwordFile = lib.mkOption {
           type = with lib.types; nullOr path;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             File containing the Redis password.
 
             This should be a string, not a nix path, since nix paths are
@@ -320,7 +320,7 @@ in
         dbNumber = lib.mkOption {
           type = lib.types.int;
           default = 0;
-          description = ''
+          description = lib.mdDoc ''
             Redis database number.
           '';
         };
@@ -329,7 +329,7 @@ in
           type = lib.types.bool;
           default = cfg.redis.host != "localhost";
           defaultText = lib.literalExpression ''config.${opt.redis.host} != "localhost"'';
-          description = ''
+          description = lib.mdDoc ''
             Connect to Redis with SSL.
           '';
         };
@@ -342,8 +342,8 @@ in
           defaultText = lib.literalExpression ''
             "''${if config.services.discourse.mail.incoming.enable then "notifications" else "noreply"}@''${config.services.discourse.hostname}"
           '';
-          description = ''
-            The <literal>from:</literal> email address used when
+          description = lib.mdDoc ''
+            The `from:` email address used when
             sending all essential system emails. The domain specified
             here must have SPF, DKIM and reverse PTR records set
             correctly for email to arrive.
@@ -353,10 +353,10 @@ in
         contactEmailAddress = lib.mkOption {
           type = lib.types.str;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             Email address of key contact responsible for this
             site. Used for critical notifications, as well as on the
-            <literal>/about</literal> contact form for urgent matters.
+            `/about` contact form for urgent matters.
           '';
         };
 
@@ -364,7 +364,7 @@ in
           serverAddress = lib.mkOption {
             type = lib.types.str;
             default = "localhost";
-            description = ''
+            description = lib.mdDoc ''
               The address of the SMTP server Discourse should use to
               send email.
             '';
@@ -373,7 +373,7 @@ in
           port = lib.mkOption {
             type = lib.types.port;
             default = 25;
-            description = ''
+            description = lib.mdDoc ''
               The port of the SMTP server Discourse should use to
               send email.
             '';
@@ -382,7 +382,7 @@ in
           username = lib.mkOption {
             type = with lib.types; nullOr str;
             default = null;
-            description = ''
+            description = lib.mdDoc ''
               The username of the SMTP server.
             '';
           };
@@ -390,7 +390,7 @@ in
           passwordFile = lib.mkOption {
             type = lib.types.nullOr lib.types.path;
             default = null;
-            description = ''
+            description = lib.mdDoc ''
               A file containing the password of the SMTP server account.
 
               This should be a string, not a nix path, since nix paths
@@ -402,7 +402,7 @@ in
             type = lib.types.str;
             default = cfg.hostname;
             defaultText = lib.literalExpression "config.${opt.hostname}";
-            description = ''
+            description = lib.mdDoc ''
               HELO domain to use for outgoing mail.
             '';
           };
@@ -410,7 +410,7 @@ in
           authentication = lib.mkOption {
             type = with lib.types; nullOr (enum ["plain" "login" "cram_md5"]);
             default = null;
-            description = ''
+            description = lib.mdDoc ''
               Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html
             '';
           };
@@ -418,7 +418,7 @@ in
           enableStartTLSAuto = lib.mkOption {
             type = lib.types.bool;
             default = true;
-            description = ''
+            description = lib.mdDoc ''
               Whether to try to use StartTLS.
             '';
           };
@@ -426,7 +426,7 @@ in
           opensslVerifyMode = lib.mkOption {
             type = lib.types.str;
             default = "peer";
-            description = ''
+            description = lib.mdDoc ''
               How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html
             '';
           };
@@ -434,7 +434,7 @@ in
           forceTLS = lib.mkOption {
             type = lib.types.bool;
             default = false;
-            description = ''
+            description = lib.mdDoc ''
               Force implicit TLS as per RFC 8314 3.3.
             '';
           };
@@ -444,7 +444,7 @@ in
           enable = lib.mkOption {
             type = lib.types.bool;
             default = false;
-            description = ''
+            description = lib.mdDoc ''
               Whether to set up Postfix to receive incoming mail.
             '';
           };
@@ -453,7 +453,7 @@ in
             type = lib.types.str;
             default = "%{reply_key}@${cfg.hostname}";
             defaultText = lib.literalExpression ''"%{reply_key}@''${config.services.discourse.hostname}"'';
-            description = ''
+            description = lib.mdDoc ''
               Template for reply by email incoming email address, for
               example: %{reply_key}@reply.example.com or
               replies+%{reply_key}@example.com
@@ -464,7 +464,7 @@ in
             type = lib.types.package;
             default = pkgs.discourse-mail-receiver;
             defaultText = lib.literalExpression "pkgs.discourse-mail-receiver";
-            description = ''
+            description = lib.mdDoc ''
               The discourse-mail-receiver package to use.
             '';
           };
@@ -472,10 +472,10 @@ in
           apiKeyFile = lib.mkOption {
             type = lib.types.nullOr lib.types.path;
             default = null;
-            description = ''
+            description = lib.mdDoc ''
               A file containing the Discourse API key used to add
               posts and messages from mail. If left at its default
-              value <literal>null</literal>, one will be automatically
+              value `null`, one will be automatically
               generated.
 
               This should be a string, not a nix path, since nix paths
@@ -504,7 +504,7 @@ in
       sidekiqProcesses = lib.mkOption {
         type = lib.types.int;
         default = 1;
-        description = ''
+        description = lib.mdDoc ''
           How many Sidekiq processes should be spawned.
         '';
       };
@@ -512,7 +512,7 @@ in
       unicornTimeout = lib.mkOption {
         type = lib.types.int;
         default = 30;
-        description = ''
+        description = lib.mdDoc ''
           Time in seconds before a request to Unicorn times out.
 
           This can be raised if the system Discourse is running on is
@@ -604,11 +604,11 @@ in
       cors_origin = "";
       serve_static_assets = false;
       sidekiq_workers = 5;
-      rtl_css = false;
       connection_reaper_age = 30;
       connection_reaper_interval = 30;
       relative_url_root = null;
       message_bus_max_backlog_size = 100;
+      message_bus_clear_every = 50;
       secret_key_base = cfg.secretKeyBaseFile;
       fallback_assets_path = null;
 
@@ -655,7 +655,12 @@ in
       long_polling_interval = null;
     };
 
-    services.redis.enable = lib.mkDefault (cfg.redis.host == "localhost");
+    services.redis.servers.discourse =
+      lib.mkIf (lib.elem cfg.redis.host [ "localhost" "127.0.0.1" ]) {
+        enable = true;
+        bind = cfg.redis.host;
+        port = cfg.backendSettings.redis_port;
+      };
 
     services.postgresql = lib.mkIf databaseActuallyCreateLocally {
       enable = true;
@@ -696,12 +701,12 @@ in
     systemd.services.discourse = {
       wantedBy = [ "multi-user.target" ];
       after = [
-        "redis.service"
+        "redis-discourse.service"
         "postgresql.service"
         "discourse-postgresql.service"
       ];
       bindsTo = [
-        "redis.service"
+        "redis-discourse.service"
       ] ++ lib.optionals (cfg.database.host == null) [
         "postgresql.service"
         "discourse-postgresql.service"
@@ -934,7 +939,6 @@ in
                   proxy_cache discourse;
                   proxy_cache_key "$scheme,$host,$request_uri";
                   proxy_cache_valid 200 301 302 7d;
-                  proxy_cache_valid any 1m;
                 '';
               };
               "/message-bus/" = proxy {
diff --git a/nixpkgs/nixos/modules/services/web-apps/documize.nix b/nixpkgs/nixos/modules/services/web-apps/documize.nix
index 7f2ed82ee33e..4353e3c24453 100644
--- a/nixpkgs/nixos/modules/services/web-apps/documize.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/documize.nix
@@ -17,8 +17,8 @@ in {
     stateDirectoryName = mkOption {
       type = types.str;
       default = "documize";
-      description = ''
-        The name of the directory below <filename>/var/lib/private</filename>
+      description = lib.mdDoc ''
+        The name of the directory below {file}`/var/lib/private`
         where documize runs in and stores, for example, backups.
       '';
     };
@@ -27,7 +27,7 @@ in {
       type = types.package;
       default = pkgs.documize-community;
       defaultText = literalExpression "pkgs.documize-community";
-      description = ''
+      description = lib.mdDoc ''
         Which package to use for documize.
       '';
     };
@@ -36,7 +36,7 @@ in {
       type = types.nullOr types.str;
       default = null;
       example = "3edIYV6c8B28b19fh";
-      description = ''
+      description = lib.mdDoc ''
         The salt string used to encode JWT tokens, if not set a random value will be generated.
       '';
     };
@@ -44,23 +44,23 @@ in {
     cert = mkOption {
       type = types.nullOr types.str;
       default = null;
-      description = ''
-        The <filename>cert.pem</filename> file used for https.
+      description = lib.mdDoc ''
+        The {file}`cert.pem` file used for https.
       '';
     };
 
     key = mkOption {
       type = types.nullOr types.str;
       default = null;
-      description = ''
-        The <filename>key.pem</filename> file used for https.
+      description = lib.mdDoc ''
+        The {file}`key.pem` file used for https.
       '';
     };
 
     port = mkOption {
       type = types.port;
       default = 5001;
-      description = ''
+      description = lib.mdDoc ''
         The http/https port number.
       '';
     };
@@ -68,7 +68,7 @@ in {
     forcesslport = mkOption {
       type = types.nullOr types.port;
       default = null;
-      description = ''
+      description = lib.mdDoc ''
         Redirect given http port number to TLS.
       '';
     };
@@ -76,8 +76,8 @@ in {
     offline = mkOption {
       type = types.bool;
       default = false;
-      description = ''
-        Set <literal>true</literal> for offline mode.
+      description = lib.mdDoc ''
+        Set `true` for offline mode.
       '';
       apply = v: if true == v then 1 else 0;
     };
@@ -122,7 +122,7 @@ in {
     location = mkOption {
       type = types.nullOr types.str;
       default = null;
-      description = ''
+      description = lib.mdDoc ''
         reserved
       '';
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/dokuwiki.nix b/nixpkgs/nixos/modules/services/web-apps/dokuwiki.nix
index 1f8ca742db95..a148dec8199a 100644
--- a/nixpkgs/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/dokuwiki.nix
@@ -66,21 +66,21 @@ let
           type = types.package;
           default = pkgs.dokuwiki;
           defaultText = literalExpression "pkgs.dokuwiki";
-          description = "Which DokuWiki package to use.";
+          description = lib.mdDoc "Which DokuWiki package to use.";
         };
 
         stateDir = mkOption {
           type = types.path;
           default = "/var/lib/dokuwiki/${name}/data";
-          description = "Location of the DokuWiki state directory.";
+          description = lib.mdDoc "Location of the DokuWiki state directory.";
         };
 
         acl = mkOption {
           type = types.nullOr types.lines;
           default = null;
           example = "*               @ALL               8";
-          description = ''
-            Access Control Lists: see <link xlink:href="https://www.dokuwiki.org/acl"/>
+          description = lib.mdDoc ''
+            Access Control Lists: see <https://www.dokuwiki.org/acl>
             Mutually exclusive with services.dokuwiki.aclFile
             Set this to a value other than null to take precedence over aclFile option.
 
@@ -92,11 +92,11 @@ let
         aclFile = mkOption {
           type = with types; nullOr str;
           default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
-          description = ''
+          description = lib.mdDoc ''
             Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
             Mutually exclusive with services.dokuwiki.acl which is preferred.
-            Consult documentation <link xlink:href="https://www.dokuwiki.org/acl"/> for further instructions.
-            Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist"/>
+            Consult documentation <https://www.dokuwiki.org/acl> for further instructions.
+            Example: <https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist>
           '';
           example = "/var/lib/dokuwiki/${name}/acl.auth.php";
         };
@@ -104,7 +104,7 @@ let
         aclUse = mkOption {
           type = types.bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             Necessary for users to log in into the system.
             Also limits anonymous users. When disabled,
             everyone is able to create and edit content.
@@ -119,7 +119,7 @@ let
             $plugins['authmysql'] = 0;
             $plugins['authpgsql'] = 0;
           '';
-          description = ''
+          description = lib.mdDoc ''
             List of the dokuwiki (un)loaded plugins.
           '';
         };
@@ -127,10 +127,10 @@ let
         superUser = mkOption {
           type = types.nullOr types.str;
           default = "@admin";
-          description = ''
+          description = lib.mdDoc ''
             You can set either a username, a list of usernames (“admin1,admin2”),
             or the name of a group by prepending an @ char to the groupname
-            Consult documentation <link xlink:href="https://www.dokuwiki.org/config:superuser"/> for further instructions.
+            Consult documentation <https://www.dokuwiki.org/config:superuser> for further instructions.
           '';
         };
 
@@ -150,9 +150,9 @@ let
           type = types.nullOr types.str;
           default = "";
           example = "search,register";
-          description = ''
+          description = lib.mdDoc ''
             Disable individual action modes. Refer to
-            <link xlink:href="https://www.dokuwiki.org/config:action_modes"/>
+            <https://www.dokuwiki.org/config:action_modes>
             for details on supported values.
           '';
         };
@@ -222,8 +222,8 @@ let
             "pm.max_spare_servers" = 4;
             "pm.max_requests" = 500;
           };
-          description = ''
-            Options for the DokuWiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+          description = lib.mdDoc ''
+            Options for the DokuWiki PHP pool. See the documentation on `php-fpm.conf`
             for details on configuration directives.
           '';
         };
@@ -235,9 +235,9 @@ let
             $conf['title'] = 'My Wiki';
             $conf['userewrite'] = 1;
           '';
-          description = ''
+          description = lib.mdDoc ''
             DokuWiki configuration. Refer to
-            <link xlink:href="https://www.dokuwiki.org/config"/>
+            <https://www.dokuwiki.org/config>
             for details on supported values.
           '';
         };
@@ -254,20 +254,20 @@ in
       sites = mkOption {
         type = types.attrsOf (types.submodule siteOpts);
         default = {};
-        description = "Specification of one or more DokuWiki sites to serve";
+        description = lib.mdDoc "Specification of one or more DokuWiki sites to serve";
       };
 
       webserver = mkOption {
         type = types.enum [ "nginx" "caddy" ];
         default = "nginx";
-        description = ''
+        description = lib.mdDoc ''
           Whether to use nginx or caddy for virtual host management.
 
-          Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
-          See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+          Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
+          See [](#opt-services.nginx.virtualHosts) for further information.
 
-          Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
-          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+          Further apache2 configuration can be done by adapting `services.httpd.virtualHosts.<name>`.
+          See [](#opt-services.httpd.virtualHosts) for further information.
         '';
       };
 
@@ -293,9 +293,7 @@ in
         inherit user;
         group = webserver.group;
 
-        # Not yet compatible with php 8 https://www.dokuwiki.org/requirements
-        # https://github.com/splitbrain/dokuwiki/issues/3545
-        phpPackage = pkgs.php74;
+        phpPackage = pkgs.php81;
         phpEnv = {
           DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
           DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
diff --git a/nixpkgs/nixos/modules/services/web-apps/engelsystem.nix b/nixpkgs/nixos/modules/services/web-apps/engelsystem.nix
index 06c3c6dfc3d7..f1d71f174471 100644
--- a/nixpkgs/nixos/modules/services/web-apps/engelsystem.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/engelsystem.nix
@@ -9,7 +9,7 @@ in {
       enable = mkOption {
         default = false;
         example = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to enable engelsystem, an online tool for coordinating volunteers
           and shifts on large events.
         '';
@@ -19,12 +19,12 @@ in {
       domain = mkOption {
         type = types.str;
         example = "engelsystem.example.com";
-        description = "Domain to serve on.";
+        description = lib.mdDoc "Domain to serve on.";
       };
 
       package = mkOption {
         type = types.package;
-        description = "Engelsystem package used for the service.";
+        description = lib.mdDoc "Engelsystem package used for the service.";
         default = pkgs.engelsystem;
         defaultText = literalExpression "pkgs.engelsystem";
       };
@@ -32,9 +32,9 @@ in {
       createDatabase = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to create a local database automatically.
-          This will override every database setting in <option>services.engelsystem.config</option>.
+          This will override every database setting in {option}`services.engelsystem.config`.
         '';
       };
     };
@@ -70,7 +70,7 @@ in {
         min_password_length = 6;
         default_locale = "de_DE";
       };
-      description = ''
+      description = lib.mdDoc ''
         Options to be added to config.php, as a nix attribute set. Options containing secret data
         should be set to an attribute set containing the attribute _secret - a string pointing to a
         file containing the value the option should be set to. See the example to get a better
diff --git a/nixpkgs/nixos/modules/services/web-apps/ethercalc.nix b/nixpkgs/nixos/modules/services/web-apps/ethercalc.nix
index d74def59c6c3..a5be86a34aa6 100644
--- a/nixpkgs/nixos/modules/services/web-apps/ethercalc.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/ethercalc.nix
@@ -10,11 +10,11 @@ in {
       enable = mkOption {
         default = false;
         type = types.bool;
-        description = ''
+        description = lib.mdDoc ''
           ethercalc, an online collaborative spreadsheet server.
 
           Persistent state will be maintained under
-          <filename>/var/lib/ethercalc</filename>. Upstream supports using a
+          {file}`/var/lib/ethercalc`. Upstream supports using a
           redis server for storage and recommends the redis backend for
           intensive use; however, the Nix module doesn't currently support
           redis.
@@ -28,19 +28,19 @@ in {
         default = pkgs.ethercalc;
         defaultText = literalExpression "pkgs.ethercalc";
         type = types.package;
-        description = "Ethercalc package to use.";
+        description = lib.mdDoc "Ethercalc package to use.";
       };
 
       host = mkOption {
         type = types.str;
         default = "0.0.0.0";
-        description = "Address to listen on (use 0.0.0.0 to allow access from any address).";
+        description = lib.mdDoc "Address to listen on (use 0.0.0.0 to allow access from any address).";
       };
 
       port = mkOption {
         type = types.port;
         default = 8000;
-        description = "Port to bind to.";
+        description = lib.mdDoc "Port to bind to.";
       };
     };
   };
diff --git a/nixpkgs/nixos/modules/services/web-apps/fluidd.nix b/nixpkgs/nixos/modules/services/web-apps/fluidd.nix
index 6ac1acc9d036..8d6d48b3dd27 100644
--- a/nixpkgs/nixos/modules/services/web-apps/fluidd.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/fluidd.nix
@@ -10,7 +10,7 @@ in
 
     package = mkOption {
       type = types.package;
-      description = "Fluidd package to be used in the module";
+      description = lib.mdDoc "Fluidd package to be used in the module";
       default = pkgs.fluidd;
       defaultText = literalExpression "pkgs.fluidd";
     };
@@ -18,7 +18,7 @@ in
     hostName = mkOption {
       type = types.str;
       default = "localhost";
-      description = "Hostname to serve fluidd on";
+      description = lib.mdDoc "Hostname to serve fluidd on";
     };
 
     nginx = mkOption {
@@ -30,7 +30,7 @@ in
           serverAliases = [ "fluidd.''${config.networking.domain}" ];
         }
       '';
-      description = "Extra configuration for the nginx virtual host of fluidd.";
+      description = lib.mdDoc "Extra configuration for the nginx virtual host of fluidd.";
     };
   };
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/galene.nix b/nixpkgs/nixos/modules/services/web-apps/galene.nix
index 1d0a620585b0..2fef43753d79 100644
--- a/nixpkgs/nixos/modules/services/web-apps/galene.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/galene.nix
@@ -17,7 +17,7 @@ in
       stateDir = mkOption {
         default = defaultstateDir;
         type = types.str;
-        description = ''
+        description = lib.mdDoc ''
           The directory where Galene stores its internal state. If left as the default
           value this directory will automatically be created before the Galene server
           starts, otherwise the sysadmin is responsible for ensuring the directory
@@ -28,19 +28,19 @@ in
       user = mkOption {
         type = types.str;
         default = "galene";
-        description = "User account under which galene runs.";
+        description = lib.mdDoc "User account under which galene runs.";
       };
 
       group = mkOption {
         type = types.str;
         default = "galene";
-        description = "Group under which galene runs.";
+        description = lib.mdDoc "Group under which galene runs.";
       };
 
       insecure = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Whether Galene should listen in http or in https. If left as the default
           value (false), Galene needs to be fed a private key and a certificate.
         '';
@@ -50,7 +50,7 @@ in
         type = types.nullOr types.str;
         default = null;
         example = "/path/to/your/cert.pem";
-        description = ''
+        description = lib.mdDoc ''
           Path to the server's certificate. The file is copied at runtime to
           Galene's data directory where it needs to reside.
         '';
@@ -60,7 +60,7 @@ in
         type = types.nullOr types.str;
         default = null;
         example = "/path/to/your/key.pem";
-        description = ''
+        description = lib.mdDoc ''
           Path to the server's private key. The file is copied at runtime to
           Galene's data directory where it needs to reside.
         '';
@@ -69,13 +69,13 @@ in
       httpAddress = mkOption {
         type = types.str;
         default = "";
-        description = "HTTP listen address for galene.";
+        description = lib.mdDoc "HTTP listen address for galene.";
       };
 
       httpPort = mkOption {
         type = types.port;
         default = 8443;
-        description = "HTTP listen port.";
+        description = lib.mdDoc "HTTP listen port.";
       };
 
       staticDir = mkOption {
@@ -83,7 +83,7 @@ in
         default = "${cfg.package.static}/static";
         defaultText = literalExpression ''"''${package.static}/static"'';
         example = "/var/lib/galene/static";
-        description = "Web server directory.";
+        description = lib.mdDoc "Web server directory.";
       };
 
       recordingsDir = mkOption {
@@ -91,7 +91,7 @@ in
         default = defaultrecordingsDir;
         defaultText = literalExpression ''"''${config.${opt.stateDir}}/recordings"'';
         example = "/var/lib/galene/recordings";
-        description = "Recordings directory.";
+        description = lib.mdDoc "Recordings directory.";
       };
 
       dataDir = mkOption {
@@ -99,7 +99,7 @@ in
         default = defaultdataDir;
         defaultText = literalExpression ''"''${config.${opt.stateDir}}/data"'';
         example = "/var/lib/galene/data";
-        description = "Data directory.";
+        description = lib.mdDoc "Data directory.";
       };
 
       groupsDir = mkOption {
@@ -107,14 +107,14 @@ in
         default = defaultgroupsDir;
         defaultText = literalExpression ''"''${config.${opt.stateDir}}/groups"'';
         example = "/var/lib/galene/groups";
-        description = "Web server directory.";
+        description = lib.mdDoc "Web server directory.";
       };
 
       package = mkOption {
         default = pkgs.galene;
         defaultText = literalExpression "pkgs.galene";
         type = types.package;
-        description = ''
+        description = lib.mdDoc ''
           Package for running Galene.
         '';
       };
@@ -164,6 +164,35 @@ in
             optional (cfg.dataDir == defaultdataDir) "galene/data" ++
             optional (cfg.groupsDir == defaultgroupsDir) "galene/groups" ++
             optional (cfg.recordingsDir == defaultrecordingsDir) "galene/recordings";
+
+          # Hardening
+          CapabilityBoundingSet = [ "" ];
+          DeviceAllow = [ "" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          ProtectSystem = "strict";
+          ReadWritePaths = cfg.recordingsDir;
+          RemoveIPC = true;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+          UMask = "0077";
         }
       ];
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/gerrit.nix b/nixpkgs/nixos/modules/services/web-apps/gerrit.nix
index 6bfc67368dd5..5b36204ff053 100644
--- a/nixpkgs/nixos/modules/services/web-apps/gerrit.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/gerrit.nix
@@ -65,14 +65,14 @@ in
         type = types.package;
         default = pkgs.gerrit;
         defaultText = literalExpression "pkgs.gerrit";
-        description = "Gerrit package to use";
+        description = lib.mdDoc "Gerrit package to use";
       };
 
       jvmPackage = mkOption {
         type = types.package;
         default = pkgs.jre_headless;
         defaultText = literalExpression "pkgs.jre_headless";
-        description = "Java Runtime Environment package to use";
+        description = lib.mdDoc "Java Runtime Environment package to use";
       };
 
       jvmOpts = mkOption {
@@ -81,13 +81,13 @@ in
           "-Dflogger.backend_factory=com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance"
           "-Dflogger.logging_context=com.google.gerrit.server.logging.LoggingContext#getInstance"
         ];
-        description = "A list of JVM options to start gerrit with.";
+        description = lib.mdDoc "A list of JVM options to start gerrit with.";
       };
 
       jvmHeapLimit = mkOption {
         type = types.str;
         default = "1024m";
-        description = ''
+        description = lib.mdDoc ''
           How much memory to allocate to the JVM heap
         '';
       };
@@ -95,8 +95,8 @@ in
       listenAddress = mkOption {
         type = types.str;
         default = "[::]:8080";
-        description = ''
-          <literal>hostname:port</literal> to listen for HTTP traffic.
+        description = lib.mdDoc ''
+          `hostname:port` to listen for HTTP traffic.
 
           This is bound using the systemd socket activation.
         '';
@@ -105,25 +105,25 @@ in
       settings = mkOption {
         type = gitIniType;
         default = {};
-        description = ''
+        description = lib.mdDoc ''
           Gerrit configuration. This will be generated to the
-          <literal>etc/gerrit.config</literal> file.
+          `etc/gerrit.config` file.
         '';
       };
 
       replicationSettings = mkOption {
         type = gitIniType;
         default = {};
-        description = ''
+        description = lib.mdDoc ''
           Replication configuration. This will be generated to the
-          <literal>etc/replication.config</literal> file.
+          `etc/replication.config` file.
         '';
       };
 
       plugins = mkOption {
         type = types.listOf types.package;
         default = [];
-        description = ''
+        description = lib.mdDoc ''
           List of plugins to add to Gerrit. Each derivation is a jar file
           itself where the name of the derivation is the name of plugin.
         '';
@@ -132,19 +132,19 @@ in
       builtinPlugins = mkOption {
         type = types.listOf (types.enum cfg.package.passthru.plugins);
         default = [];
-        description = ''
+        description = lib.mdDoc ''
           List of builtins plugins to install. Those are shipped in the
-          <literal>gerrit.war</literal> file.
+          `gerrit.war` file.
         '';
       };
 
       serverId = mkOption {
         type = types.str;
-        description = ''
+        description = lib.mdDoc ''
           Set a UUID that uniquely identifies the server.
 
           This can be generated with
-          <literal>nix-shell -p util-linux --run uuidgen</literal>.
+          `nix-shell -p util-linux --run uuidgen`.
         '';
       };
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/gotify-server.nix b/nixpkgs/nixos/modules/services/web-apps/gotify-server.nix
index 03e01f46a944..9e278b41ad15 100644
--- a/nixpkgs/nixos/modules/services/web-apps/gotify-server.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/gotify-server.nix
@@ -11,7 +11,7 @@ in {
 
       port = mkOption {
         type = types.port;
-        description = ''
+        description = lib.mdDoc ''
           Port the server listens to.
         '';
       };
@@ -19,8 +19,8 @@ in {
       stateDirectoryName = mkOption {
         type = types.str;
         default = "gotify-server";
-        description = ''
-          The name of the directory below <filename>/var/lib</filename> where
+        description = lib.mdDoc ''
+          The name of the directory below {file}`/var/lib` where
           gotify stores its runtime data.
         '';
       };
diff --git a/nixpkgs/nixos/modules/services/web-apps/grocy.nix b/nixpkgs/nixos/modules/services/web-apps/grocy.nix
index be2de638dd96..173dd63ddaa1 100644
--- a/nixpkgs/nixos/modules/services/web-apps/grocy.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/grocy.nix
@@ -10,7 +10,7 @@ in {
 
     hostName = mkOption {
       type = types.str;
-      description = ''
+      description = lib.mdDoc ''
         FQDN for the grocy instance.
       '';
     };
@@ -18,7 +18,7 @@ in {
     nginx.enableSSL = mkOption {
       type = types.bool;
       default = true;
-      description = ''
+      description = lib.mdDoc ''
         Whether or not to enable SSL (with ACME and let's encrypt)
         for the grocy vhost.
       '';
@@ -39,7 +39,7 @@ in {
         "pm.max_requests" = "500";
       };
 
-      description = ''
+      description = lib.mdDoc ''
         Options for grocy's PHPFPM pool.
       '';
     };
@@ -47,8 +47,8 @@ in {
     dataDir = mkOption {
       type = types.str;
       default = "/var/lib/grocy";
-      description = ''
-        Home directory of the <literal>grocy</literal> user which contains
+      description = lib.mdDoc ''
+        Home directory of the `grocy` user which contains
         the application's state.
       '';
     };
@@ -58,7 +58,7 @@ in {
         type = types.str;
         default = "USD";
         example = "EUR";
-        description = ''
+        description = lib.mdDoc ''
           ISO 4217 code for the currency to display.
         '';
       };
@@ -66,7 +66,7 @@ in {
       culture = mkOption {
         type = types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ];
         default = "en";
-        description = ''
+        description = lib.mdDoc ''
           Display language of the frontend.
         '';
       };
@@ -75,14 +75,14 @@ in {
         showWeekNumber = mkOption {
           default = true;
           type = types.bool;
-          description = ''
+          description = lib.mdDoc ''
             Show the number of the weeks in the calendar views.
           '';
         };
         firstDayOfWeek = mkOption {
           default = null;
           type = types.nullOr (types.enum (range 0 6));
-          description = ''
+          description = lib.mdDoc ''
             Which day of the week (0=Sunday, 1=Monday etc.) should be the
             first day.
           '';
@@ -115,9 +115,9 @@ in {
       user = "grocy";
       group = "nginx";
 
-      # PHP 7.4 is the only version which is supported/tested by upstream:
-      # https://github.com/grocy/grocy/blob/v3.0.0/README.md#how-to-install
-      phpPackage = pkgs.php74;
+      # PHP 8.0 is the only version which is supported/tested by upstream:
+      # https://github.com/grocy/grocy/blob/v3.3.0/README.md#how-to-install
+      phpPackage = pkgs.php80;
 
       inherit (cfg.phpfpm) settings;
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/healthchecks.nix b/nixpkgs/nixos/modules/services/web-apps/healthchecks.nix
new file mode 100644
index 000000000000..e58cc6f202be
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/web-apps/healthchecks.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, buildEnv, ... }:
+
+with lib;
+
+let
+  defaultUser = "healthchecks";
+  cfg = config.services.healthchecks;
+  pkg = cfg.package;
+  boolToPython = b: if b then "True" else "False";
+  environment = {
+    PYTHONPATH = pkg.pythonPath;
+    STATIC_ROOT = cfg.dataDir + "/static";
+    DB_NAME = "${cfg.dataDir}/healthchecks.sqlite";
+  } // cfg.settings;
+
+  environmentFile = pkgs.writeText "healthchecks-environment" (lib.generators.toKeyValue { } environment);
+
+  healthchecksManageScript = with pkgs; (writeShellScriptBin "healthchecks-manage" ''
+    if [[ "$USER" != "${cfg.user}" ]]; then
+        echo "please run as user 'healtchecks'." >/dev/stderr
+        exit 1
+    fi
+    export $(cat ${environmentFile} | xargs);
+    exec ${pkg}/opt/healthchecks/manage.py "$@"
+  '');
+in
+{
+  options.services.healthchecks = {
+    enable = mkEnableOption "healthchecks" // {
+      description = ''
+        Enable healthchecks.
+        It is expected to be run behind a HTTP reverse proxy.
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.healthchecks;
+      defaultText = literalExpression "pkgs.healthchecks";
+      type = types.package;
+      description = lib.mdDoc "healthchecks package to use.";
+    };
+
+    user = mkOption {
+      default = defaultUser;
+      type = types.str;
+      description = ''
+        User account under which healthchecks runs.
+
+        <note><para>
+        If left as the default value this user will automatically be created
+        on system activation, otherwise you are responsible for
+        ensuring the user exists before the healthchecks service starts.
+        </para></note>
+      '';
+    };
+
+    group = mkOption {
+      default = defaultUser;
+      type = types.str;
+      description = ''
+        Group account under which healthchecks runs.
+
+        <note><para>
+        If left as the default value this group will automatically be created
+        on system activation, otherwise you are responsible for
+        ensuring the group exists before the healthchecks service starts.
+        </para></note>
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = lib.mdDoc "Address the server will listen on.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8000;
+      description = lib.mdDoc "Port the server will listen on.";
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/healthchecks";
+      description = ''
+        The directory used to store all data for healthchecks.
+
+        <note><para>
+        If left as the default value this directory will automatically be created before
+        the healthchecks server starts, otherwise you are responsible for ensuring the
+        directory exists with appropriate ownership and permissions.
+        </para></note>
+      '';
+    };
+
+    settings = lib.mkOption {
+      description = ''
+        Environment variables which are read by healthchecks <literal>(local)_settings.py</literal>.
+
+        Settings which are explictly covered in options bewlow, are type-checked and/or transformed
+        before added to the environment, everything else is passed as a string.
+
+        See <link xlink:href="">https://healthchecks.io/docs/self_hosted_configuration/</link>
+        for a full documentation of settings.
+
+        We add two variables to this list inside the packages <literal>local_settings.py.</literal>
+        - STATIC_ROOT to set a state directory for dynamically generated static files.
+        - SECRET_KEY_FILE to read SECRET_KEY from a file at runtime and keep it out of /nix/store.
+      '';
+      type = types.submodule {
+        freeformType = types.attrsOf types.str;
+        options = {
+          ALLOWED_HOSTS = lib.mkOption {
+            type = types.listOf types.str;
+            default = [ "*" ];
+            description = lib.mdDoc "The host/domain names that this site can serve.";
+            apply = lib.concatStringsSep ",";
+          };
+
+          SECRET_KEY_FILE = mkOption {
+            type = types.path;
+            description = lib.mdDoc "Path to a file containing the secret key.";
+          };
+
+          DEBUG = mkOption {
+            type = types.bool;
+            default = false;
+            description = lib.mdDoc "Enable debug mode.";
+            apply = boolToPython;
+          };
+
+          REGISTRATION_OPEN = mkOption {
+            type = types.bool;
+            default = false;
+            description = lib.mdDoc ''
+              A boolean that controls whether site visitors can create new accounts.
+              Set it to false if you are setting up a private Healthchecks instance,
+              but it needs to be publicly accessible (so, for example, your cloud
+              services can send pings to it).
+              If you close new user registration, you can still selectively invite
+              users to your team account.
+            '';
+            apply = boolToPython;
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ healthchecksManageScript ];
+
+    systemd.targets.healthchecks = {
+      description = "Target for all Healthchecks services";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "network-online.target" ];
+    };
+
+    systemd.services =
+      let
+        commonConfig = {
+          WorkingDirectory = cfg.dataDir;
+          User = cfg.user;
+          Group = cfg.group;
+          EnvironmentFile = environmentFile;
+          StateDirectory = mkIf (cfg.dataDir == "/var/lib/healthchecks") "healthchecks";
+          StateDirectoryMode = mkIf (cfg.dataDir == "/var/lib/healthchecks") "0750";
+        };
+      in
+        {
+        healthchecks-migration = {
+          description = "Healthchecks migrations";
+          wantedBy = [ "healthchecks.target" ];
+
+          serviceConfig = commonConfig // {
+            Restart = "on-failure";
+            Type = "oneshot";
+            ExecStart = ''
+              ${pkg}/opt/healthchecks/manage.py migrate
+            '';
+          };
+        };
+
+        healthchecks = {
+          description = "Healthchecks WSGI Service";
+          wantedBy = [ "healthchecks.target" ];
+          after = [ "healthchecks-migration.service" ];
+
+          preStart = ''
+            ${pkg}/opt/healthchecks/manage.py collectstatic --no-input
+            ${pkg}/opt/healthchecks/manage.py remove_stale_contenttypes --no-input
+            ${pkg}/opt/healthchecks/manage.py compress
+          '';
+
+          serviceConfig = commonConfig // {
+            Restart = "always";
+            ExecStart = ''
+              ${pkgs.python3Packages.gunicorn}/bin/gunicorn hc.wsgi \
+                --bind ${cfg.listenAddress}:${toString cfg.port} \
+                --pythonpath ${pkg}/opt/healthchecks
+            '';
+          };
+        };
+
+        healthchecks-sendalerts = {
+          description = "Healthchecks Alert Service";
+          wantedBy = [ "healthchecks.target" ];
+          after = [ "healthchecks.service" ];
+
+          serviceConfig = commonConfig // {
+            Restart = "always";
+            ExecStart = ''
+              ${pkg}/opt/healthchecks/manage.py sendalerts
+            '';
+          };
+        };
+
+        healthchecks-sendreports = {
+          description = "Healthchecks Reporting Service";
+          wantedBy = [ "healthchecks.target" ];
+          after = [ "healthchecks.service" ];
+
+          serviceConfig = commonConfig // {
+            Restart = "always";
+            ExecStart = ''
+              ${pkg}/opt/healthchecks/manage.py sendreports --loop
+            '';
+          };
+        };
+      };
+
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} =
+        {
+          description = "healthchecks service owner";
+          isSystemUser = true;
+          group = defaultUser;
+        };
+    };
+
+    users.groups = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} =
+        {
+          members = [ defaultUser ];
+        };
+    };
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/web-apps/hedgedoc.nix b/nixpkgs/nixos/modules/services/web-apps/hedgedoc.nix
index 9eeabb9d5662..348192ea8486 100644
--- a/nixpkgs/nixos/modules/services/web-apps/hedgedoc.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/hedgedoc.nix
@@ -13,17 +13,22 @@ let
     then "hedgedoc"
     else "codimd";
 
+  settingsFormat = pkgs.formats.json {};
+
   prettyJSON = conf:
     pkgs.runCommandLocal "hedgedoc-config.json" {
       nativeBuildInputs = [ pkgs.jq ];
     } ''
-      echo '${builtins.toJSON conf}' | jq \
-        '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out
+      jq '{production:del(.[]|nulls)|del(.[][]?|nulls)}' \
+        < ${settingsFormat.generate "hedgedoc-ugly.json" cfg.settings} \
+        > $out
     '';
 in
 {
   imports = [
     (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
+    (mkRenamedOptionModule
+      [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ])
   ];
 
   options.services.hedgedoc = {
@@ -32,7 +37,7 @@ in
     groups = mkOption {
       type = types.listOf types.str;
       default = [];
-      description = ''
+      description = lib.mdDoc ''
         Groups to which the service user should be added.
       '';
     };
@@ -40,18 +45,18 @@ in
     workDir = mkOption {
       type = types.path;
       default = "/var/lib/${name}";
-      description = ''
+      description = lib.mdDoc ''
         Working directory for the HedgeDoc service.
       '';
     };
 
-    configuration = {
+    settings = let options = {
       debug = mkEnableOption "debug mode";
       domain = mkOption {
         type = types.nullOr types.str;
         default = null;
         example = "hedgedoc.org";
-        description = ''
+        description = lib.mdDoc ''
           Domain name for the HedgeDoc instance.
         '';
       };
@@ -59,14 +64,14 @@ in
         type = types.nullOr types.str;
         default = null;
         example = "/url/path/to/hedgedoc";
-        description = ''
+        description = lib.mdDoc ''
           Path under which HedgeDoc is accessible.
         '';
       };
       host = mkOption {
         type = types.str;
         default = "localhost";
-        description = ''
+        description = lib.mdDoc ''
           Address to listen on.
         '';
       };
@@ -74,7 +79,7 @@ in
         type = types.int;
         default = 3000;
         example = 80;
-        description = ''
+        description = lib.mdDoc ''
           Port to listen on.
         '';
       };
@@ -82,7 +87,7 @@ in
         type = types.nullOr types.str;
         default = null;
         example = "/run/hedgedoc.sock";
-        description = ''
+        description = lib.mdDoc ''
           Specify where a UNIX domain socket should be placed.
         '';
       };
@@ -90,44 +95,44 @@ in
         type = types.listOf types.str;
         default = [];
         example = [ "localhost" "hedgedoc.org" ];
-        description = ''
+        description = lib.mdDoc ''
           List of domains to whitelist.
         '';
       };
       useSSL = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Enable to use SSL server. This will also enable
-          <option>protocolUseSSL</option>.
+          {option}`protocolUseSSL`.
         '';
       };
       hsts = {
         enable = mkOption {
           type = types.bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             Whether to enable HSTS if HTTPS is also enabled.
           '';
         };
         maxAgeSeconds = mkOption {
           type = types.int;
           default = 31536000;
-          description = ''
+          description = lib.mdDoc ''
             Max duration for clients to keep the HSTS status.
           '';
         };
         includeSubdomains = mkOption {
           type = types.bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             Whether to include subdomains in HSTS.
           '';
         };
         preload = mkOption {
           type = types.bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             Whether to allow preloading of the site's HSTS status.
           '';
         };
@@ -145,40 +150,39 @@ in
             addDefaults = true;
           }
         '';
-        description = ''
+        description = lib.mdDoc ''
           Specify the Content Security Policy which is passed to Helmet.
-          For configuration details see <link xlink:href="https://helmetjs.github.io/docs/csp/"
-          >https://helmetjs.github.io/docs/csp/</link>.
+          For configuration details see <https://helmetjs.github.io/docs/csp/>.
         '';
       };
       protocolUseSSL = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Enable to use TLS for resource paths.
-          This only applies when <option>domain</option> is set.
+          This only applies when {option}`domain` is set.
         '';
       };
       urlAddPort = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Enable to add the port to callback URLs.
-          This only applies when <option>domain</option> is set
+          This only applies when {option}`domain` is set
           and only for ports other than 80 and 443.
         '';
       };
       useCDN = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Whether to use CDN resources or not.
         '';
       };
       allowAnonymous = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to allow anonymous usage.
         '';
       };
@@ -193,14 +197,21 @@ in
       allowFreeURL = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Whether to allow note creation by accessing a nonexistent note URL.
         '';
       };
+      requireFreeURLAuthentication = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to require authentication for FreeURL mode style note creation.
+        '';
+      };
       defaultPermission = mkOption {
         type = types.enum [ "freely" "editable" "limited" "locked" "private" ];
         default = "editable";
-        description = ''
+        description = lib.mdDoc ''
           Default permissions for notes.
           This only applies for signed-in users.
         '';
@@ -211,12 +222,12 @@ in
         example = ''
           postgres://user:pass@host:5432/dbname
         '';
-        description = ''
+        description = lib.mdDoc ''
           Specify which database to use.
           HedgeDoc supports mysql, postgres, sqlite and mssql.
-          See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
-          https://sequelize.readthedocs.io/en/v3/</link> for more information.
-          Note: This option overrides <option>db</option>.
+          See [
+          https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information.
+          Note: This option overrides {option}`db`.
         '';
       };
       db = mkOption {
@@ -228,52 +239,52 @@ in
             storage = "/var/lib/${name}/db.${name}.sqlite";
           }
         '';
-        description = ''
+        description = lib.mdDoc ''
           Specify the configuration for sequelize.
           HedgeDoc supports mysql, postgres, sqlite and mssql.
-          See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
-          https://sequelize.readthedocs.io/en/v3/</link> for more information.
-          Note: This option overrides <option>db</option>.
+          See [
+          https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information.
+          Note: This option overrides {option}`db`.
         '';
       };
       sslKeyPath= mkOption {
         type = types.nullOr types.str;
         default = null;
         example = "/var/lib/hedgedoc/hedgedoc.key";
-        description = ''
-          Path to the SSL key. Needed when <option>useSSL</option> is enabled.
+        description = lib.mdDoc ''
+          Path to the SSL key. Needed when {option}`useSSL` is enabled.
         '';
       };
       sslCertPath = mkOption {
         type = types.nullOr types.str;
         default = null;
         example = "/var/lib/hedgedoc/hedgedoc.crt";
-        description = ''
-          Path to the SSL cert. Needed when <option>useSSL</option> is enabled.
+        description = lib.mdDoc ''
+          Path to the SSL cert. Needed when {option}`useSSL` is enabled.
         '';
       };
       sslCAPath = mkOption {
         type = types.listOf types.str;
         default = [];
         example = [ "/var/lib/hedgedoc/ca.crt" ];
-        description = ''
-          SSL ca chain. Needed when <option>useSSL</option> is enabled.
+        description = lib.mdDoc ''
+          SSL ca chain. Needed when {option}`useSSL` is enabled.
         '';
       };
       dhParamPath = mkOption {
         type = types.nullOr types.str;
         default = null;
         example = "/var/lib/hedgedoc/dhparam.pem";
-        description = ''
-          Path to the SSL dh params. Needed when <option>useSSL</option> is enabled.
+        description = lib.mdDoc ''
+          Path to the SSL dh params. Needed when {option}`useSSL` is enabled.
         '';
       };
       tmpPath = mkOption {
         type = types.str;
         default = "/tmp";
-        description = ''
+        description = lib.mdDoc ''
           Path to the temp directory HedgeDoc should use.
-          Note that <option>serviceConfig.PrivateTmp</option> is enabled for
+          Note that {option}`serviceConfig.PrivateTmp` is enabled for
           the HedgeDoc systemd service by default.
           (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
@@ -281,7 +292,7 @@ in
       defaultNotePath = mkOption {
         type = types.nullOr types.str;
         default = "./public/default.md";
-        description = ''
+        description = lib.mdDoc ''
           Path to the default Note file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
@@ -289,7 +300,7 @@ in
       docsPath = mkOption {
         type = types.nullOr types.str;
         default = "./public/docs";
-        description = ''
+        description = lib.mdDoc ''
           Path to the docs directory.
           (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
@@ -297,7 +308,7 @@ in
       indexPath = mkOption {
         type = types.nullOr types.str;
         default = "./public/views/index.ejs";
-        description = ''
+        description = lib.mdDoc ''
           Path to the index template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
@@ -305,7 +316,7 @@ in
       hackmdPath = mkOption {
         type = types.nullOr types.str;
         default = "./public/views/hackmd.ejs";
-        description = ''
+        description = lib.mdDoc ''
           Path to the hackmd template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
@@ -314,7 +325,7 @@ in
         type = types.nullOr types.str;
         default = null;
         defaultText = literalExpression "./public/views/error.ejs";
-        description = ''
+        description = lib.mdDoc ''
           Path to the error template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
@@ -323,7 +334,7 @@ in
         type = types.nullOr types.str;
         default = null;
         defaultText = literalExpression "./public/views/pretty.ejs";
-        description = ''
+        description = lib.mdDoc ''
           Path to the pretty template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
@@ -332,7 +343,7 @@ in
         type = types.nullOr types.str;
         default = null;
         defaultText = literalExpression "./public/views/slide.hbs";
-        description = ''
+        description = lib.mdDoc ''
           Path to the slide template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
@@ -341,21 +352,21 @@ in
         type = types.str;
         default = "${cfg.workDir}/uploads";
         defaultText = literalExpression "/var/lib/${name}/uploads";
-        description = ''
+        description = lib.mdDoc ''
           Path under which uploaded files are saved.
         '';
       };
       sessionName = mkOption {
         type = types.str;
         default = "connect.sid";
-        description = ''
+        description = lib.mdDoc ''
           Specify the name of the session cookie.
         '';
       };
       sessionSecret = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = ''
+        description = lib.mdDoc ''
           Specify the secret used to sign the session cookie.
           If unset, one will be generated on startup.
         '';
@@ -363,56 +374,56 @@ in
       sessionLife = mkOption {
         type = types.int;
         default = 1209600000;
-        description = ''
+        description = lib.mdDoc ''
           Session life time in milliseconds.
         '';
       };
       heartbeatInterval = mkOption {
         type = types.int;
         default = 5000;
-        description = ''
+        description = lib.mdDoc ''
           Specify the socket.io heartbeat interval.
         '';
       };
       heartbeatTimeout = mkOption {
         type = types.int;
         default = 10000;
-        description = ''
+        description = lib.mdDoc ''
           Specify the socket.io heartbeat timeout.
         '';
       };
       documentMaxLength = mkOption {
         type = types.int;
         default = 100000;
-        description = ''
+        description = lib.mdDoc ''
           Specify the maximum document length.
         '';
       };
       email = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to enable email sign-in.
         '';
       };
       allowEmailRegister = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to enable email registration.
         '';
       };
       allowGravatar = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to use gravatar as profile picture source.
         '';
       };
       imageUploadType = mkOption {
         type = types.enum [ "imgur" "s3" "minio" "filesystem" ];
         default = "filesystem";
-        description = ''
+        description = lib.mdDoc ''
           Specify where to upload images.
         '';
       };
@@ -421,85 +432,85 @@ in
           options = {
             accessKey = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Minio access key.
               '';
             };
             secretKey = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Minio secret key.
               '';
             };
-            endpoint = mkOption {
+            endPoint = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Minio endpoint.
               '';
             };
             port = mkOption {
               type = types.int;
               default = 9000;
-              description = ''
+              description = lib.mdDoc ''
                 Minio listen port.
               '';
             };
             secure = mkOption {
               type = types.bool;
               default = true;
-              description = ''
+              description = lib.mdDoc ''
                 Whether to use HTTPS for Minio.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the minio third-party integration.";
+        description = lib.mdDoc "Configure the minio third-party integration.";
       };
       s3 = mkOption {
         type = types.nullOr (types.submodule {
           options = {
             accessKeyId = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 AWS access key id.
               '';
             };
             secretAccessKey = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 AWS access key.
               '';
             };
             region = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 AWS S3 region.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the s3 third-party integration.";
+        description = lib.mdDoc "Configure the s3 third-party integration.";
       };
       s3bucket = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = ''
-          Specify the bucket name for upload types <literal>s3</literal> and <literal>minio</literal>.
+        description = lib.mdDoc ''
+          Specify the bucket name for upload types `s3` and `minio`.
         '';
       };
       allowPDFExport = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to enable PDF exports.
         '';
       };
       imgur.clientId = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = ''
+        description = lib.mdDoc ''
           Imgur API client ID.
         '';
       };
@@ -508,13 +519,13 @@ in
           options = {
             connectionString = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Azure Blob Storage connection string.
               '';
             };
             container = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Azure Blob Storage container name.
                 It will be created if non-existent.
               '';
@@ -522,162 +533,162 @@ in
           };
         });
         default = null;
-        description = "Configure the azure third-party integration.";
+        description = lib.mdDoc "Configure the azure third-party integration.";
       };
       oauth2 = mkOption {
         type = types.nullOr (types.submodule {
           options = {
             authorizationURL = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the OAuth authorization URL.
               '';
             };
             tokenURL = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the OAuth token URL.
               '';
             };
             baseURL = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the OAuth base URL.
               '';
             };
             userProfileURL = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the OAuth userprofile URL.
               '';
             };
             userProfileUsernameAttr = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the name of the attribute for the username from the claim.
               '';
             };
             userProfileDisplayNameAttr = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the name of the attribute for the display name from the claim.
               '';
             };
             userProfileEmailAttr = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the name of the attribute for the email from the claim.
               '';
             };
             scope = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the OAuth scope.
               '';
             };
             providerName = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the name to be displayed for this strategy.
               '';
             };
             rolesClaim = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the role claim name.
               '';
             };
             accessRole = mkOption {
               type = with types; nullOr str;
               default = null;
-              description = ''
+              description = lib.mdDoc ''
                 Specify role which should be included in the ID token roles claim to grant access
               '';
             };
             clientID = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the OAuth client ID.
               '';
             };
             clientSecret = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Specify the OAuth client secret.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the OAuth integration.";
+        description = lib.mdDoc "Configure the OAuth integration.";
       };
       facebook = mkOption {
         type = types.nullOr (types.submodule {
           options = {
             clientID = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Facebook API client ID.
               '';
             };
             clientSecret = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Facebook API client secret.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the facebook third-party integration";
+        description = lib.mdDoc "Configure the facebook third-party integration";
       };
       twitter = mkOption {
         type = types.nullOr (types.submodule {
           options = {
             consumerKey = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Twitter API consumer key.
               '';
             };
             consumerSecret = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Twitter API consumer secret.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the Twitter third-party integration.";
+        description = lib.mdDoc "Configure the Twitter third-party integration.";
       };
       github = mkOption {
         type = types.nullOr (types.submodule {
           options = {
             clientID = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 GitHub API client ID.
               '';
             };
             clientSecret = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Github API client secret.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the GitHub third-party integration.";
+        description = lib.mdDoc "Configure the GitHub third-party integration.";
       };
       gitlab = mkOption {
         type = types.nullOr (types.submodule {
@@ -685,27 +696,27 @@ in
             baseURL = mkOption {
               type = types.str;
               default = "";
-              description = ''
+              description = lib.mdDoc ''
                 GitLab API authentication endpoint.
                 Only needed for other endpoints than gitlab.com.
               '';
             };
             clientID = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 GitLab API client ID.
               '';
             };
             clientSecret = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 GitLab API client secret.
               '';
             };
             scope = mkOption {
               type = types.enum [ "api" "read_user" ];
               default = "api";
-              description = ''
+              description = lib.mdDoc ''
                 GitLab API requested scope.
                 GitLab snippet import/export requires api scope.
               '';
@@ -713,79 +724,79 @@ in
           };
         });
         default = null;
-        description = "Configure the GitLab third-party integration.";
+        description = lib.mdDoc "Configure the GitLab third-party integration.";
       };
       mattermost = mkOption {
         type = types.nullOr (types.submodule {
           options = {
             baseURL = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Mattermost authentication endpoint.
               '';
             };
             clientID = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Mattermost API client ID.
               '';
             };
             clientSecret = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Mattermost API client secret.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the Mattermost third-party integration.";
+        description = lib.mdDoc "Configure the Mattermost third-party integration.";
       };
       dropbox = mkOption {
         type = types.nullOr (types.submodule {
           options = {
             clientID = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Dropbox API client ID.
               '';
             };
             clientSecret = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Dropbox API client secret.
               '';
             };
             appKey = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Dropbox app key.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the Dropbox third-party integration.";
+        description = lib.mdDoc "Configure the Dropbox third-party integration.";
       };
       google = mkOption {
         type = types.nullOr (types.submodule {
           options = {
             clientID = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Google API client ID.
               '';
             };
             clientSecret = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Google API client secret.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the Google third-party integration.";
+        description = lib.mdDoc "Configure the Google third-party integration.";
       };
       ldap = mkOption {
         type = types.nullOr (types.submodule {
@@ -793,76 +804,78 @@ in
             providerName = mkOption {
               type = types.str;
               default = "";
-              description = ''
+              description = lib.mdDoc ''
                 Optional name to be displayed at login form, indicating the LDAP provider.
               '';
             };
             url = mkOption {
               type = types.str;
               example = "ldap://localhost";
-              description = ''
+              description = lib.mdDoc ''
                 URL of LDAP server.
               '';
             };
             bindDn = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Bind DN for LDAP access.
               '';
             };
             bindCredentials = mkOption {
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Bind credentials for LDAP access.
               '';
             };
             searchBase = mkOption {
               type = types.str;
               example = "o=users,dc=example,dc=com";
-              description = ''
+              description = lib.mdDoc ''
                 LDAP directory to begin search from.
               '';
             };
             searchFilter = mkOption {
               type = types.str;
               example = "(uid={{username}})";
-              description = ''
+              description = lib.mdDoc ''
                 LDAP filter to search with.
               '';
             };
             searchAttributes = mkOption {
-              type = types.listOf types.str;
+              type = types.nullOr (types.listOf types.str);
+              default = null;
               example = [ "displayName" "mail" ];
-              description = ''
+              description = lib.mdDoc ''
                 LDAP attributes to search with.
               '';
             };
             userNameField = mkOption {
               type = types.str;
               default = "";
-              description = ''
+              description = lib.mdDoc ''
                 LDAP field which is used as the username on HedgeDoc.
-                By default <option>useridField</option> is used.
+                By default {option}`useridField` is used.
               '';
             };
             useridField = mkOption {
               type = types.str;
               example = "uid";
-              description = ''
+              description = lib.mdDoc ''
                 LDAP field which is a unique identifier for users on HedgeDoc.
               '';
             };
             tlsca = mkOption {
               type = types.str;
+              default = "/etc/ssl/certs/ca-certificates.crt";
               example = "server-cert.pem,root.pem";
-              description = ''
+              description = lib.mdDoc ''
                 Root CA for LDAP TLS in PEM format.
               '';
             };
           };
         });
         default = null;
-        description = "Configure the LDAP integration.";
+        description = lib.mdDoc "Configure the LDAP integration.";
       };
       saml = mkOption {
         type = types.nullOr (types.submodule {
@@ -870,21 +883,21 @@ in
             idpSsoUrl = mkOption {
               type = types.str;
               example = "https://idp.example.com/sso";
-              description = ''
+              description = lib.mdDoc ''
                 IdP authentication endpoint.
               '';
             };
             idpCert = mkOption {
               type = types.path;
               example = "/path/to/cert.pem";
-              description = ''
+              description = lib.mdDoc ''
                 Path to IdP certificate file in PEM format.
               '';
             };
             issuer = mkOption {
               type = types.str;
               default = "";
-              description = ''
+              description = lib.mdDoc ''
                 Optional identity of the service provider.
                 This defaults to the server URL.
               '';
@@ -892,7 +905,7 @@ in
             identifierFormat = mkOption {
               type = types.str;
               default = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
-              description = ''
+              description = lib.mdDoc ''
                 Optional name identifier format.
               '';
             };
@@ -900,7 +913,7 @@ in
               type = types.str;
               default = "";
               example = "memberOf";
-              description = ''
+              description = lib.mdDoc ''
                 Optional attribute name for group list.
               '';
             };
@@ -908,7 +921,7 @@ in
               type = types.listOf types.str;
               default = [];
               example = [ "Temporary-staff" "External-users" ];
-              description = ''
+              description = lib.mdDoc ''
                 Excluded group names.
               '';
             };
@@ -916,7 +929,7 @@ in
               type = types.listOf types.str;
               default = [];
               example = [ "Hedgedoc-Users" ];
-              description = ''
+              description = lib.mdDoc ''
                 Required group names.
               '';
             };
@@ -951,8 +964,18 @@ in
           };
         });
         default = null;
-        description = "Configure the SAML integration.";
+        description = lib.mdDoc "Configure the SAML integration.";
+      };
+    }; in lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        inherit options;
       };
+      description = lib.mdDoc ''
+        HedgeDoc configuration, see
+        <https://docs.hedgedoc.org/configuration/>
+        for documentation.
+      '';
     };
 
     environmentFile = mkOption {
@@ -960,9 +983,7 @@ in
       default = null;
       example = "/var/lib/hedgedoc/hedgedoc.env";
       description = ''
-        Environment file as defined in <citerefentry>
-        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
-        </citerefentry>.
+        Environment file as defined in <citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
 
         Secrets may be passed to the service without adding them to the world-readable
         Nix store, by specifying placeholder variables as the option value in Nix and
@@ -989,16 +1010,17 @@ in
       type = types.package;
       default = pkgs.hedgedoc;
       defaultText = literalExpression "pkgs.hedgedoc";
-      description = ''
+      description = lib.mdDoc ''
         Package that provides HedgeDoc.
       '';
     };
+
   };
 
   config = mkIf cfg.enable {
     assertions = [
-      { assertion = cfg.configuration.db == {} -> (
-          cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null
+      { assertion = cfg.settings.db == {} -> (
+          cfg.settings.dbURL != "" && cfg.settings.dbURL != null
         );
         message = "Database configuration for HedgeDoc missing."; }
     ];
@@ -1019,10 +1041,12 @@ in
       preStart = ''
         ${pkgs.envsubst}/bin/envsubst \
           -o ${cfg.workDir}/config.json \
-          -i ${prettyJSON cfg.configuration}
+          -i ${prettyJSON cfg.settings}
+        mkdir -p ${cfg.settings.uploadsPath}
       '';
       serviceConfig = {
         WorkingDirectory = cfg.workDir;
+        StateDirectory = [ cfg.workDir cfg.settings.uploadsPath ];
         ExecStart = "${cfg.package}/bin/hedgedoc";
         EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
         Environment = [
diff --git a/nixpkgs/nixos/modules/services/web-apps/hledger-web.nix b/nixpkgs/nixos/modules/services/web-apps/hledger-web.nix
index 4f6a34e6d2fe..4f02a637cdd8 100644
--- a/nixpkgs/nixos/modules/services/web-apps/hledger-web.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/hledger-web.nix
@@ -12,7 +12,7 @@ in {
     host = mkOption {
       type = types.str;
       default = "127.0.0.1";
-      description = ''
+      description = lib.mdDoc ''
         Address to listen on.
       '';
     };
@@ -21,7 +21,7 @@ in {
       type = types.port;
       default = 5000;
       example = 80;
-      description = ''
+      description = lib.mdDoc ''
         Port to listen on.
       '';
     };
@@ -30,21 +30,21 @@ in {
       view = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Enable the view capability.
         '';
       };
       add = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Enable the add capability.
         '';
       };
       manage = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Enable the manage capability.
         '';
       };
@@ -53,7 +53,7 @@ in {
     stateDir = mkOption {
       type = types.path;
       default = "/var/lib/hledger-web";
-      description = ''
+      description = lib.mdDoc ''
         Path the service has access to. If left as the default value this
         directory will automatically be created before the hledger-web server
         starts, otherwise the sysadmin is responsible for ensuring the
@@ -64,8 +64,8 @@ in {
     journalFiles = mkOption {
       type = types.listOf types.str;
       default = [ ".hledger.journal" ];
-      description = ''
-        Paths to journal files relative to <option>services.hledger-web.stateDir</option>.
+      description = lib.mdDoc ''
+        Paths to journal files relative to {option}`services.hledger-web.stateDir`.
       '';
     };
 
@@ -73,7 +73,7 @@ in {
       type = with types; nullOr str;
       default = null;
       example = "https://example.org";
-      description = ''
+      description = lib.mdDoc ''
         Base URL, when sharing over a network.
       '';
     };
@@ -82,7 +82,7 @@ in {
       type = types.listOf types.str;
       default = [];
       example = [ "--forecast" ];
-      description = ''
+      description = lib.mdDoc ''
         Extra command line arguments to pass to hledger-web.
       '';
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix b/nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
index b9761061aaae..b96baaec7678 100644
--- a/nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
@@ -17,7 +17,7 @@ in {
     pool = mkOption {
       type = str;
       default = poolName;
-      description = ''
+      description = lib.mdDoc ''
          Name of existing PHP-FPM pool that is used to run Icingaweb2.
          If not specified, a pool will automatically created with default values.
       '';
@@ -26,7 +26,7 @@ in {
     libraryPaths = mkOption {
       type = attrsOf package;
       default = { };
-      description = ''
+      description = lib.mdDoc ''
         Libraries to add to the Icingaweb2 library path.
         The name of the attribute is the name of the library, the value
         is the package to add.
@@ -36,7 +36,7 @@ in {
     virtualHost = mkOption {
       type = nullOr str;
       default = "icingaweb2";
-      description = ''
+      description = lib.mdDoc ''
         Name of the nginx virtualhost to use and setup. If null, no virtualhost is set up.
       '';
     };
@@ -45,7 +45,7 @@ in {
       type = str;
       default = "UTC";
       example = "Europe/Berlin";
-      description = "PHP-compliant timezone specification";
+      description = lib.mdDoc "PHP-compliant timezone specification";
     };
 
     modules = {
@@ -64,7 +64,7 @@ in {
           "snow" = icingaweb2Modules.theme-snow;
         }
       '';
-      description = ''
+      description = lib.mdDoc ''
         Name-package attrset of Icingaweb 2 modules packages to enable.
 
         If you enable modules manually (e.g. via the web ui), they will not be touched.
@@ -84,7 +84,7 @@ in {
           level = "CRITICAL";
         };
       };
-      description = ''
+      description = lib.mdDoc ''
         config.ini contents.
         Will automatically be converted to a .ini file.
         If you don't set global.module_path, the module will take care of it.
@@ -108,7 +108,7 @@ in {
           dbname = "icingaweb2";
         };
       };
-      description = ''
+      description = lib.mdDoc ''
         resources.ini contents.
         Will automatically be converted to a .ini file.
 
@@ -127,7 +127,7 @@ in {
           resource = "icingaweb_db";
         };
       };
-      description = ''
+      description = lib.mdDoc ''
         authentication.ini contents.
         Will automatically be converted to a .ini file.
 
@@ -145,7 +145,7 @@ in {
           resource = "icingaweb_db";
         };
       };
-      description = ''
+      description = lib.mdDoc ''
         groups.ini contents.
         Will automatically be converted to a .ini file.
 
@@ -163,7 +163,7 @@ in {
           permissions = "*";
         };
       };
-      description = ''
+      description = lib.mdDoc ''
         roles.ini contents.
         Will automatically be converted to a .ini file.
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix b/nixpkgs/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
index e9c1d4ffe5ea..0579c602216d 100644
--- a/nixpkgs/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
@@ -34,32 +34,32 @@ in {
     enable = mkOption {
       type = bool;
       default = true;
-      description = "Whether to enable the icingaweb2 monitoring module.";
+      description = lib.mdDoc "Whether to enable the icingaweb2 monitoring module.";
     };
 
     generalConfig = {
       mutable = mkOption {
         type = bool;
         default = false;
-        description = "Make config.ini of the monitoring module mutable (e.g. via the web interface).";
+        description = lib.mdDoc "Make config.ini of the monitoring module mutable (e.g. via the web interface).";
       };
 
       protectedVars = mkOption {
         type = listOf str;
         default = [ "*pw*" "*pass*" "community" ];
-        description = "List of string patterns for custom variables which should be excluded from user’s view.";
+        description = lib.mdDoc "List of string patterns for custom variables which should be excluded from user’s view.";
       };
     };
 
     mutableBackends = mkOption {
       type = bool;
       default = false;
-      description = "Make backends.ini of the monitoring module mutable (e.g. via the web interface).";
+      description = lib.mdDoc "Make backends.ini of the monitoring module mutable (e.g. via the web interface).";
     };
 
     backends = mkOption {
       default = { icinga = { resource = "icinga_ido"; }; };
-      description = "Monitoring backends to define";
+      description = lib.mdDoc "Monitoring backends to define";
       type = attrsOf (submodule ({ name, ... }: {
         options = {
           name = mkOption {
@@ -71,13 +71,13 @@ in {
 
           resource = mkOption {
             type = str;
-            description = "Name of the IDO resource";
+            description = lib.mdDoc "Name of the IDO resource";
           };
 
           disabled = mkOption {
             type = bool;
             default = false;
-            description = "Disable this backend";
+            description = lib.mdDoc "Disable this backend";
           };
         };
       }));
@@ -86,12 +86,12 @@ in {
     mutableTransports = mkOption {
       type = bool;
       default = true;
-      description = "Make commandtransports.ini of the monitoring module mutable (e.g. via the web interface).";
+      description = lib.mdDoc "Make commandtransports.ini of the monitoring module mutable (e.g. via the web interface).";
     };
 
     transports = mkOption {
       default = {};
-      description = "Command transports to define";
+      description = lib.mdDoc "Command transports to define";
       type = attrsOf (submodule ({ name, ... }: {
         options = {
           name = mkOption {
@@ -104,44 +104,44 @@ in {
           type = mkOption {
             type = enum [ "api" "local" "remote" ];
             default = "api";
-            description = "Type of  this transport";
+            description = lib.mdDoc "Type of  this transport";
           };
 
           instance = mkOption {
             type = nullOr str;
             default = null;
-            description = "Assign a icinga instance to this transport";
+            description = lib.mdDoc "Assign a icinga instance to this transport";
           };
 
           path = mkOption {
             type = str;
-            description = "Path to the socket for local or remote transports";
+            description = lib.mdDoc "Path to the socket for local or remote transports";
           };
 
           host = mkOption {
             type = str;
-            description = "Host for the api or remote transport";
+            description = lib.mdDoc "Host for the api or remote transport";
           };
 
           port = mkOption {
             type = nullOr str;
             default = null;
-            description = "Port to connect to for the api or remote transport";
+            description = lib.mdDoc "Port to connect to for the api or remote transport";
           };
 
           username = mkOption {
             type = str;
-            description = "Username for the api or remote transport";
+            description = lib.mdDoc "Username for the api or remote transport";
           };
 
           password = mkOption {
             type = str;
-            description = "Password for the api transport";
+            description = lib.mdDoc "Password for the api transport";
           };
 
           resource = mkOption {
             type = str;
-            description = "SSH identity resource for the remote transport";
+            description = lib.mdDoc "SSH identity resource for the remote transport";
           };
         };
       }));
diff --git a/nixpkgs/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixpkgs/nixos/modules/services/web-apps/ihatemoney/default.nix
index ad314c885ba8..c771f0afa231 100644
--- a/nixpkgs/nixos/modules/services/web-apps/ihatemoney/default.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/ihatemoney/default.nix
@@ -51,42 +51,42 @@ in
       backend = mkOption {
         type = types.enum [ "sqlite" "postgresql" ];
         default = "sqlite";
-        description = ''
+        description = lib.mdDoc ''
           The database engine to use for ihatemoney.
-          If <literal>postgresql</literal> is selected, then a database called
-          <literal>${db}</literal> will be created. If you disable this option,
+          If `postgresql` is selected, then a database called
+          `${db}` will be created. If you disable this option,
           it will however not be removed.
         '';
       };
       adminHashedPassword = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "The hashed password of the administrator. To obtain it, run <literal>ihatemoney generate_password_hash</literal>";
+        description = lib.mdDoc "The hashed password of the administrator. To obtain it, run `ihatemoney generate_password_hash`";
       };
       uwsgiConfig = mkOption {
         type = types.attrs;
         example = {
           http = ":8000";
         };
-        description = "Additionnal configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
+        description = lib.mdDoc "Additionnal configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
       };
       defaultSender = {
         name = mkOption {
           type = types.str;
           default = "Budget manager";
-          description = "The display name of the sender of ihatemoney emails";
+          description = lib.mdDoc "The display name of the sender of ihatemoney emails";
         };
         email = mkOption {
           type = types.str;
           default = "ihatemoney@${config.networking.hostName}";
           defaultText = literalExpression ''"ihatemoney@''${config.networking.hostName}"'';
-          description = "The email of the sender of ihatemoney emails";
+          description = lib.mdDoc "The email of the sender of ihatemoney emails";
         };
       };
       secureCookie = mkOption {
         type = types.bool;
         default = true;
-        description = "Use secure cookies. Disable this when ihatemoney is served via http instead of https";
+        description = lib.mdDoc "Use secure cookies. Disable this when ihatemoney is served via http instead of https";
       };
       enableDemoProject = mkEnableOption "access to the demo project in ihatemoney";
       enablePublicProjectCreation = mkEnableOption "permission to create projects in ihatemoney by anyone";
@@ -95,12 +95,12 @@ in
       legalLink = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "The URL to a page explaining legal statements about your service, eg. GDPR-related information.";
+        description = lib.mdDoc "The URL to a page explaining legal statements about your service, eg. GDPR-related information.";
       };
       extraConfig = mkOption {
         type = types.str;
         default = "";
-        description = "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
+        description = lib.mdDoc "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
       };
     };
     config = mkIf cfg.enable {
diff --git a/nixpkgs/nixos/modules/services/web-apps/invidious.nix b/nixpkgs/nixos/modules/services/web-apps/invidious.nix
index 10b30bf1fd1d..0b9d9b03c6ae 100644
--- a/nixpkgs/nixos/modules/services/web-apps/invidious.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/invidious.nix
@@ -152,27 +152,27 @@ in
       type = types.package;
       default = pkgs.invidious;
       defaultText = "pkgs.invidious";
-      description = "The Invidious package to use.";
+      description = lib.mdDoc "The Invidious package to use.";
     };
 
     settings = lib.mkOption {
       type = settingsFormat.type;
       default = { };
-      description = ''
+      description = lib.mdDoc ''
         The settings Invidious should use.
 
-        See <link xlink:href="https://github.com/iv-org/invidious/blob/master/config/config.example.yml">config.example.yml</link> for a list of all possible options.
+        See [config.example.yml](https://github.com/iv-org/invidious/blob/master/config/config.example.yml) for a list of all possible options.
       '';
     };
 
     extraSettingsFile = lib.mkOption {
       type = types.nullOr types.str;
       default = null;
-      description = ''
+      description = lib.mdDoc ''
         A file including Invidious settings.
 
-        It gets merged with the setttings specified in <option>services.invidious.settings</option>
-        and can be used to store secrets like <literal>hmac_key</literal> outside of the nix store.
+        It gets merged with the setttings specified in {option}`services.invidious.settings`
+        and can be used to store secrets like `hmac_key` outside of the nix store.
       '';
     };
 
@@ -182,7 +182,7 @@ in
     domain = lib.mkOption {
       type = types.nullOr types.str;
       default = null;
-      description = ''
+      description = lib.mdDoc ''
         The FQDN Invidious is reachable on.
 
         This is used to configure nginx and for building absolute URLs.
@@ -193,12 +193,12 @@ in
       type = types.port;
       # Default from https://docs.invidious.io/Configuration.md
       default = 3000;
-      description = ''
+      description = lib.mdDoc ''
         The port Invidious should listen on.
 
         To allow access from outside,
-        you can use either <option>services.invidious.nginx</option>
-        or add <literal>config.services.invidious.port</literal> to <option>networking.firewall.allowedTCPPorts</option>.
+        you can use either {option}`services.invidious.nginx`
+        or add `config.services.invidious.port` to {option}`networking.firewall.allowedTCPPorts`.
       '';
     };
 
@@ -206,7 +206,7 @@ in
       createLocally = lib.mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to create a local database with PostgreSQL.
         '';
       };
@@ -214,10 +214,10 @@ in
       host = lib.mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = ''
+        description = lib.mdDoc ''
           The database host Invidious should use.
 
-          If <literal>null</literal>, the local unix socket is used. Otherwise
+          If `null`, the local unix socket is used. Otherwise
           TCP is used.
         '';
       };
@@ -226,7 +226,7 @@ in
         type = types.port;
         default = options.services.postgresql.port.default;
         defaultText = lib.literalExpression "options.services.postgresql.port.default";
-        description = ''
+        description = lib.mdDoc ''
           The port of the database Invidious should use.
 
           Defaults to the the default postgresql port.
@@ -237,7 +237,7 @@ in
         type = types.nullOr types.str;
         apply = lib.mapNullable toString;
         default = null;
-        description = ''
+        description = lib.mdDoc ''
           Path to file containing the database password.
         '';
       };
diff --git a/nixpkgs/nixos/modules/services/web-apps/invoiceplane.nix b/nixpkgs/nixos/modules/services/web-apps/invoiceplane.nix
index 095eec36dec3..2a936027bd47 100644
--- a/nixpkgs/nixos/modules/services/web-apps/invoiceplane.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/invoiceplane.nix
@@ -72,7 +72,7 @@ let
         stateDir = mkOption {
           type = types.path;
           default = "/var/lib/invoiceplane/${name}";
-          description = ''
+          description = lib.mdDoc ''
             This directory is used for uploads of attachements and cache.
             The directory passed here is automatically created and permissions
             adjusted as required.
@@ -83,41 +83,41 @@ let
           host = mkOption {
             type = types.str;
             default = "localhost";
-            description = "Database host address.";
+            description = lib.mdDoc "Database host address.";
           };
 
           port = mkOption {
             type = types.port;
             default = 3306;
-            description = "Database host port.";
+            description = lib.mdDoc "Database host port.";
           };
 
           name = mkOption {
             type = types.str;
             default = "invoiceplane";
-            description = "Database name.";
+            description = lib.mdDoc "Database name.";
           };
 
           user = mkOption {
             type = types.str;
             default = "invoiceplane";
-            description = "Database user.";
+            description = lib.mdDoc "Database user.";
           };
 
           passwordFile = mkOption {
             type = types.nullOr types.path;
             default = null;
             example = "/run/keys/invoiceplane-dbpassword";
-            description = ''
+            description = lib.mdDoc ''
               A file containing the password corresponding to
-              <option>database.user</option>.
+              {option}`database.user`.
             '';
           };
 
           createLocally = mkOption {
             type = types.bool;
             default = true;
-            description = "Create the database and database user locally.";
+            description = lib.mdDoc "Create the database and database user locally.";
           };
         };
 
@@ -160,8 +160,8 @@ let
             "pm.max_spare_servers" = 4;
             "pm.max_requests" = 500;
           };
-          description = ''
-            Options for the InvoicePlane PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+          description = lib.mdDoc ''
+            Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf`
             for details on configuration directives.
           '';
         };
@@ -174,9 +174,9 @@ let
             DISABLE_SETUP=true
             IP_URL=https://invoice.example.com
           '';
-          description = ''
+          description = lib.mdDoc ''
             InvoicePlane configuration. Refer to
-            <link xlink:href="https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example"/>
+            <https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example>
             for details on supported values.
           '';
         };
@@ -194,20 +194,20 @@ in
         options.sites = mkOption {
           type = types.attrsOf (types.submodule siteOpts);
           default = {};
-          description = "Specification of one or more WordPress sites to serve";
+          description = lib.mdDoc "Specification of one or more WordPress sites to serve";
         };
 
         options.webserver = mkOption {
           type = types.enum [ "caddy" ];
           default = "caddy";
-          description = ''
+          description = lib.mdDoc ''
             Which webserver to use for virtual host management. Currently only
             caddy is supported.
           '';
         };
       };
       default = {};
-      description = "InvoicePlane configuration.";
+      description = lib.mdDoc "InvoicePlane configuration.";
     };
 
   };
@@ -236,7 +236,7 @@ in
     };
 
     services.phpfpm = {
-      phpPackage = pkgs.php74;
+      phpPackage = pkgs.php81;
       pools = mapAttrs' (hostName: cfg: (
         nameValuePair "invoiceplane-${hostName}" {
           inherit user;
@@ -302,4 +302,3 @@ in
 
   ]);
 }
-
diff --git a/nixpkgs/nixos/modules/services/web-apps/jirafeau.nix b/nixpkgs/nixos/modules/services/web-apps/jirafeau.nix
index 328c61c8e646..c95d8ffd524d 100644
--- a/nixpkgs/nixos/modules/services/web-apps/jirafeau.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/jirafeau.nix
@@ -25,7 +25,7 @@ in
     adminPasswordSha256 = mkOption {
       type = types.str;
       default = "";
-      description = ''
+      description = lib.mdDoc ''
         SHA-256 of the desired administration password. Leave blank/unset for no password.
       '';
     };
@@ -33,7 +33,7 @@ in
     dataDir = mkOption {
       type = types.path;
       default = "/var/lib/jirafeau/data/";
-      description = "Location of Jirafeau storage directory.";
+      description = lib.mdDoc "Location of Jirafeau storage directory.";
     };
 
     enable = mkEnableOption "Jirafeau file upload application.";
@@ -58,13 +58,13 @@ in
     hostName = mkOption {
       type = types.str;
       default = "localhost";
-      description = "URL of instance. Must have trailing slash.";
+      description = lib.mdDoc "URL of instance. Must have trailing slash.";
     };
 
     maxUploadSizeMegabytes = mkOption {
       type = types.int;
       default = 0;
-      description = "Maximum upload size of accepted files.";
+      description = lib.mdDoc "Maximum upload size of accepted files.";
     };
 
     maxUploadTimeout = mkOption {
@@ -89,14 +89,14 @@ in
           serverAliases = [ "wiki.''${config.networking.domain}" ];
         }
       '';
-      description = "Extra configuration for the nginx virtual host of Jirafeau.";
+      description = lib.mdDoc "Extra configuration for the nginx virtual host of Jirafeau.";
     };
 
     package = mkOption {
       type = types.package;
       default = pkgs.jirafeau;
       defaultText = literalExpression "pkgs.jirafeau";
-      description = "Jirafeau package to use";
+      description = lib.mdDoc "Jirafeau package to use";
     };
 
     poolConfig = mkOption {
@@ -109,8 +109,8 @@ in
         "pm.max_spare_servers" = 4;
         "pm.max_requests" = 500;
       };
-      description = ''
-        Options for Jirafeau PHP pool. See documentation on <literal>php-fpm.conf</literal> for
+      description = lib.mdDoc ''
+        Options for Jirafeau PHP pool. See documentation on `php-fpm.conf` for
         details on configuration directives.
       '';
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/jitsi-meet.nix b/nixpkgs/nixos/modules/services/web-apps/jitsi-meet.nix
index 2f1c4acec1e8..b38a510bb87e 100644
--- a/nixpkgs/nixos/modules/services/web-apps/jitsi-meet.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -51,7 +51,7 @@ in
     hostName = mkOption {
       type = str;
       example = "meet.example.org";
-      description = ''
+      description = lib.mdDoc ''
         FQDN of the Jitsi Meet instance.
       '';
     };
@@ -65,10 +65,10 @@ in
           defaultLang = "fi";
         }
       '';
-      description = ''
-        Client-side web application settings that override the defaults in <filename>config.js</filename>.
+      description = lib.mdDoc ''
+        Client-side web application settings that override the defaults in {file}`config.js`.
 
-        See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/config.js" /> for default
+        See <https://github.com/jitsi/jitsi-meet/blob/master/config.js> for default
         configuration with comments.
       '';
     };
@@ -76,8 +76,8 @@ in
     extraConfig = mkOption {
       type = lines;
       default = "";
-      description = ''
-        Text to append to <filename>config.js</filename> web application config file.
+      description = lib.mdDoc ''
+        Text to append to {file}`config.js` web application config file.
 
         Can be used to insert JavaScript logic to determine user's region in cascading bridges setup.
       '';
@@ -92,10 +92,10 @@ in
           SHOW_WATERMARK_FOR_GUESTS = false;
         }
       '';
-      description = ''
-        Client-side web-app interface settings that override the defaults in <filename>interface_config.js</filename>.
+      description = lib.mdDoc ''
+        Client-side web-app interface settings that override the defaults in {file}`interface_config.js`.
 
-        See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js" /> for
+        See <https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js> for
         default configuration with comments.
       '';
     };
@@ -104,10 +104,10 @@ in
       enable = mkOption {
         type = bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to enable Jitsi Videobridge instance and configure it to connect to Prosody.
 
-          Additional configuration is possible with <option>services.jitsi-videobridge</option>.
+          Additional configuration is possible with {option}`services.jitsi-videobridge`.
         '';
       };
 
@@ -115,10 +115,10 @@ in
         type = nullOr str;
         default = null;
         example = "/run/keys/videobridge";
-        description = ''
+        description = lib.mdDoc ''
           File containing password to the Prosody account for videobridge.
 
-          If <literal>null</literal>, a file with password will be generated automatically. Setting
+          If `null`, a file with password will be generated automatically. Setting
           this option is useful if you plan to connect additional videobridges to the XMPP server.
         '';
       };
@@ -127,44 +127,44 @@ in
     jicofo.enable = mkOption {
       type = bool;
       default = true;
-      description = ''
+      description = lib.mdDoc ''
         Whether to enable JiCoFo instance and configure it to connect to Prosody.
 
-        Additional configuration is possible with <option>services.jicofo</option>.
+        Additional configuration is possible with {option}`services.jicofo`.
       '';
     };
 
     jibri.enable = mkOption {
       type = bool;
       default = false;
-      description = ''
+      description = lib.mdDoc ''
         Whether to enable a Jibri instance and configure it to connect to Prosody.
 
-        Additional configuration is possible with <option>services.jibri</option>, and
-        <option>services.jibri.finalizeScript</option> is especially useful.
+        Additional configuration is possible with {option}`services.jibri`, and
+        {option}`services.jibri.finalizeScript` is especially useful.
       '';
     };
 
     nginx.enable = mkOption {
       type = bool;
       default = true;
-      description = ''
+      description = lib.mdDoc ''
         Whether to enable nginx virtual host that will serve the javascript application and act as
         a proxy for the XMPP server. Further nginx configuration can be done by adapting
-        <option>services.nginx.virtualHosts.&lt;hostName&gt;</option>.
+        {option}`services.nginx.virtualHosts.<hostName>`.
         When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
-        this, set the <option>services.nginx.virtualHosts.&lt;hostName&gt;.enableACME</option> to
-        <literal>false</literal> and if appropriate do the same for
-        <option>services.nginx.virtualHosts.&lt;hostName&gt;.forceSSL</option>.
+        this, set the {option}`services.nginx.virtualHosts.<hostName>.enableACME` to
+        `false` and if appropriate do the same for
+        {option}`services.nginx.virtualHosts.<hostName>.forceSSL`.
       '';
     };
 
-    caddy.enable = mkEnableOption "Whether to enablle caddy reverse proxy to expose jitsi-meet";
+    caddy.enable = mkEnableOption "Whether to enable caddy reverse proxy to expose jitsi-meet";
 
     prosody.enable = mkOption {
       type = bool;
       default = true;
-      description = ''
+      description = lib.mdDoc ''
         Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this
         off if you want to configure it manually.
       '';
@@ -253,9 +253,20 @@ in
         '';
       };
     };
-    systemd.services.prosody.serviceConfig = mkIf cfg.prosody.enable {
-      EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
-      SupplementaryGroups = [ "jitsi-meet" ];
+    systemd.services.prosody = mkIf cfg.prosody.enable {
+      preStart = let
+        videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
+      in ''
+        ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
+        ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
+        ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
+        ${config.services.prosody.package}/bin/prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
+        ${config.services.prosody.package}/bin/prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
+      '';
+      serviceConfig = {
+        EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
+        SupplementaryGroups = [ "jitsi-meet" ];
+      };
     };
 
     users.groups.jitsi-meet = {};
@@ -266,14 +277,12 @@ in
     systemd.services.jitsi-meet-init-secrets = {
       wantedBy = [ "multi-user.target" ];
       before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service");
-      path = [ config.services.prosody.package ];
       serviceConfig = {
         Type = "oneshot";
       };
 
       script = let
         secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
-        videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
       in
       ''
         cd /var/lib/jitsi-meet
@@ -291,12 +300,6 @@ in
         chmod 640 secrets-env
       ''
       + optionalString cfg.prosody.enable ''
-        prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
-        prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
-        prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
-        prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
-        prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
-
         # generate self-signed certificates
         if [ ! -f /var/lib/jitsi-meet.crt ]; then
           ${getBin pkgs.openssl}/bin/openssl req \
diff --git a/nixpkgs/nixos/modules/services/web-apps/keycloak.nix b/nixpkgs/nixos/modules/services/web-apps/keycloak.nix
index c4a2127663a9..b878cb74b52e 100644
--- a/nixpkgs/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/keycloak.nix
@@ -4,115 +4,114 @@ let
   cfg = config.services.keycloak;
   opt = options.services.keycloak;
 
-  inherit (lib) types mkOption concatStringsSep mapAttrsToList
-    escapeShellArg recursiveUpdate optionalAttrs boolToString mkOrder
-    sort filterAttrs concatMapStringsSep concatStrings mkIf
-    optionalString optionals mkDefault literalExpression hasSuffix
-    foldl' isAttrs filter attrNames elem literalDocBook
-    maintainers;
-
-  inherit (builtins) match typeOf;
+  inherit (lib)
+    types
+    mkMerge
+    mkOption
+    mkChangedOptionModule
+    mkRenamedOptionModule
+    mkRemovedOptionModule
+    concatStringsSep
+    mapAttrsToList
+    escapeShellArg
+    mkIf
+    optionalString
+    optionals
+    mkDefault
+    literalExpression
+    isAttrs
+    literalDocBook
+    maintainers
+    catAttrs
+    collect
+    splitString
+    ;
+
+  inherit (builtins)
+    elem
+    typeOf
+    isInt
+    isString
+    hashString
+    isPath
+    ;
+
+  prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}";
 in
 {
+  imports =
+    [
+      (mkRenamedOptionModule
+        [ "services" "keycloak" "bindAddress" ]
+        [ "services" "keycloak" "settings" "http-host" ])
+      (mkRenamedOptionModule
+        [ "services" "keycloak" "forceBackendUrlToFrontendUrl"]
+        [ "services" "keycloak" "settings" "hostname-strict-backchannel"])
+      (mkChangedOptionModule
+        [ "services" "keycloak" "httpPort" ]
+        [ "services" "keycloak" "settings" "http-port" ]
+        (config:
+          builtins.fromJSON config.services.keycloak.httpPort))
+      (mkChangedOptionModule
+        [ "services" "keycloak" "httpsPort" ]
+        [ "services" "keycloak" "settings" "https-port" ]
+        (config:
+          builtins.fromJSON config.services.keycloak.httpsPort))
+      (mkRemovedOptionModule
+        [ "services" "keycloak" "frontendUrl" ]
+        ''
+          Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead.
+          NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients.
+                See its description for more information.
+        '')
+      (mkRemovedOptionModule
+        [ "services" "keycloak" "extraConfig" ]
+        "Use `services.keycloak.settings' instead.")
+    ];
+
   options.services.keycloak =
     let
-      inherit (types) bool str nullOr attrsOf path enum anything
-        package port;
+      inherit (types)
+        bool
+        str
+        int
+        nullOr
+        attrsOf
+        oneOf
+        path
+        enum
+        package
+        port;
+
+      assertStringPath = optionName: value:
+        if isPath value then
+          throw ''
+            services.keycloak.${optionName}:
+              ${toString value}
+              is a Nix path, but should be a string, since Nix
+              paths are copied into the world-readable Nix store.
+          ''
+        else value;
     in
     {
       enable = mkOption {
         type = bool;
         default = false;
         example = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to enable the Keycloak identity and access management
           server.
         '';
       };
 
-      bindAddress = mkOption {
-        type = str;
-        default = "\${jboss.bind.address:0.0.0.0}";
-        example = "127.0.0.1";
-        description = ''
-          On which address Keycloak should accept new connections.
-
-          A special syntax can be used to allow command line Java system
-          properties to override the value: ''${property.name:value}
-        '';
-      };
-
-      httpPort = mkOption {
-        type = str;
-        default = "\${jboss.http.port:80}";
-        example = "8080";
-        description = ''
-          On which port Keycloak should listen for new HTTP connections.
-
-          A special syntax can be used to allow command line Java system
-          properties to override the value: ''${property.name:value}
-        '';
-      };
-
-      httpsPort = mkOption {
-        type = str;
-        default = "\${jboss.https.port:443}";
-        example = "8443";
-        description = ''
-          On which port Keycloak should listen for new HTTPS connections.
-
-          A special syntax can be used to allow command line Java system
-          properties to override the value: ''${property.name:value}
-        '';
-      };
-
-      frontendUrl = mkOption {
-        type = str;
-        apply = x:
-          if x == "" || hasSuffix "/" x then
-            x
-          else
-            x + "/";
-        example = "keycloak.example.com/auth";
-        description = ''
-          The public URL used as base for all frontend requests. Should
-          normally include a trailing <literal>/auth</literal>.
-
-          See <link xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
-          Hostname section of the Keycloak server installation
-          manual</link> for more information.
-        '';
-      };
-
-      forceBackendUrlToFrontendUrl = mkOption {
-        type = bool;
-        default = false;
-        example = true;
-        description = ''
-          Whether Keycloak should force all requests to go through the
-          frontend URL configured in <xref
-          linkend="opt-services.keycloak.frontendUrl" />. By default,
-          Keycloak allows backend requests to instead use its local
-          hostname or IP address and may also advertise it to clients
-          through its OpenID Connect Discovery endpoint.
-
-          See <link
-          xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
-          Hostname section of the Keycloak server installation
-          manual</link> for more information.
-        '';
-      };
-
       sslCertificate = mkOption {
         type = nullOr path;
         default = null;
         example = "/run/keys/ssl_cert";
-        description = ''
+        apply = assertStringPath "sslCertificate";
+        description = lib.mdDoc ''
           The path to a PEM formatted certificate to use for TLS/SSL
           connections.
-
-          This should be a string, not a Nix path, since Nix paths are
-          copied into the world-readable Nix store.
         '';
       };
 
@@ -120,29 +119,29 @@ in
         type = nullOr path;
         default = null;
         example = "/run/keys/ssl_key";
-        description = ''
+        apply = assertStringPath "sslCertificateKey";
+        description = lib.mdDoc ''
           The path to a PEM formatted private key to use for TLS/SSL
           connections.
-
-          This should be a string, not a Nix path, since Nix paths are
-          copied into the world-readable Nix store.
         '';
       };
 
       plugins = lib.mkOption {
         type = lib.types.listOf lib.types.path;
-        default = [];
-        description = ''
-          Keycloak plugin jar, ear files or derivations with them
+        default = [ ];
+        description = lib.mdDoc ''
+          Keycloak plugin jar, ear files or derivations containing
+          them. Packaged plugins are available through
+          `pkgs.keycloak.plugins`.
         '';
       };
 
       database = {
         type = mkOption {
-          type = enum [ "mysql" "postgresql" ];
+          type = enum [ "mysql" "mariadb" "postgresql" ];
           default = "postgresql";
-          example = "mysql";
-          description = ''
+          example = "mariadb";
+          description = lib.mdDoc ''
             The type of database Keycloak should connect to.
           '';
         };
@@ -150,7 +149,7 @@ in
         host = mkOption {
           type = str;
           default = "localhost";
-          description = ''
+          description = lib.mdDoc ''
             Hostname of the database to connect to.
           '';
         };
@@ -159,6 +158,7 @@ in
           let
             dbPorts = {
               postgresql = 5432;
+              mariadb = 3306;
               mysql = 3306;
             };
           in
@@ -166,7 +166,7 @@ in
             type = port;
             default = dbPorts.${cfg.database.type};
             defaultText = literalDocBook "default port of selected database";
-            description = ''
+            description = lib.mdDoc ''
               Port of the database to connect to.
             '';
           };
@@ -175,7 +175,7 @@ in
           type = bool;
           default = cfg.database.host != "localhost";
           defaultText = literalExpression ''config.${opt.database.host} != "localhost"'';
-          description = ''
+          description = lib.mdDoc ''
             Whether the database connection should be secured by SSL /
             TLS.
           '';
@@ -184,13 +184,13 @@ in
         caCert = mkOption {
           type = nullOr path;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             The SSL / TLS CA certificate that verifies the identity of the
             database server.
 
             Required when PostgreSQL is used and SSL is turned on.
 
-            For MySQL, if left at <literal>null</literal>, the default
+            For MySQL, if left at `null`, the default
             Java keystore is used, which should suffice if the server
             certificate is issued by an official CA.
           '';
@@ -199,7 +199,7 @@ in
         createLocally = mkOption {
           type = bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             Whether a database should be automatically created on the
             local host. Set this to false if you plan on provisioning a
             local database yourself. This has no effect if
@@ -207,30 +207,40 @@ in
           '';
         };
 
+        name = mkOption {
+          type = str;
+          default = "keycloak";
+          description = lib.mdDoc ''
+            Database name to use when connecting to an external or
+            manually provisioned database; has no effect when a local
+            database is automatically provisioned.
+
+            To use this with a local database, set [](#opt-services.keycloak.database.createLocally) to
+            `false` and create the database and user
+            manually.
+          '';
+        };
+
         username = mkOption {
           type = str;
           default = "keycloak";
-          description = ''
+          description = lib.mdDoc ''
             Username to use when connecting to an external or manually
             provisioned database; has no effect when a local database is
             automatically provisioned.
 
-            To use this with a local database, set <xref
-            linkend="opt-services.keycloak.database.createLocally" /> to
-            <literal>false</literal> and create the database and user
-            manually. The database should be called
-            <literal>keycloak</literal>.
+            To use this with a local database, set [](#opt-services.keycloak.database.createLocally) to
+            `false` and create the database and user
+            manually.
           '';
         };
 
         passwordFile = mkOption {
           type = path;
           example = "/run/keys/db_password";
-          description = ''
-            File containing the database password.
-
-            This should be a string, not a Nix path, since Nix paths are
-            copied into the world-readable Nix store.
+          apply = assertStringPath "passwordFile";
+          description = lib.mdDoc ''
+            The path to a file containing the database password.
           '';
         };
       };
@@ -239,7 +249,7 @@ in
         type = package;
         default = pkgs.keycloak;
         defaultText = literalExpression "pkgs.keycloak";
-        description = ''
+        description = lib.mdDoc ''
           Keycloak package to use.
         '';
       };
@@ -247,8 +257,8 @@ in
       initialAdminPassword = mkOption {
         type = str;
         default = "changeme";
-        description = ''
-          Initial password set for the <literal>admin</literal>
+        description = lib.mdDoc ''
+          Initial password set for the `admin`
           user. The password is not stored safely and should be changed
           immediately in the admin panel.
         '';
@@ -257,78 +267,187 @@ in
       themes = mkOption {
         type = attrsOf package;
         default = { };
-        description = ''
+        description = lib.mdDoc ''
           Additional theme packages for Keycloak. Each theme is linked into
           subdirectory with a corresponding attribute name.
 
           Theme packages consist of several subdirectories which provide
-          different theme types: for example, <literal>account</literal>,
-          <literal>login</literal> etc. After adding a theme to this option you
+          different theme types: for example, `account`,
+          `login` etc. After adding a theme to this option you
           can select it by its name in Keycloak administration console.
         '';
       };
 
-      extraConfig = mkOption {
-        type = attrsOf anything;
-        default = { };
+      settings = mkOption {
+        type = lib.types.submodule {
+          freeformType = attrsOf (nullOr (oneOf [ str int bool (attrsOf path) ]));
+
+          options = {
+            http-host = mkOption {
+              type = str;
+              default = "0.0.0.0";
+              example = "127.0.0.1";
+              description = lib.mdDoc ''
+                On which address Keycloak should accept new connections.
+              '';
+            };
+
+            http-port = mkOption {
+              type = port;
+              default = 80;
+              example = 8080;
+              description = lib.mdDoc ''
+                On which port Keycloak should listen for new HTTP connections.
+              '';
+            };
+
+            https-port = mkOption {
+              type = port;
+              default = 443;
+              example = 8443;
+              description = lib.mdDoc ''
+                On which port Keycloak should listen for new HTTPS connections.
+              '';
+            };
+
+            http-relative-path = mkOption {
+              type = str;
+              default = "";
+              example = "/auth";
+              description = ''
+                The path relative to <literal>/</literal> for serving
+                resources.
+
+                <note>
+                  <para>
+                    In versions of Keycloak using Wildfly (&lt;17),
+                    this defaulted to <literal>/auth</literal>. If
+                    upgrading from the Wildfly version of Keycloak,
+                    i.e. a NixOS version before 22.05, you'll likely
+                    want to set this to <literal>/auth</literal> to
+                    keep compatibility with your clients.
+
+                    See <link xlink:href="https://www.keycloak.org/migration/migrating-to-quarkus"/>
+                    for more information on migrating from Wildfly to Quarkus.
+                  </para>
+                </note>
+              '';
+            };
+
+            hostname = mkOption {
+              type = str;
+              example = "keycloak.example.com";
+              description = lib.mdDoc ''
+                The hostname part of the public URL used as base for
+                all frontend requests.
+
+                See <https://www.keycloak.org/server/hostname>
+                for more information about hostname configuration.
+              '';
+            };
+
+            hostname-strict-backchannel = mkOption {
+              type = bool;
+              default = false;
+              example = true;
+              description = lib.mdDoc ''
+                Whether Keycloak should force all requests to go
+                through the frontend URL. By default, Keycloak allows
+                backend requests to instead use its local hostname or
+                IP address and may also advertise it to clients
+                through its OpenID Connect Discovery endpoint.
+
+                See <https://www.keycloak.org/server/hostname>
+                for more information about hostname configuration.
+              '';
+            };
+
+            proxy = mkOption {
+              type = enum [ "edge" "reencrypt" "passthrough" "none" ];
+              default = "none";
+              example = "edge";
+              description = ''
+                The proxy address forwarding mode if the server is
+                behind a reverse proxy.
+
+                <variablelist>
+                  <varlistentry>
+                    <term>edge</term>
+                    <listitem>
+                      <para>
+                        Enables communication through HTTP between the
+                        proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                  <varlistentry>
+                    <term>reencrypt</term>
+                    <listitem>
+                      <para>
+                        Requires communication through HTTPS between the
+                        proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                  <varlistentry>
+                    <term>passthrough</term>
+                    <listitem>
+                      <para>
+                        Enables communication through HTTP or HTTPS between
+                        the proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                </variablelist>
+
+                See <link xlink:href="https://www.keycloak.org/server/reverseproxy"/> for more information.
+              '';
+            };
+          };
+        };
+
         example = literalExpression ''
           {
-            "subsystem=keycloak-server" = {
-              "spi=hostname" = {
-                "provider=default" = null;
-                "provider=fixed" = {
-                  enabled = true;
-                  properties.hostname = "keycloak.example.com";
-                };
-                default-provider = "fixed";
-              };
-            };
+            hostname = "keycloak.example.com";
+            proxy = "reencrypt";
+            https-key-store-file = "/path/to/file";
+            https-key-store-password = { _secret = "/run/keys/store_password"; };
           }
         '';
-        description = ''
-          Additional Keycloak configuration options to set in
-          <literal>standalone.xml</literal>.
-
-          Options are expressed as a Nix attribute set which matches the
-          structure of the jboss-cli configuration. The configuration is
-          effectively overlayed on top of the default configuration
-          shipped with Keycloak. To remove existing nodes and undefine
-          attributes from the default configuration, set them to
-          <literal>null</literal>.
-
-          The example configuration does the equivalent of the following
-          script, which removes the hostname provider
-          <literal>default</literal>, adds the deprecated hostname
-          provider <literal>fixed</literal> and defines it the default:
-
-          <programlisting>
-          /subsystem=keycloak-server/spi=hostname/provider=default:remove()
-          /subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
-          /subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
-          </programlisting>
-
-          You can discover available options by using the <link
-          xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
-          program and by referring to the <link
-          xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
-          Server Installation and Configuration Guide</link>.
+
+        description = lib.mdDoc ''
+          Configuration options corresponding to parameters set in
+          {file}`conf/keycloak.conf`.
+
+          Most available options are documented at <https://www.keycloak.org/server/all-config>.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute `_secret` - a
+          string pointing to a file containing the value the option
+          should be set to. See the example to get a better picture of
+          this: in the resulting
+          {file}`conf/keycloak.conf` file, the
+          `https-key-store-password` key will be set
+          to the contents of the
+          {file}`/run/keys/store_password` file.
         '';
       };
-
     };
 
   config =
     let
-      # We only want to create a database if we're actually going to connect to it.
+      # We only want to create a database if we're actually going to
+      # connect to it.
       databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
       createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
-      createLocalMySQL = databaseActuallyCreateLocally && cfg.database.type == "mysql";
+      createLocalMySQL = databaseActuallyCreateLocally && elem cfg.database.type [ "mysql" "mariadb" ];
 
       mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } ''
         ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
       '';
 
-      # Both theme and theme type directories need to be actual directories in one hierarchy to pass Keycloak checks.
+      # Both theme and theme type directories need to be actual
+      # directories in one hierarchy to pass Keycloak checks.
       themesBundle = pkgs.runCommand "keycloak-themes" { } ''
         linkTheme() {
           theme="$1"
@@ -347,7 +466,7 @@ in
         }
 
         mkdir -p "$out"
-        for theme in ${cfg.package}/themes/*; do
+        for theme in ${keycloakBuild}/themes/*; do
           if [ -d "$theme" ]; then
             linkTheme "$theme" "$(basename "$theme")"
           fi
@@ -356,329 +475,25 @@ in
         ${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)}
       '';
 
-      keycloakConfig' = foldl' recursiveUpdate
-        {
-          "interface=public".inet-address = cfg.bindAddress;
-          "socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort;
-          "subsystem=keycloak-server" = {
-            "spi=hostname"."provider=default" = {
-              enabled = true;
-              properties = {
-                inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl;
-              };
-            };
-            "theme=defaults".dir = toString themesBundle;
-          };
-          "subsystem=datasources"."data-source=KeycloakDS" = {
-            max-pool-size = "20";
-            user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
-            password = "@db-password@";
-          };
-        } [
-        (optionalAttrs (cfg.database.type == "postgresql") {
-          "subsystem=datasources" = {
-            "jdbc-driver=postgresql" = {
-              driver-module-name = "org.postgresql";
-              driver-name = "postgresql";
-              driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource";
-            };
-            "data-source=KeycloakDS" = {
-              connection-url = "jdbc:postgresql://${cfg.database.host}:${toString cfg.database.port}/keycloak";
-              driver-name = "postgresql";
-              "connection-properties=ssl".value = boolToString cfg.database.useSSL;
-            } // (optionalAttrs (cfg.database.caCert != null) {
-              "connection-properties=sslrootcert".value = cfg.database.caCert;
-              "connection-properties=sslmode".value = "verify-ca";
-            });
-          };
-        })
-        (optionalAttrs (cfg.database.type == "mysql") {
-          "subsystem=datasources" = {
-            "jdbc-driver=mysql" = {
-              driver-module-name = "com.mysql";
-              driver-name = "mysql";
-              driver-class-name = "com.mysql.jdbc.Driver";
-            };
-            "data-source=KeycloakDS" = {
-              connection-url = "jdbc:mysql://${cfg.database.host}:${toString cfg.database.port}/keycloak";
-              driver-name = "mysql";
-              "connection-properties=useSSL".value = boolToString cfg.database.useSSL;
-              "connection-properties=requireSSL".value = boolToString cfg.database.useSSL;
-              "connection-properties=verifyServerCertificate".value = boolToString cfg.database.useSSL;
-              "connection-properties=characterEncoding".value = "UTF-8";
-              valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker";
-              validate-on-match = true;
-              exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter";
-            } // (optionalAttrs (cfg.database.caCert != null) {
-              "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}";
-              "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword";
-            });
-          };
-        })
-        (optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
-          "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort;
-          "subsystem=elytron" = mkOrder 900 {
-            "key-store=httpsKS" = mkOrder 900 {
-              path = "/run/keycloak/ssl/certificate_private_key_bundle.p12";
-              credential-reference.clear-text = "notsosecretpassword";
-              type = "JKS";
-            };
-            "key-manager=httpsKM" = mkOrder 901 {
-              key-store = "httpsKS";
-              credential-reference.clear-text = "notsosecretpassword";
-            };
-            "server-ssl-context=httpsSSC" = mkOrder 902 {
-              key-manager = "httpsKM";
-            };
-          };
-          "subsystem=undertow" = mkOrder 901 {
-            "server=default-server"."https-listener=https".ssl-context = "httpsSSC";
-          };
-        })
-        cfg.extraConfig
-      ];
-
-
-      /* Produces a JBoss CLI script that creates paths and sets
-         attributes matching those described by `attrs`. When the
-         script is run, the existing settings are effectively overlayed
-         by those from `attrs`. Existing attributes can be unset by
-         defining them `null`.
-
-         JBoss paths and attributes / maps are distinguished by their
-         name, where paths follow a `key=value` scheme.
-
-         Example:
-           mkJbossScript {
-             "subsystem=keycloak-server"."spi=hostname" = {
-               "provider=fixed" = null;
-               "provider=default" = {
-                 enabled = true;
-                 properties = {
-                   inherit frontendUrl;
-                   forceBackendUrlToFrontendUrl = false;
-                 };
-               };
-             };
-           }
-           => ''
-             if (outcome != success) of /:read-resource()
-                 /:add()
-             end-if
-             if (outcome != success) of /subsystem=keycloak-server:read-resource()
-                 /subsystem=keycloak-server:add()
-             end-if
-             if (outcome != success) of /subsystem=keycloak-server/spi=hostname:read-resource()
-                 /subsystem=keycloak-server/spi=hostname:add()
-             end-if
-             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=default:read-resource()
-                 /subsystem=keycloak-server/spi=hostname/provider=default:add(enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" })
-             end-if
-             if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
-               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
-             end-if
-             if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
-               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
-             end-if
-             if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
-               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
-             end-if
-             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=fixed:read-resource()
-                 /subsystem=keycloak-server/spi=hostname/provider=fixed:remove()
-             end-if
-           ''
-      */
-      mkJbossScript = attrs:
-        let
-          /* From a JBoss path and an attrset, produces a JBoss CLI
-             snippet that writes the corresponding attributes starting
-             at `path`. Recurses down into subattrsets as necessary,
-             producing the variable name from its full path in the
-             attrset.
-
-             Example:
-               writeAttributes "/subsystem=keycloak-server/spi=hostname/provider=default" {
-                 enabled = true;
-                 properties = {
-                   forceBackendUrlToFrontendUrl = false;
-                   frontendUrl = "https://keycloak.example.com/auth";
-                 };
-               }
-               => ''
-                 if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
-                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
-                 end-if
-                 if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
-                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
-                 end-if
-                 if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
-                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
-                 end-if
-               ''
-          */
-          writeAttributes = path: set:
-            let
-              # JBoss expressions like `${var}` need to be prefixed
-              # with `expression` to evaluate.
-              prefixExpression = string:
-                let
-                  matchResult = match ''"\$\{.*}"'' string;
-                in
-                if matchResult != null then
-                  "expression " + string
-                else
-                  string;
-
-              writeAttribute = attribute: value:
-                let
-                  type = typeOf value;
-                in
-                if type == "set" then
-                  let
-                    names = attrNames value;
-                  in
-                  foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names
-                else if value == null then ''
-                  if (outcome == success) of ${path}:read-attribute(name="${attribute}")
-                      ${path}:undefine-attribute(name="${attribute}")
-                  end-if
-                ''
-                else if elem type [ "string" "path" "bool" ] then
-                  let
-                    value' = if type == "bool" then boolToString value else ''"${value}"'';
-                  in
-                  ''
-                    if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}")
-                      ${path}:write-attribute(name=${attribute}, value=${value'})
-                    end-if
-                  ''
-                else throw "Unsupported type '${type}' for path '${path}'!";
-            in
-            concatStrings
-              (mapAttrsToList
-                (attribute: value: (writeAttribute attribute value))
-                set);
-
-
-          /* Produces an argument list for the JBoss `add()` function,
-             which adds a JBoss path and takes as its arguments the
-             required subpaths and attributes.
-
-             Example:
-               makeArgList {
-                 enabled = true;
-                 properties = {
-                   forceBackendUrlToFrontendUrl = false;
-                   frontendUrl = "https://keycloak.example.com/auth";
-                 };
-               }
-               => ''
-                 enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" }
-               ''
-          */
-          makeArgList = set:
-            let
-              makeArg = attribute: value:
-                let
-                  type = typeOf value;
-                in
-                if type == "set" then
-                  "${attribute} = { " + (makeArgList value) + " }"
-                else if elem type [ "string" "path" "bool" ] then
-                  "${attribute} = ${if type == "bool" then boolToString value else ''"${value}"''}"
-                else if value == null then
-                  ""
-                else
-                  throw "Unsupported type '${type}' for attribute '${attribute}'!";
-
-            in
-            concatStringsSep ", " (mapAttrsToList makeArg set);
-
-
-          /* Recurses into the `nodeValue` attrset. Only subattrsets that
-             are JBoss paths, i.e. follows the `key=value` format, are recursed
-             into - the rest are considered JBoss attributes / maps.
-          */
-          recurse = nodePath: nodeValue:
-            let
-              nodeContent =
-                if isAttrs nodeValue && nodeValue._type or "" == "order" then
-                  nodeValue.content
-                else
-                  nodeValue;
-              isPath = name:
-                let
-                  value = nodeContent.${name};
-                in
-                if (match ".*([=]).*" name) == [ "=" ] then
-                  if isAttrs value || value == null then
-                    true
-                  else
-                    throw "Parsing path '${concatStringsSep "." (nodePath ++ [ name ])}' failed: JBoss attributes cannot contain '='!"
-                else
-                  false;
-              jbossPath = "/" + concatStringsSep "/" nodePath;
-              children = if !isAttrs nodeContent then { } else nodeContent;
-              subPaths = filter isPath (attrNames children);
-              getPriority = name:
-                let
-                  value = children.${name};
-                in
-                if value._type or "" == "order" then value.priority else 1000;
-              orderedSubPaths = sort (a: b: getPriority a < getPriority b) subPaths;
-              jbossAttrs = filterAttrs (name: _: !(isPath name)) children;
-              text =
-                if nodeContent != null then
-                  ''
-                    if (outcome != success) of ${jbossPath}:read-resource()
-                        ${jbossPath}:add(${makeArgList jbossAttrs})
-                    end-if
-                  '' + writeAttributes jbossPath jbossAttrs
-                else
-                  ''
-                    if (outcome == success) of ${jbossPath}:read-resource()
-                        ${jbossPath}:remove()
-                    end-if
-                  '';
-            in
-            text + concatMapStringsSep "\n" (name: recurse (nodePath ++ [ name ]) children.${name}) orderedSubPaths;
-        in
-        recurse [ ] attrs;
-
-      jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig');
-
-      keycloakConfig = pkgs.runCommand "keycloak-config"
-        {
-          nativeBuildInputs = [ cfg.package ];
-        }
-        ''
-          export JBOSS_BASE_DIR="$(pwd -P)";
-          export JBOSS_MODULEPATH="${cfg.package}/modules";
-          export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log";
-
-          cp -r ${cfg.package}/standalone/configuration .
-          chmod -R u+rwX ./configuration
-
-          mkdir -p {deployments,ssl}
-
-          standalone.sh&
-
-          attempt=1
-          max_attempts=30
-          while ! jboss-cli.sh --connect ':read-attribute(name=server-state)'; do
-              if [[ "$attempt" == "$max_attempts" ]]; then
-                  echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2
-                  exit 1
-              fi
-              echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)"
-              sleep 1
-              (( attempt++ ))
-          done
-
-          jboss-cli.sh --connect --file=${jbossCliScript} --echo-command
+      keycloakConfig = lib.generators.toKeyValue {
+        mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
+          mkValueString = v: with builtins;
+            if isInt v then toString v
+            else if isString v then v
+            else if true == v then "true"
+            else if false == v then "false"
+            else if isSecret v then hashString "sha256" v._secret
+            else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+        };
+      };
 
-          cp configuration/standalone.xml $out
-        '';
+      isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+      filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{ } null])) cfg.settings;
+      confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig);
+      keycloakBuild = cfg.package.override {
+        inherit confFile;
+        plugins = cfg.package.enabledPlugins ++ cfg.plugins;
+      };
     in
     mkIf cfg.enable
       {
@@ -689,7 +504,46 @@ in
           }
         ];
 
-        environment.systemPackages = [ cfg.package ];
+        environment.systemPackages = [ keycloakBuild ];
+
+        services.keycloak.settings =
+          let
+            postgresParams = concatStringsSep "&" (
+              optionals cfg.database.useSSL [
+                "ssl=true"
+              ] ++ optionals (cfg.database.caCert != null) [
+                "sslrootcert=${cfg.database.caCert}"
+                "sslmode=verify-ca"
+              ]
+            );
+            mariadbParams = concatStringsSep "&" ([
+              "characterEncoding=UTF-8"
+            ] ++ optionals cfg.database.useSSL [
+              "useSSL=true"
+              "requireSSL=true"
+              "verifyServerCertificate=true"
+            ] ++ optionals (cfg.database.caCert != null) [
+              "trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}"
+              "trustCertificateKeyStorePassword=notsosecretpassword"
+            ]);
+            dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams;
+          in
+          mkMerge [
+            {
+              db = if cfg.database.type == "postgresql" then "postgres" else cfg.database.type;
+              db-username = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
+              db-password._secret = cfg.database.passwordFile;
+              db-url-host = cfg.database.host;
+              db-url-port = toString cfg.database.port;
+              db-url-database = if databaseActuallyCreateLocally then "keycloak" else cfg.database.name;
+              db-url-properties = prefixUnlessEmpty "?" dbProps;
+              db-url = null;
+            }
+            (mkIf (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
+              https-certificate-file = "/run/keycloak/ssl/ssl_cert";
+              https-certificate-key-file = "/run/keycloak/ssl/ssl_key";
+            })
+          ];
 
         systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL {
           after = [ "postgresql.service" ];
@@ -708,7 +562,7 @@ in
             shopt -s inherit_errexit
 
             create_role="$(mktemp)"
-            trap 'rm -f "$create_role"' ERR EXIT
+            trap 'rm -f "$create_role"' EXIT
 
             db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
             echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" > "$create_role"
@@ -752,41 +606,37 @@ in
                 "mysql.service"
               ]
               else [ ];
+            secretPaths = catAttrs "_secret" (collect isSecret cfg.settings);
+            mkSecretReplacement = file: ''
+              replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf
+            '';
+            secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
           in
           {
             after = databaseServices;
             bindsTo = databaseServices;
             wantedBy = [ "multi-user.target" ];
             path = with pkgs; [
-              cfg.package
+              keycloakBuild
               openssl
               replace-secret
             ];
             environment = {
-              JBOSS_LOG_DIR = "/var/log/keycloak";
-              JBOSS_BASE_DIR = "/run/keycloak";
-              JBOSS_MODULEPATH = "${cfg.package}/modules";
+              KC_HOME_DIR = "/run/keycloak";
+              KC_CONF_DIR = "/run/keycloak/conf";
             };
             serviceConfig = {
-              LoadCredential = [
-                "db_password:${cfg.database.passwordFile}"
-              ] ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
-                "ssl_cert:${cfg.sslCertificate}"
-                "ssl_key:${cfg.sslCertificateKey}"
-              ];
+              LoadCredential =
+                map (p: "${baseNameOf p}:${p}") secretPaths
+                ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
+                  "ssl_cert:${cfg.sslCertificate}"
+                  "ssl_key:${cfg.sslCertificateKey}"
+                ];
               User = "keycloak";
               Group = "keycloak";
               DynamicUser = true;
-              RuntimeDirectory = map (p: "keycloak/" + p) [
-                "configuration"
-                "deployments"
-                "data"
-                "ssl"
-                "log"
-                "tmp"
-              ];
+              RuntimeDirectory = "keycloak";
               RuntimeDirectoryMode = 0700;
-              LogsDirectory = "keycloak";
               AmbientCapabilities = "CAP_NET_BIND_SERVICE";
             };
             script = ''
@@ -795,41 +645,30 @@ in
 
               umask u=rwx,g=,o=
 
-              install_plugin() {
-                if [ -d "$1" ]; then
-                  find "$1" -type f \( -iname \*.ear -o -iname \*.jar \) -exec install -m 0500 -o keycloak -g keycloak "{}" "/run/keycloak/deployments/" \;
-                else
-                  install -m 0500 -o keycloak -g keycloak "$1" "/run/keycloak/deployments/"
-                fi
-              }
-
-              install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
-              install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
-
-              replace-secret '@db-password@' "$CREDENTIALS_DIRECTORY/db_password" /run/keycloak/configuration/standalone.xml
-
-              export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration
-              add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}'
-            ''
-            + lib.optionalString (cfg.plugins != []) (lib.concatStringsSep "\n" (map (pl: "install_plugin ${lib.escapeShellArg pl}") cfg.plugins)) + "\n"
-            + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
-              pushd /run/keycloak/ssl/
-              cat "$CREDENTIALS_DIRECTORY/ssl_cert" <(echo) \
-                  "$CREDENTIALS_DIRECTORY/ssl_key" <(echo) \
-                  /etc/ssl/certs/ca-certificates.crt \
-                  > allcerts.pem
-              openssl pkcs12 -export -in "$CREDENTIALS_DIRECTORY/ssl_cert" -inkey "$CREDENTIALS_DIRECTORY/ssl_key" -chain \
-                             -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \
-                             -CAfile allcerts.pem -passout pass:notsosecretpassword
-              popd
+              ln -s ${themesBundle} /run/keycloak/themes
+              ln -s ${keycloakBuild}/providers /run/keycloak/
+
+              install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf
+
+              ${secretReplacements}
+
+            '' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
+              mkdir -p /run/keycloak/ssl
+              cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
             '' + ''
-              ${cfg.package}/bin/standalone.sh
+              export KEYCLOAK_ADMIN=admin
+              export KEYCLOAK_ADMIN_PASSWORD=${cfg.initialAdminPassword}
+              kc.sh start
             '';
           };
 
         services.postgresql.enable = mkDefault createLocalPostgreSQL;
         services.mysql.enable = mkDefault createLocalMySQL;
-        services.mysql.package = mkIf createLocalMySQL pkgs.mariadb;
+        services.mysql.package =
+          let
+            dbPkg = if cfg.database.type == "mariadb" then pkgs.mariadb else pkgs.mysql80;
+          in
+          mkIf createLocalMySQL (mkDefault dbPkg);
       };
 
   meta.doc = ./keycloak.xml;
diff --git a/nixpkgs/nixos/modules/services/web-apps/keycloak.xml b/nixpkgs/nixos/modules/services/web-apps/keycloak.xml
index cb706932f48f..861756e33ac0 100644
--- a/nixpkgs/nixos/modules/services/web-apps/keycloak.xml
+++ b/nixpkgs/nixos/modules/services/web-apps/keycloak.xml
@@ -27,10 +27,10 @@
 
      <para>
        Refer to the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console">Admin
-       Console section of the Keycloak Server Administration Guide</link> for
-       information on how to administer your
-       <productname>Keycloak</productname> instance.
+       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html">
+       Keycloak Server Administration Guide</link> for information on
+       how to administer your <productname>Keycloak</productname>
+       instance.
      </para>
    </section>
 
@@ -38,27 +38,28 @@
      <title>Database access</title>
      <para>
        <productname>Keycloak</productname> can be used with either
-       <productname>PostgreSQL</productname> or
+       <productname>PostgreSQL</productname>,
+       <productname>MariaDB</productname> or
        <productname>MySQL</productname>. Which one is used can be
        configured in <xref
        linkend="opt-services.keycloak.database.type" />. The selected
        database will automatically be enabled and a database and role
        created unless <xref
-       linkend="opt-services.keycloak.database.host" /> is changed from
-       its default of <literal>localhost</literal> or <xref
-       linkend="opt-services.keycloak.database.createLocally" /> is set
-       to <literal>false</literal>.
+       linkend="opt-services.keycloak.database.host" /> is changed
+       from its default of <literal>localhost</literal> or <xref
+       linkend="opt-services.keycloak.database.createLocally" /> is
+       set to <literal>false</literal>.
      </para>
 
      <para>
        External database access can also be configured by setting
        <xref linkend="opt-services.keycloak.database.host" />, <xref
+       linkend="opt-services.keycloak.database.name" />, <xref
        linkend="opt-services.keycloak.database.username" />, <xref
        linkend="opt-services.keycloak.database.useSSL" /> and <xref
        linkend="opt-services.keycloak.database.caCert" /> as
-       appropriate. Note that you need to manually create a database
-       called <literal>keycloak</literal> and allow the configured
-       database user full access to it.
+       appropriate. Note that you need to manually create the database
+       and allow the configured database user full access to it.
      </para>
 
      <para>
@@ -79,22 +80,27 @@
      </warning>
    </section>
 
-   <section xml:id="module-services-keycloak-frontendurl">
-     <title>Frontend URL</title>
+   <section xml:id="module-services-keycloak-hostname">
+     <title>Hostname</title>
      <para>
-       The frontend URL is used as base for all frontend requests and
-       must be configured through <xref linkend="opt-services.keycloak.frontendUrl" />.
-       It should normally include a trailing <literal>/auth</literal>
-       (the default web context). If you use a reverse proxy, you need
-       to set this option to <literal>""</literal>, so that frontend URL
-       is derived from HTTP headers. <literal>X-Forwarded-*</literal> headers
-       support also should be enabled, using <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html#identifying-client-ip-addresses">
-       respective guidelines</link>.
+       The hostname is used to build the public URL used as base for
+       all frontend requests and must be configured through <xref
+       linkend="opt-services.keycloak.settings.hostname" />.
      </para>
 
+     <note>
+       <para>
+         If you're migrating an old Wildfly based Keycloak instance
+         and want to keep compatibility with your current clients,
+         you'll likely want to set <xref
+         linkend="opt-services.keycloak.settings.http-relative-path"
+         /> to <literal>/auth</literal>. See the option description
+         for more details.
+       </para>
+     </note>
+
      <para>
-       <xref linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl" />
+       <xref linkend="opt-services.keycloak.settings.hostname-strict-backchannel" />
        determines whether Keycloak should force all requests to go
        through the frontend URL. By default,
        <productname>Keycloak</productname> allows backend requests to
@@ -104,10 +110,10 @@
      </para>
 
      <para>
-       See the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">Hostname
-       section of the Keycloak Server Installation and Configuration
-       Guide</link> for more information.
+        For more information on hostname configuration, see the <link
+        xlink:href="https://www.keycloak.org/server/hostname">Hostname
+        section of the Keycloak Server Installation and Configuration
+        Guide</link>.
      </para>
    </section>
 
@@ -139,68 +145,40 @@
    <section xml:id="module-services-keycloak-themes">
      <title>Themes</title>
      <para>
-        You can package custom themes and make them visible to Keycloak via
-        <xref linkend="opt-services.keycloak.themes" />
-        option. See the <link xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
+        You can package custom themes and make them visible to
+        Keycloak through <xref linkend="opt-services.keycloak.themes"
+        />. See the <link
+        xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
         Themes section of the Keycloak Server Development Guide</link>
-        and respective NixOS option description for more information.
+        and the description of the aforementioned NixOS option for
+        more information.
      </para>
    </section>
 
-   <section xml:id="module-services-keycloak-extra-config">
-     <title>Additional configuration</title>
+   <section xml:id="module-services-keycloak-settings">
+     <title>Configuration file settings</title>
      <para>
-       Additional Keycloak configuration options, for which no
-       explicit <productname>NixOS</productname> options are provided,
-       can be set in <xref linkend="opt-services.keycloak.extraConfig" />.
+       Keycloak server configuration parameters can be set in <xref
+       linkend="opt-services.keycloak.settings" />. These correspond
+       directly to options in
+       <filename>conf/keycloak.conf</filename>. Some of the most
+       important parameters are documented as suboptions, the rest can
+       be found in the <link
+       xlink:href="https://www.keycloak.org/server/all-config">All
+       configuration section of the Keycloak Server Installation and
+       Configuration Guide</link>.
      </para>
 
      <para>
-       Options are expressed as a Nix attribute set which matches the
-       structure of the jboss-cli configuration. The configuration is
-       effectively overlayed on top of the default configuration
-       shipped with Keycloak. To remove existing nodes and undefine
-       attributes from the default configuration, set them to
-       <literal>null</literal>.
-     </para>
-     <para>
-       For example, the following script, which removes the hostname
-       provider <literal>default</literal>, adds the deprecated
-       hostname provider <literal>fixed</literal> and defines it the
-       default:
-
-<programlisting>
-/subsystem=keycloak-server/spi=hostname/provider=default:remove()
-/subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
-/subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
-</programlisting>
-
-       would be expressed as
-
-<programlisting>
-services.keycloak.extraConfig = {
-  "subsystem=keycloak-server" = {
-    "spi=hostname" = {
-      "provider=default" = null;
-      "provider=fixed" = {
-        enabled = true;
-        properties.hostname = "keycloak.example.com";
-      };
-      default-provider = "fixed";
-    };
-  };
-};
-</programlisting>
-     </para>
-     <para>
-       You can discover available options by using the <link
-       xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
-       program and by referring to the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
-       Server Installation and Configuration Guide</link>.
+       Options containing secret data should be set to an attribute
+       set containing the attribute <literal>_secret</literal> - a
+       string pointing to a file containing the value the option
+       should be set to. See the description of <xref
+       linkend="opt-services.keycloak.settings" /> for an example.
      </para>
    </section>
 
+
    <section xml:id="module-services-keycloak-example-config">
      <title>Example configuration</title>
      <para>
@@ -208,9 +186,11 @@ services.keycloak.extraConfig = {
 <programlisting>
 services.keycloak = {
   <link linkend="opt-services.keycloak.enable">enable</link> = true;
+  settings = {
+    <link linkend="opt-services.keycloak.settings.hostname">hostname</link> = "keycloak.example.com";
+    <link linkend="opt-services.keycloak.settings.hostname-strict-backchannel">hostname-strict-backchannel</link> = true;
+  };
   <link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl";  # change on first login
-  <link linkend="opt-services.keycloak.frontendUrl">frontendUrl</link> = "https://keycloak.example.com/auth";
-  <link linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl">forceBackendUrlToFrontendUrl</link> = true;
   <link linkend="opt-services.keycloak.sslCertificate">sslCertificate</link> = "/run/keys/ssl_cert";
   <link linkend="opt-services.keycloak.sslCertificateKey">sslCertificateKey</link> = "/run/keys/ssl_key";
   <link linkend="opt-services.keycloak.database.passwordFile">database.passwordFile</link> = "/run/keys/db_password";
diff --git a/nixpkgs/nixos/modules/services/web-apps/komga.nix b/nixpkgs/nixos/modules/services/web-apps/komga.nix
new file mode 100644
index 000000000000..a2809e64a9c3
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/web-apps/komga.nix
@@ -0,0 +1,99 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.komga;
+
+in {
+  options = {
+    services.komga = {
+      enable = mkEnableOption "Komga, a free and open source comics/mangas media server";
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = lib.mdDoc ''
+          The port that Komga will listen on.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "komga";
+        description = lib.mdDoc ''
+          User account under which Komga runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "komga";
+        description = lib.mdDoc ''
+          Group under which Komga runs.
+        '';
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/komga";
+        description = lib.mdDoc ''
+          State and configuration directory Komga will use.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          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 ];
+
+    users.groups = mkIf (cfg.group == "komga") {
+      komga = {};
+    };
+
+    users.users = mkIf (cfg.user == "komga") {
+      komga = {
+        group = cfg.group;
+        home = cfg.stateDir;
+        description = "Komga Daemon user";
+        isSystemUser = true;
+      };
+    };
+
+    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 = "${pkgs.komga}/bin/komga";
+
+        StateDirectory = mkIf (cfg.stateDir == "/var/lib/komga") "komga";
+      };
+
+    };
+  };
+
+  meta.maintainers = with maintainers; [ govanify ];
+}
diff --git a/nixpkgs/nixos/modules/services/web-apps/lemmy.nix b/nixpkgs/nixos/modules/services/web-apps/lemmy.nix
index 7cd2357c4556..3e726149e93d 100644
--- a/nixpkgs/nixos/modules/services/web-apps/lemmy.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/lemmy.nix
@@ -16,14 +16,14 @@ in
 
     jwtSecretPath = mkOption {
       type = types.path;
-      description = "Path to read the jwt secret from.";
+      description = lib.mdDoc "Path to read the jwt secret from.";
     };
 
     ui = {
       port = mkOption {
         type = types.port;
         default = 1234;
-        description = "Port where lemmy-ui should listen for incoming requests.";
+        description = lib.mdDoc "Port where lemmy-ui should listen for incoming requests.";
       };
     };
 
@@ -31,7 +31,7 @@ in
 
     settings = mkOption {
       default = { };
-      description = "Lemmy configuration";
+      description = lib.mdDoc "Lemmy configuration";
 
       type = types.submodule {
         freeformType = settingsFormat.type;
@@ -39,13 +39,13 @@ in
         options.hostname = mkOption {
           type = types.str;
           default = null;
-          description = "The domain name of your instance (eg 'lemmy.ml').";
+          description = lib.mdDoc "The domain name of your instance (eg 'lemmy.ml').";
         };
 
         options.port = mkOption {
           type = types.port;
           default = 8536;
-          description = "Port where lemmy should listen for incoming requests.";
+          description = lib.mdDoc "Port where lemmy should listen for incoming requests.";
         };
 
         options.federation = {
@@ -56,12 +56,12 @@ in
           enabled = mkOption {
             type = types.bool;
             default = true;
-            description = "Enable Captcha.";
+            description = lib.mdDoc "Enable Captcha.";
           };
           difficulty = mkOption {
             type = types.enum [ "easy" "medium" "hard" ];
             default = "medium";
-            description = "The difficultly of the captcha to solve.";
+            description = lib.mdDoc "The difficultly of the captcha to solve.";
           };
         };
 
@@ -164,7 +164,7 @@ in
 
         wantedBy = [ "multi-user.target" ];
 
-        after = [ "pict-rs.service " ] ++ lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
+        after = [ "pict-rs.service" ] ++ lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
 
         requires = lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/limesurvey.nix b/nixpkgs/nixos/modules/services/web-apps/limesurvey.nix
index 5ccd742a303b..e0995e0b5a44 100644
--- a/nixpkgs/nixos/modules/services/web-apps/limesurvey.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/limesurvey.nix
@@ -39,41 +39,41 @@ in
         type = types.enum [ "mysql" "pgsql" "odbc" "mssql" ];
         example = "pgsql";
         default = "mysql";
-        description = "Database engine to use.";
+        description = lib.mdDoc "Database engine to use.";
       };
 
       host = mkOption {
         type = types.str;
         default = "localhost";
-        description = "Database host address.";
+        description = lib.mdDoc "Database host address.";
       };
 
       port = mkOption {
         type = types.int;
         default = if cfg.database.type == "pgsql" then 5442 else 3306;
         defaultText = literalExpression "3306";
-        description = "Database host port.";
+        description = lib.mdDoc "Database host port.";
       };
 
       name = mkOption {
         type = types.str;
         default = "limesurvey";
-        description = "Database name.";
+        description = lib.mdDoc "Database name.";
       };
 
       user = mkOption {
         type = types.str;
         default = "limesurvey";
-        description = "Database user.";
+        description = lib.mdDoc "Database user.";
       };
 
       passwordFile = mkOption {
         type = types.nullOr types.path;
         default = null;
         example = "/run/keys/limesurvey-dbpassword";
-        description = ''
+        description = lib.mdDoc ''
           A file containing the password corresponding to
-          <option>database.user</option>.
+          {option}`database.user`.
         '';
       };
 
@@ -85,14 +85,14 @@ in
           else null
         ;
         defaultText = literalExpression "/run/mysqld/mysqld.sock";
-        description = "Path to the unix socket file to use for authentication.";
+        description = lib.mdDoc "Path to the unix socket file to use for authentication.";
       };
 
       createLocally = mkOption {
         type = types.bool;
         default = cfg.database.type == "mysql";
         defaultText = literalExpression "true";
-        description = ''
+        description = lib.mdDoc ''
           Create the database and database user locally.
           This currently only applies if database type "mysql" is selected.
         '';
@@ -109,9 +109,9 @@ in
           enableACME = true;
         }
       '';
-      description = ''
-        Apache configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
-        See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+      description = lib.mdDoc ''
+        Apache configuration can be done by adapting `services.httpd.virtualHosts.<name>`.
+        See [](#opt-services.httpd.virtualHosts) for further information.
       '';
     };
 
@@ -125,8 +125,8 @@ in
         "pm.max_spare_servers" = 4;
         "pm.max_requests" = 500;
       };
-      description = ''
-        Options for the LimeSurvey PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+      description = lib.mdDoc ''
+        Options for the LimeSurvey PHP pool. See the documentation on `php-fpm.conf`
         for details on configuration directives.
       '';
     };
@@ -134,9 +134,9 @@ in
     config = mkOption {
       type = configType;
       default = {};
-      description = ''
+      description = lib.mdDoc ''
         LimeSurvey configuration. Refer to
-        <link xlink:href="https://manual.limesurvey.org/Optional_settings"/>
+        <https://manual.limesurvey.org/Optional_settings>
         for details on supported values.
       '';
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/mastodon.nix b/nixpkgs/nixos/modules/services/web-apps/mastodon.nix
index 8208c85bfd70..d0594ff74192 100644
--- a/nixpkgs/nixos/modules/services/web-apps/mastodon.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/mastodon.nix
@@ -9,6 +9,8 @@ let
     RAILS_ENV = "production";
     NODE_ENV = "production";
 
+    LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
+
     # mastodon-web concurrency.
     WEB_CONCURRENCY = toString cfg.webProcesses;
     MAX_THREADS = toString cfg.webThreads;
@@ -111,17 +113,17 @@ in {
           affect other virtualHosts running on your nginx instance, if any.
           Alternatively you can configure a reverse-proxy of your choice to serve these paths:
 
-          <code>/ -> $(nix-instantiate --eval '&lt;nixpkgs&gt;' -A mastodon.outPath)/public</code>
+          <literal>/ -> $(nix-instantiate --eval '&lt;nixpkgs&gt;' -A mastodon.outPath)/public</literal>
 
-          <code>/ -> 127.0.0.1:{{ webPort }} </code>(If there was no file in the directory above.)
+          <literal>/ -> 127.0.0.1:{{ webPort }} </literal>(If there was no file in the directory above.)
 
-          <code>/system/ -> /var/lib/mastodon/public-system/</code>
+          <literal>/system/ -> /var/lib/mastodon/public-system/</literal>
 
-          <code>/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}</code>
+          <literal>/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}</literal>
 
           Make sure that websockets are forwarded properly. You might want to set up caching
           of some requests. Take a look at mastodon's provided nginx configuration at
-          <code>https://github.com/tootsuite/mastodon/blob/master/dist/nginx.conf</code>.
+          <literal>https://github.com/mastodon/mastodon/blob/master/dist/nginx.conf</literal>.
         '';
         type = lib.types.bool;
         default = false;
@@ -133,20 +135,20 @@ in {
           that user will be created, otherwise it should be set to the
           name of a user created elsewhere.  In both cases,
           <package>mastodon</package> and a package containing only
-          the shell script <code>mastodon-env</code> will be added to
+          the shell script <literal>mastodon-env</literal> will be added to
           the user's package set. To run a command from
-          <package>mastodon</package> such as <code>tootctl</code>
+          <package>mastodon</package> such as <literal>tootctl</literal>
           with the environment configured by this module use
-          <code>mastodon-env</code>, as in:
+          <literal>mastodon-env</literal>, as in:
 
-          <code>mastodon-env tootctl accounts create newuser --email newuser@example.com</code>
+          <literal>mastodon-env tootctl accounts create newuser --email newuser@example.com</literal>
         '';
         type = lib.types.str;
         default = "mastodon";
       };
 
       group = lib.mkOption {
-        description = ''
+        description = lib.mdDoc ''
           Group under which mastodon runs.
         '';
         type = lib.types.str;
@@ -154,12 +156,12 @@ in {
       };
 
       streamingPort = lib.mkOption {
-        description = "TCP port used by the mastodon-streaming service.";
+        description = lib.mdDoc "TCP port used by the mastodon-streaming service.";
         type = lib.types.port;
         default = 55000;
       };
       streamingProcesses = lib.mkOption {
-        description = ''
+        description = lib.mdDoc ''
           Processes used by the mastodon-streaming service.
           Defaults to the number of CPU cores minus one.
         '';
@@ -168,41 +170,41 @@ in {
       };
 
       webPort = lib.mkOption {
-        description = "TCP port used by the mastodon-web service.";
+        description = lib.mdDoc "TCP port used by the mastodon-web service.";
         type = lib.types.port;
         default = 55001;
       };
       webProcesses = lib.mkOption {
-        description = "Processes used by the mastodon-web service.";
+        description = lib.mdDoc "Processes used by the mastodon-web service.";
         type = lib.types.int;
         default = 2;
       };
       webThreads = lib.mkOption {
-        description = "Threads per process used by the mastodon-web service.";
+        description = lib.mdDoc "Threads per process used by the mastodon-web service.";
         type = lib.types.int;
         default = 5;
       };
 
       sidekiqPort = lib.mkOption {
-        description = "TCP port used by the mastodon-sidekiq service.";
+        description = lib.mdDoc "TCP port used by the mastodon-sidekiq service.";
         type = lib.types.port;
         default = 55002;
       };
       sidekiqThreads = lib.mkOption {
-        description = "Worker threads used by the mastodon-sidekiq service.";
+        description = lib.mdDoc "Worker threads used by the mastodon-sidekiq service.";
         type = lib.types.int;
         default = 25;
       };
 
       vapidPublicKeyFile = lib.mkOption {
-        description = ''
+        description = lib.mdDoc ''
           Path to file containing the public key used for Web Push
           Voluntary Application Server Identification.  A new keypair can
           be generated by running:
 
-          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code>
+          `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys`
 
-          If <option>mastodon.vapidPrivateKeyFile</option>does not
+          If {option}`mastodon.vapidPrivateKeyFile`does not
           exist, it and this file will be created with a new keypair.
         '';
         default = "/var/lib/mastodon/secrets/vapid-public-key";
@@ -210,17 +212,17 @@ in {
       };
 
       localDomain = lib.mkOption {
-        description = "The domain serving your Mastodon instance.";
+        description = lib.mdDoc "The domain serving your Mastodon instance.";
         example = "social.example.org";
         type = lib.types.str;
       };
 
       secretKeyBaseFile = lib.mkOption {
-        description = ''
+        description = lib.mdDoc ''
           Path to file containing the secret key base.
           A new secret key base can be generated by running:
 
-          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code>
+          `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake secret`
 
           If this file does not exist, it will be created with a new secret key base.
         '';
@@ -229,11 +231,11 @@ in {
       };
 
       otpSecretFile = lib.mkOption {
-        description = ''
+        description = lib.mdDoc ''
           Path to file containing the OTP secret.
           A new OTP secret can be generated by running:
 
-          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code>
+          `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake secret`
 
           If this file does not exist, it will be created with a new OTP secret.
         '';
@@ -242,12 +244,12 @@ in {
       };
 
       vapidPrivateKeyFile = lib.mkOption {
-        description = ''
+        description = lib.mdDoc ''
           Path to file containing the private key used for Web Push
           Voluntary Application Server Identification.  A new keypair can
           be generated by running:
 
-          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code>
+          `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys`
 
           If this file does not exist, it will be created with a new
           private key.
@@ -257,7 +259,7 @@ in {
       };
 
       trustedProxy = lib.mkOption {
-        description = ''
+        description = lib.mdDoc ''
           You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
           otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
           bad because IP addresses are used for important rate limits and security functions.
@@ -267,7 +269,7 @@ in {
       };
 
       enableUnixSocket = lib.mkOption {
-        description = ''
+        description = lib.mdDoc ''
           Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
           is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
           processes and streaming API (Node.js) processes.
@@ -278,27 +280,27 @@ in {
 
       redis = {
         createLocally = lib.mkOption {
-          description = "Configure local Redis server for Mastodon.";
+          description = lib.mdDoc "Configure local Redis server for Mastodon.";
           type = lib.types.bool;
           default = true;
         };
 
         host = lib.mkOption {
-          description = "Redis host.";
+          description = lib.mdDoc "Redis host.";
           type = lib.types.str;
           default = "127.0.0.1";
         };
 
         port = lib.mkOption {
-          description = "Redis port.";
+          description = lib.mdDoc "Redis port.";
           type = lib.types.port;
-          default = 6379;
+          default = 31637;
         };
       };
 
       database = {
         createLocally = lib.mkOption {
-          description = "Configure local PostgreSQL database server for Mastodon.";
+          description = lib.mdDoc "Configure local PostgreSQL database server for Mastodon.";
           type = lib.types.bool;
           default = true;
         };
@@ -307,75 +309,75 @@ in {
           type = lib.types.str;
           default = "/run/postgresql";
           example = "192.168.23.42";
-          description = "Database host address or unix socket.";
+          description = lib.mdDoc "Database host address or unix socket.";
         };
 
         port = lib.mkOption {
           type = lib.types.int;
           default = 5432;
-          description = "Database host port.";
+          description = lib.mdDoc "Database host port.";
         };
 
         name = lib.mkOption {
           type = lib.types.str;
           default = "mastodon";
-          description = "Database name.";
+          description = lib.mdDoc "Database name.";
         };
 
         user = lib.mkOption {
           type = lib.types.str;
           default = "mastodon";
-          description = "Database user.";
+          description = lib.mdDoc "Database user.";
         };
 
         passwordFile = lib.mkOption {
           type = lib.types.nullOr lib.types.path;
           default = "/var/lib/mastodon/secrets/db-password";
           example = "/run/keys/mastodon-db-password";
-          description = ''
+          description = lib.mdDoc ''
             A file containing the password corresponding to
-            <option>database.user</option>.
+            {option}`database.user`.
           '';
         };
       };
 
       smtp = {
         createLocally = lib.mkOption {
-          description = "Configure local Postfix SMTP server for Mastodon.";
+          description = lib.mdDoc "Configure local Postfix SMTP server for Mastodon.";
           type = lib.types.bool;
           default = true;
         };
 
         authenticate = lib.mkOption {
-          description = "Authenticate with the SMTP server using username and password.";
+          description = lib.mdDoc "Authenticate with the SMTP server using username and password.";
           type = lib.types.bool;
           default = false;
         };
 
         host = lib.mkOption {
-          description = "SMTP host used when sending emails to users.";
+          description = lib.mdDoc "SMTP host used when sending emails to users.";
           type = lib.types.str;
           default = "127.0.0.1";
         };
 
         port = lib.mkOption {
-          description = "SMTP port used when sending emails to users.";
+          description = lib.mdDoc "SMTP port used when sending emails to users.";
           type = lib.types.port;
           default = 25;
         };
 
         fromAddress = lib.mkOption {
-          description = ''"From" address used when sending Emails to users.'';
+          description = lib.mdDoc ''"From" address used when sending Emails to users.'';
           type = lib.types.str;
         };
 
         user = lib.mkOption {
-          description = "SMTP login name.";
+          description = lib.mdDoc "SMTP login name.";
           type = lib.types.str;
         };
 
         passwordFile = lib.mkOption {
-          description = ''
+          description = lib.mdDoc ''
             Path to file containing the SMTP password.
           '';
           default = "/var/lib/mastodon/secrets/smtp-password";
@@ -386,7 +388,7 @@ in {
 
       elasticsearch = {
         host = lib.mkOption {
-          description = ''
+          description = lib.mdDoc ''
             Elasticsearch host.
             If it is not null, Elasticsearch full text search will be enabled.
           '';
@@ -395,7 +397,7 @@ in {
         };
 
         port = lib.mkOption {
-          description = "Elasticsearch port.";
+          description = lib.mdDoc "Elasticsearch port.";
           type = lib.types.port;
           default = 9200;
         };
@@ -405,13 +407,13 @@ in {
         type = lib.types.package;
         default = pkgs.mastodon;
         defaultText = lib.literalExpression "pkgs.mastodon";
-        description = "Mastodon package to use.";
+        description = lib.mdDoc "Mastodon package to use.";
       };
 
       extraConfig = lib.mkOption {
         type = lib.types.attrs;
         default = {};
-        description = ''
+        description = lib.mdDoc ''
           Extra environment variables to pass to all mastodon services.
         '';
       };
@@ -419,7 +421,7 @@ in {
       automaticMigrations = lib.mkOption {
         type = lib.types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Do automatic database migrations.
         '';
       };
@@ -603,8 +605,10 @@ in {
       enable = true;
       hostname = lib.mkDefault "${cfg.localDomain}";
     };
-    services.redis = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") {
+    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.postgresql = lib.mkIf databaseActuallyCreateLocally {
       enable = true;
diff --git a/nixpkgs/nixos/modules/services/web-apps/matomo.nix b/nixpkgs/nixos/modules/services/web-apps/matomo.nix
index c6d4ed6d39de..80c4db1263e1 100644
--- a/nixpkgs/nixos/modules/services/web-apps/matomo.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/matomo.nix
@@ -32,7 +32,7 @@ in {
       enable = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Enable Matomo web analytics with php-fpm backend.
           Either the nginx option or the webServerUser option is mandatory.
         '';
@@ -40,7 +40,7 @@ in {
 
       package = mkOption {
         type = types.package;
-        description = ''
+        description = lib.mdDoc ''
           Matomo package for the service to use.
           This can be used to point to newer releases from nixos-unstable,
           as they don't get backported if they are not security-relevant.
@@ -64,13 +64,13 @@ in {
       periodicArchiveProcessing = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Enable periodic archive processing, which generates aggregated reports from the visits.
 
           This means that you can safely disable browser triggers for Matomo archiving,
           and safely enable to delete old visitor logs.
           Before deleting visitor logs,
-          make sure though that you run <literal>systemctl start matomo-archive-processing.service</literal>
+          make sure though that you run `systemctl start matomo-archive-processing.service`
           at least once without errors if you have already collected data before.
         '';
       };
@@ -84,7 +84,7 @@ in {
           else "${user}.''${config.${options.networking.hostName}}"
         '';
         example = "matomo.yourdomain.org";
-        description = ''
+        description = lib.mdDoc ''
           URL of the host, without https prefix. You may want to change it if you
           run Matomo on a different URL than matomo.yourdomain.
         '';
@@ -112,12 +112,12 @@ in {
             enableACME = false;
           }
         '';
-        description = ''
+        description = lib.mdDoc ''
             With this option, you can customize an nginx virtualHost which already has sensible defaults for Matomo.
             Either this option or the webServerUser option is mandatory.
             Set this to {} to just enable the virtualHost if you don't need any customization.
-            If enabled, then by default, the <option>serverName</option> is
-            <literal>''${user}.''${config.networking.hostName}.''${config.networking.domain}</literal>,
+            If enabled, then by default, the {option}`serverName` is
+            `''${user}.''${config.networking.hostName}.''${config.networking.domain}`,
             SSL is active, and certificates are acquired via ACME.
             If this is set to null (the default), no nginx virtualHost will be configured.
         '';
diff --git a/nixpkgs/nixos/modules/services/web-apps/mattermost.nix b/nixpkgs/nixos/modules/services/web-apps/mattermost.nix
index 2901f307dc5a..6e9e2abcaa8c 100644
--- a/nixpkgs/nixos/modules/services/web-apps/mattermost.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/mattermost.nix
@@ -107,19 +107,19 @@ in
         type = types.package;
         default = pkgs.mattermost;
         defaultText = "pkgs.mattermost";
-        description = "Mattermost derivation to use.";
+        description = lib.mdDoc "Mattermost derivation to use.";
       };
 
       statePath = mkOption {
         type = types.str;
         default = "/var/lib/mattermost";
-        description = "Mattermost working directory";
+        description = lib.mdDoc "Mattermost working directory";
       };
 
       siteUrl = mkOption {
         type = types.str;
         example = "https://chat.example.com";
-        description = ''
+        description = lib.mdDoc ''
           URL this Mattermost instance is reachable under, without trailing slash.
         '';
       };
@@ -127,14 +127,14 @@ in
       siteName = mkOption {
         type = types.str;
         default = "Mattermost";
-        description = "Name of this Mattermost site.";
+        description = lib.mdDoc "Name of this Mattermost site.";
       };
 
       listenAddress = mkOption {
         type = types.str;
         default = ":8065";
         example = "[::1]:8065";
-        description = ''
+        description = lib.mdDoc ''
           Address and port this Mattermost instance listens to.
         '';
       };
@@ -142,7 +142,7 @@ in
       mutableConfig = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Whether the Mattermost config.json is writeable by Mattermost.
 
           Most of the settings can be edited in the system console of
@@ -159,7 +159,7 @@ in
       preferNixConfig = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           If both mutableConfig and this option are set, the Nix configuration
           will take precedence over any settings configured in the server
           console.
@@ -169,7 +169,7 @@ in
       extraConfig = mkOption {
         type = types.attrs;
         default = { };
-        description = ''
+        description = lib.mdDoc ''
           Addtional configuration options as Nix attribute set in config.json schema.
         '';
       };
@@ -178,7 +178,7 @@ in
         type = types.listOf (types.oneOf [types.path types.package]);
         default = [];
         example = "[ ./com.github.moussetc.mattermost.plugin.giphy-2.0.0.tar.gz ]";
-        description = ''
+        description = lib.mdDoc ''
           Plugins to add to the configuration. Overrides any installed if non-null.
           This is a list of paths to .tar.gz files or derivations evaluating to
           .tar.gz files.
@@ -188,7 +188,7 @@ in
       localDatabaseCreate = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Create a local PostgreSQL database for Mattermost automatically.
         '';
       };
@@ -196,7 +196,7 @@ in
       localDatabaseName = mkOption {
         type = types.str;
         default = "mattermost";
-        description = ''
+        description = lib.mdDoc ''
           Local Mattermost database name.
         '';
       };
@@ -204,7 +204,7 @@ in
       localDatabaseUser = mkOption {
         type = types.str;
         default = "mattermost";
-        description = ''
+        description = lib.mdDoc ''
           Local Mattermost database username.
         '';
       };
@@ -212,7 +212,7 @@ in
       localDatabasePassword = mkOption {
         type = types.str;
         default = "mmpgsecret";
-        description = ''
+        description = lib.mdDoc ''
           Password for local Mattermost database user.
         '';
       };
@@ -220,7 +220,7 @@ in
       user = mkOption {
         type = types.str;
         default = "mattermost";
-        description = ''
+        description = lib.mdDoc ''
           User which runs the Mattermost service.
         '';
       };
@@ -228,7 +228,7 @@ in
       group = mkOption {
         type = types.str;
         default = "mattermost";
-        description = ''
+        description = lib.mdDoc ''
           Group which runs the Mattermost service.
         '';
       };
@@ -239,13 +239,13 @@ in
           type = types.package;
           default = pkgs.matterircd;
           defaultText = "pkgs.matterircd";
-          description = "matterircd derivation to use.";
+          description = lib.mdDoc "matterircd derivation to use.";
         };
         parameters = mkOption {
           type = types.listOf types.str;
           default = [ ];
           example = [ "-mmserver chat.example.com" "-bind [::]:6667" ];
-          description = ''
+          description = lib.mdDoc ''
             Set commandline parameters to pass to matterircd. See
             https://github.com/42wim/matterircd#usage for more information.
           '';
diff --git a/nixpkgs/nixos/modules/services/web-apps/mediawiki.nix b/nixpkgs/nixos/modules/services/web-apps/mediawiki.nix
index 977b6f60b230..01083eff612b 100644
--- a/nixpkgs/nixos/modules/services/web-apps/mediawiki.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/mediawiki.nix
@@ -177,20 +177,20 @@ in
         type = types.package;
         default = pkgs.mediawiki;
         defaultText = literalExpression "pkgs.mediawiki";
-        description = "Which MediaWiki package to use.";
+        description = lib.mdDoc "Which MediaWiki package to use.";
       };
 
       name = mkOption {
         type = types.str;
         default = "MediaWiki";
         example = "Foobar Wiki";
-        description = "Name of the wiki.";
+        description = lib.mdDoc "Name of the wiki.";
       };
 
       uploadsDir = mkOption {
         type = types.nullOr types.path;
         default = "${stateDir}/uploads";
-        description = ''
+        description = lib.mdDoc ''
           This directory is used for uploads of pictures. The directory passed here is automatically
           created and permissions adjusted as required.
         '';
@@ -198,15 +198,15 @@ in
 
       passwordFile = mkOption {
         type = types.path;
-        description = "A file containing the initial password for the admin user.";
+        description = lib.mdDoc "A file containing the initial password for the admin user.";
         example = "/run/keys/mediawiki-password";
       };
 
       skins = mkOption {
         default = {};
         type = types.attrsOf types.path;
-        description = ''
-          Attribute set of paths whose content is copied to the <filename>skins</filename>
+        description = lib.mdDoc ''
+          Attribute set of paths whose content is copied to the {file}`skins`
           subdirectory of the MediaWiki installation in addition to the default skins.
         '';
       };
@@ -214,11 +214,11 @@ in
       extensions = mkOption {
         default = {};
         type = types.attrsOf (types.nullOr types.path);
-        description = ''
-          Attribute set of paths whose content is copied to the <filename>extensions</filename>
+        description = lib.mdDoc ''
+          Attribute set of paths whose content is copied to the {file}`extensions`
           subdirectory of the MediaWiki installation and enabled in configuration.
 
-          Use <literal>null</literal> instead of path to enable extensions that are part of MediaWiki.
+          Use `null` instead of path to enable extensions that are part of MediaWiki.
         '';
         example = literalExpression ''
           {
@@ -235,52 +235,52 @@ in
         type = mkOption {
           type = types.enum [ "mysql" "postgres" "sqlite" "mssql" "oracle" ];
           default = "mysql";
-          description = "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
+          description = lib.mdDoc "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
         };
 
         host = mkOption {
           type = types.str;
           default = "localhost";
-          description = "Database host address.";
+          description = lib.mdDoc "Database host address.";
         };
 
         port = mkOption {
           type = types.port;
           default = 3306;
-          description = "Database host port.";
+          description = lib.mdDoc "Database host port.";
         };
 
         name = mkOption {
           type = types.str;
           default = "mediawiki";
-          description = "Database name.";
+          description = lib.mdDoc "Database name.";
         };
 
         user = mkOption {
           type = types.str;
           default = "mediawiki";
-          description = "Database user.";
+          description = lib.mdDoc "Database user.";
         };
 
         passwordFile = mkOption {
           type = types.nullOr types.path;
           default = null;
           example = "/run/keys/mediawiki-dbpassword";
-          description = ''
+          description = lib.mdDoc ''
             A file containing the password corresponding to
-            <option>database.user</option>.
+            {option}`database.user`.
           '';
         };
 
         tablePrefix = mkOption {
           type = types.nullOr types.str;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             If you only have access to a single database and wish to install more than
             one version of MediaWiki, or have other applications that also use the
             database, you can give the table names a unique prefix to stop any naming
             conflicts or confusion.
-            See <link xlink:href='https://www.mediawiki.org/wiki/Manual:$wgDBprefix'/>.
+            See <https://www.mediawiki.org/wiki/Manual:$wgDBprefix>.
           '';
         };
 
@@ -288,14 +288,14 @@ in
           type = types.nullOr types.path;
           default = if cfg.database.createLocally then "/run/mysqld/mysqld.sock" else null;
           defaultText = literalExpression "/run/mysqld/mysqld.sock";
-          description = "Path to the unix socket file to use for authentication.";
+          description = lib.mdDoc "Path to the unix socket file to use for authentication.";
         };
 
         createLocally = mkOption {
           type = types.bool;
           default = cfg.database.type == "mysql";
           defaultText = literalExpression "true";
-          description = ''
+          description = lib.mdDoc ''
             Create the database and database user locally.
             This currently only applies if database type "mysql" is selected.
           '';
@@ -312,9 +312,9 @@ in
             enableACME = true;
           }
         '';
-        description = ''
-          Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
-          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+        description = lib.mdDoc ''
+          Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
+          See [](#opt-services.httpd.virtualHosts) for further information.
         '';
       };
 
@@ -328,18 +328,18 @@ in
           "pm.max_spare_servers" = 4;
           "pm.max_requests" = 500;
         };
-        description = ''
-          Options for the MediaWiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+        description = lib.mdDoc ''
+          Options for the MediaWiki PHP pool. See the documentation on `php-fpm.conf`
           for details on configuration directives.
         '';
       };
 
       extraConfig = mkOption {
         type = types.lines;
-        description = ''
+        description = lib.mdDoc ''
           Any additional text to be appended to MediaWiki's
           LocalSettings.php configuration file. For configuration
-          settings, see <link xlink:href="https://www.mediawiki.org/wiki/Manual:Configuration_settings"/>.
+          settings, see <https://www.mediawiki.org/wiki/Manual:Configuration_settings>.
         '';
         default = "";
         example = ''
diff --git a/nixpkgs/nixos/modules/services/web-apps/miniflux.nix b/nixpkgs/nixos/modules/services/web-apps/miniflux.nix
index 641c9be85d8c..55e3664bee68 100644
--- a/nixpkgs/nixos/modules/services/web-apps/miniflux.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/miniflux.nix
@@ -29,9 +29,9 @@ in
             LISTEN_ADDR = "localhost:8080";
           }
         '';
-        description = ''
+        description = lib.mdDoc ''
           Configuration for Miniflux, refer to
-          <link xlink:href="https://miniflux.app/docs/configuration.html"/>
+          <https://miniflux.app/docs/configuration.html>
           for documentation on the supported values.
 
           Correct configuration for the database is already provided.
@@ -41,7 +41,7 @@ in
 
       adminCredentialsFile = mkOption  {
         type = types.path;
-        description = ''
+        description = lib.mdDoc ''
           File containing the ADMIN_USERNAME and
           ADMIN_PASSWORD (length >= 6) in the format of
           an EnvironmentFile=, as described by systemd.exec(5).
diff --git a/nixpkgs/nixos/modules/services/web-apps/moodle.nix b/nixpkgs/nixos/modules/services/web-apps/moodle.nix
index 19f3e754691e..6d1a9839ca1f 100644
--- a/nixpkgs/nixos/modules/services/web-apps/moodle.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/moodle.nix
@@ -56,7 +56,7 @@ let
   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
 
-  phpExt = pkgs.php74.withExtensions
+  phpExt = pkgs.php81.withExtensions
         ({ enabled, all }: with all; [ iconv mbstring curl openssl tokenizer xmlrpc soap ctype zip gd simplexml dom  intl json sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache ]);
 in
 {
@@ -68,13 +68,13 @@ in
       type = types.package;
       default = pkgs.moodle;
       defaultText = literalExpression "pkgs.moodle";
-      description = "The Moodle package to use.";
+      description = lib.mdDoc "The Moodle package to use.";
     };
 
     initialPassword = mkOption {
       type = types.str;
       example = "correcthorsebatterystaple";
-      description = ''
+      description = lib.mdDoc ''
         Specifies the initial password for the admin, i.e. the password assigned if the user does not already exist.
         The password specified here is world-readable in the Nix store, so it should be changed promptly.
       '';
@@ -84,18 +84,18 @@ in
       type = mkOption {
         type = types.enum [ "mysql" "pgsql" ];
         default = "mysql";
-        description = "Database engine to use.";
+        description = lib.mdDoc "Database engine to use.";
       };
 
       host = mkOption {
         type = types.str;
         default = "localhost";
-        description = "Database host address.";
+        description = lib.mdDoc "Database host address.";
       };
 
       port = mkOption {
         type = types.int;
-        description = "Database host port.";
+        description = lib.mdDoc "Database host port.";
         default = {
           mysql = 3306;
           pgsql = 5432;
@@ -106,22 +106,22 @@ in
       name = mkOption {
         type = types.str;
         default = "moodle";
-        description = "Database name.";
+        description = lib.mdDoc "Database name.";
       };
 
       user = mkOption {
         type = types.str;
         default = "moodle";
-        description = "Database user.";
+        description = lib.mdDoc "Database user.";
       };
 
       passwordFile = mkOption {
         type = types.nullOr types.path;
         default = null;
         example = "/run/keys/moodle-dbpassword";
-        description = ''
+        description = lib.mdDoc ''
           A file containing the password corresponding to
-          <option>database.user</option>.
+          {option}`database.user`.
         '';
       };
 
@@ -132,7 +132,7 @@ in
           else if pgsqlLocal then "/run/postgresql"
           else null;
         defaultText = literalExpression "/run/mysqld/mysqld.sock";
-        description = "Path to the unix socket file to use for authentication.";
+        description = lib.mdDoc "Path to the unix socket file to use for authentication.";
       };
 
       createLocally = mkOption {
@@ -152,9 +152,9 @@ in
           enableACME = true;
         }
       '';
-      description = ''
-        Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
-        See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+      description = lib.mdDoc ''
+        Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
+        See [](#opt-services.httpd.virtualHosts) for further information.
       '';
     };
 
@@ -168,8 +168,8 @@ in
         "pm.max_spare_servers" = 4;
         "pm.max_requests" = 500;
       };
-      description = ''
-        Options for the Moodle PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+      description = lib.mdDoc ''
+        Options for the Moodle PHP pool. See the documentation on `php-fpm.conf`
         for details on configuration directives.
       '';
     };
@@ -177,10 +177,10 @@ in
     extraConfig = mkOption {
       type = types.lines;
       default = "";
-      description = ''
+      description = lib.mdDoc ''
         Any additional text to be appended to the config.php
         configuration file. This is a PHP script. For configuration
-        details, see <link xlink:href="https://docs.moodle.org/37/en/Configuration_file"/>.
+        details, see <https://docs.moodle.org/37/en/Configuration_file>.
       '';
       example = ''
         $CFG->disableupdatenotifications = true;
diff --git a/nixpkgs/nixos/modules/services/web-apps/netbox.nix b/nixpkgs/nixos/modules/services/web-apps/netbox.nix
new file mode 100644
index 000000000000..2826e57f2c77
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/web-apps/netbox.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, buildEnv, ... }:
+
+with lib;
+
+let
+  cfg = config.services.netbox;
+  staticDir = cfg.dataDir + "/static";
+  configFile = pkgs.writeTextFile {
+    name = "configuration.py";
+    text = ''
+      STATIC_ROOT = '${staticDir}'
+      ALLOWED_HOSTS = ['*']
+      DATABASE = {
+        'NAME': 'netbox',
+        'USER': 'netbox',
+        'HOST': '/run/postgresql',
+      }
+
+      # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
+      # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
+      # to use two separate database IDs.
+      REDIS = {
+          'tasks': {
+              'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=0',
+              'SSL': False,
+          },
+          'caching': {
+              'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=1',
+              'SSL': False,
+          }
+      }
+
+      with open("${cfg.secretKeyFile}", "r") as file:
+          SECRET_KEY = file.readline()
+
+      ${optionalString cfg.enableLdap "REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'"}
+
+      ${cfg.extraConfig}
+    '';
+  };
+  pkg = (pkgs.netbox.overrideAttrs (old: {
+    installPhase = old.installPhase + ''
+      ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py
+    '' + optionalString cfg.enableLdap ''
+      ln -s ${ldapConfigPath} $out/opt/netbox/netbox/netbox/ldap_config.py
+    '';
+  })).override {
+    plugins = ps: ((cfg.plugins ps)
+      ++ optional cfg.enableLdap [ ps.django-auth-ldap ]);
+  };
+  netboxManageScript = with pkgs; (writeScriptBin "netbox-manage" ''
+    #!${stdenv.shell}
+    export PYTHONPATH=${pkg.pythonPath}
+    sudo -u netbox ${pkg}/bin/netbox "$@"
+  '');
+
+in {
+  options.services.netbox = {
+    enable = mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Enable Netbox.
+
+        This module requires a reverse proxy that serves `/static` separately.
+        See this [example](https://github.com/netbox-community/netbox/blob/develop/contrib/nginx.conf/) on how to configure this.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "[::1]";
+      description = lib.mdDoc ''
+        Address the server will listen on.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8001;
+      description = lib.mdDoc ''
+        Port the server will listen on.
+      '';
+    };
+
+    plugins = mkOption {
+      type = types.functionTo (types.listOf types.package);
+      default = _: [];
+      defaultText = literalExpression ''
+        python3Packages: with python3Packages; [];
+      '';
+      description = lib.mdDoc ''
+        List of plugin packages to install.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/netbox";
+      description = lib.mdDoc ''
+        Storage path of netbox.
+      '';
+    };
+
+    secretKeyFile = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Path to a file containing the secret key.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = lib.mdDoc ''
+        Additional lines of configuration appended to the `configuration.py`.
+        See the [documentation](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/) for more possible options.
+      '';
+    };
+
+    enableLdap = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Enable LDAP-Authentication for Netbox.
+
+        This requires a configuration file being pass through `ldapConfigPath`.
+      '';
+    };
+
+    ldapConfigPath = mkOption {
+      type = types.path;
+      default = "";
+      description = lib.mdDoc ''
+        Path to the Configuration-File for LDAP-Authentification, will be loaded as `ldap_config.py`.
+        See the [documentation](https://netbox.readthedocs.io/en/stable/installation/6-ldap/#configuration) for possible options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.redis.servers.netbox.enable = true;
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "netbox" ];
+      ensureUsers = [
+        {
+          name = "netbox";
+          ensurePermissions = {
+            "DATABASE netbox" = "ALL PRIVILEGES";
+          };
+        }
+      ];
+    };
+
+    environment.systemPackages = [ netboxManageScript ];
+
+    systemd.targets.netbox = {
+      description = "Target for all NetBox services";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" "redis-netbox.service" ];
+    };
+
+    systemd.services = let
+      defaultServiceConfig = {
+        WorkingDirectory = "${cfg.dataDir}";
+        User = "netbox";
+        Group = "netbox";
+        StateDirectory = "netbox";
+        StateDirectoryMode = "0750";
+        Restart = "on-failure";
+      };
+    in {
+      netbox-migration = {
+        description = "NetBox migrations";
+        wantedBy = [ "netbox.target" ];
+
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+
+        serviceConfig = defaultServiceConfig // {
+          Type = "oneshot";
+          ExecStart = ''
+            ${pkg}/bin/netbox migrate
+          '';
+        };
+      };
+
+      netbox = {
+        description = "NetBox WSGI Service";
+        wantedBy = [ "netbox.target" ];
+        after = [ "netbox-migration.service" ];
+
+        preStart = ''
+          ${pkg}/bin/netbox trace_paths --no-input
+          ${pkg}/bin/netbox collectstatic --no-input
+          ${pkg}/bin/netbox remove_stale_contenttypes --no-input
+        '';
+
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+
+        serviceConfig = defaultServiceConfig // {
+          ExecStart = ''
+            ${pkgs.python3Packages.gunicorn}/bin/gunicorn netbox.wsgi \
+              --bind ${cfg.listenAddress}:${toString cfg.port} \
+              --pythonpath ${pkg}/opt/netbox/netbox
+          '';
+        };
+      };
+
+      netbox-rq = {
+        description = "NetBox Request Queue Worker";
+        wantedBy = [ "netbox.target" ];
+        after = [ "netbox.service" ];
+
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+
+        serviceConfig = defaultServiceConfig // {
+          ExecStart = ''
+            ${pkg}/bin/netbox rqworker high default low
+          '';
+        };
+      };
+
+      netbox-housekeeping = {
+        description = "NetBox housekeeping job";
+        after = [ "netbox.service" ];
+
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+
+        serviceConfig = defaultServiceConfig // {
+          Type = "oneshot";
+          ExecStart = ''
+            ${pkg}/bin/netbox housekeeping
+          '';
+        };
+      };
+    };
+
+    systemd.timers.netbox-housekeeping = {
+      description = "Run NetBox housekeeping job";
+      wantedBy = [ "timers.target" ];
+
+      timerConfig = {
+        OnCalendar = "daily";
+      };
+    };
+
+    users.users.netbox = {
+      home = "${cfg.dataDir}";
+      isSystemUser = true;
+      group = "netbox";
+    };
+    users.groups.netbox = {};
+    users.groups."${config.services.redis.servers.netbox.user}".members = [ "netbox" ];
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix b/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix
index b32220a5e579..feee7494a71a 100644
--- a/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix
@@ -6,6 +6,8 @@ let
   cfg = config.services.nextcloud;
   fpm = config.services.phpfpm.pools.nextcloud;
 
+  jsonFormat = pkgs.formats.json {};
+
   inherit (cfg) datadir;
 
   phpPackage = cfg.phpPackage.buildEnv {
@@ -80,18 +82,19 @@ in {
     enable = mkEnableOption "nextcloud";
     hostName = mkOption {
       type = types.str;
-      description = "FQDN for the nextcloud instance.";
+      description = lib.mdDoc "FQDN for the nextcloud instance.";
     };
     home = mkOption {
       type = types.str;
       default = "/var/lib/nextcloud";
-      description = "Storage path of nextcloud.";
+      description = lib.mdDoc "Storage path of nextcloud.";
     };
     datadir = mkOption {
       type = types.str;
-      defaultText = "config.services.nextcloud.home";
-      description = ''
-        Data storage path of nextcloud.  Will be <xref linkend="opt-services.nextcloud.home" /> by default.
+      default = config.services.nextcloud.home;
+      defaultText = literalExpression "config.services.nextcloud.home";
+      description = lib.mdDoc ''
+        Data storage path of nextcloud.  Will be [](#opt-services.nextcloud.home) by default.
         This folder will be populated with a config.php and data folder which contains the state of the instance (excl the database).";
       '';
       example = "/mnt/nextcloud-file";
@@ -99,10 +102,10 @@ in {
     extraApps = mkOption {
       type = types.attrsOf types.package;
       default = { };
-      description = ''
+      description = lib.mdDoc ''
         Extra apps to install. Should be an attrSet of appid to packages generated by fetchNextcloudApp.
         The appid must be identical to the "id" value in the apps appinfo/info.xml.
-        Using this will disable the appstore to prevent Nextcloud from updating these apps (see <xref linkend="opt-services.nextcloud.appstoreEnable" />).
+        Using this will disable the appstore to prevent Nextcloud from updating these apps (see [](#opt-services.nextcloud.appstoreEnable)).
       '';
       example = literalExpression ''
         {
@@ -124,8 +127,8 @@ in {
     extraAppsEnable = mkOption {
       type = types.bool;
       default = true;
-      description = ''
-        Automatically enable the apps in <xref linkend="opt-services.nextcloud.extraApps" /> every time nextcloud starts.
+      description = lib.mdDoc ''
+        Automatically enable the apps in [](#opt-services.nextcloud.extraApps) every time nextcloud starts.
         If set to false, apps need to be enabled in the Nextcloud user interface or with nextcloud-occ app:enable.
       '';
     };
@@ -133,33 +136,33 @@ in {
       type = types.nullOr types.bool;
       default = null;
       example = true;
-      description = ''
+      description = lib.mdDoc ''
         Allow the installation of apps and app updates from the store.
-        Enabled by default unless there are packages in <xref linkend="opt-services.nextcloud.extraApps" />.
-        Set to true to force enable the store even if <xref linkend="opt-services.nextcloud.extraApps" /> is used.
+        Enabled by default unless there are packages in [](#opt-services.nextcloud.extraApps).
+        Set to true to force enable the store even if [](#opt-services.nextcloud.extraApps) is used.
         Set to false to disable the installation of apps from the global appstore. App management is always enabled regardless of this setting.
       '';
     };
     logLevel = mkOption {
       type = types.ints.between 0 4;
       default = 2;
-      description = "Log level value between 0 (DEBUG) and 4 (FATAL).";
+      description = lib.mdDoc "Log level value between 0 (DEBUG) and 4 (FATAL).";
     };
     https = mkOption {
       type = types.bool;
       default = false;
-      description = "Use https for generated links.";
+      description = lib.mdDoc "Use https for generated links.";
     };
     package = mkOption {
       type = types.package;
-      description = "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud22" "nextcloud23" ];
+      description = lib.mdDoc "Which package to use for the Nextcloud instance.";
+      relatedPackages = [ "nextcloud23" "nextcloud24" ];
     };
     phpPackage = mkOption {
       type = types.package;
-      relatedPackages = [ "php74" "php80" ];
+      relatedPackages = [ "php80" "php81" ];
       defaultText = "pkgs.php";
-      description = ''
+      description = lib.mdDoc ''
         PHP package to use for Nextcloud.
       '';
     };
@@ -167,7 +170,7 @@ in {
     maxUploadSize = mkOption {
       default = "512M";
       type = types.str;
-      description = ''
+      description = lib.mdDoc ''
         Defines the upload limit for files. This changes the relevant options
         in php.ini and nginx if enabled.
       '';
@@ -176,7 +179,7 @@ in {
     skeletonDirectory = mkOption {
       default = "";
       type = types.str;
-      description = ''
+      description = lib.mdDoc ''
         The directory where the skeleton files are located. These files will be
         copied to the data directory of new users. Leave empty to not copy any
         skeleton files.
@@ -186,7 +189,7 @@ in {
     webfinger = mkOption {
       type = types.bool;
       default = false;
-      description = ''
+      description = lib.mdDoc ''
         Enable this option if you plan on using the webfinger plugin.
         The appropriate nginx rewrite rules will be added to your configuration.
       '';
@@ -196,7 +199,7 @@ in {
       type = with types; functionTo (listOf package);
       default = all: [];
       defaultText = literalExpression "all: []";
-      description = ''
+      description = lib.mdDoc ''
         Additional PHP extensions to use for nextcloud.
         By default, only extensions necessary for a vanilla nextcloud installation are enabled,
         but you may choose from the list of available extensions and add further ones.
@@ -223,7 +226,7 @@ in {
         "openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
         catch_workers_output = "yes";
       };
-      description = ''
+      description = lib.mdDoc ''
         Options for PHP's php.ini file for nextcloud.
       '';
     };
@@ -238,89 +241,106 @@ in {
         "pm.max_spare_servers" = "4";
         "pm.max_requests" = "500";
       };
-      description = ''
-        Options for nextcloud's PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
+      description = lib.mdDoc ''
+        Options for nextcloud's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
       '';
     };
 
     poolConfig = mkOption {
       type = types.nullOr types.lines;
       default = null;
-      description = ''
-        Options for nextcloud's PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
+      description = lib.mdDoc ''
+        Options for nextcloud's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
       '';
     };
 
+    database = {
+
+      createLocally = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Create the database and database user locally. Only available for
+          mysql database.
+          Note that this option will use the latest version of MariaDB which
+          is not officially supported by Nextcloud. As for now a workaround
+          is used to also support MariaDB version >= 10.6.
+        '';
+      };
+
+    };
+
+
     config = {
       dbtype = mkOption {
         type = types.enum [ "sqlite" "pgsql" "mysql" ];
         default = "sqlite";
-        description = "Database type.";
+        description = lib.mdDoc "Database type.";
       };
       dbname = mkOption {
         type = types.nullOr types.str;
         default = "nextcloud";
-        description = "Database name.";
+        description = lib.mdDoc "Database name.";
       };
       dbuser = mkOption {
         type = types.nullOr types.str;
         default = "nextcloud";
-        description = "Database user.";
+        description = lib.mdDoc "Database user.";
       };
       dbpassFile = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = ''
+        description = lib.mdDoc ''
           The full path to a file that contains the database password.
         '';
       };
       dbhost = mkOption {
         type = types.nullOr types.str;
         default = "localhost";
-        description = ''
+        description = lib.mdDoc ''
           Database host.
 
           Note: for using Unix authentication with PostgreSQL, this should be
-          set to <literal>/run/postgresql</literal>.
+          set to `/run/postgresql`.
         '';
       };
       dbport = mkOption {
         type = with types; nullOr (either int str);
         default = null;
-        description = "Database port.";
+        description = lib.mdDoc "Database port.";
       };
       dbtableprefix = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Table prefix in Nextcloud database.";
+        description = lib.mdDoc "Table prefix in Nextcloud database.";
       };
       adminuser = mkOption {
         type = types.str;
         default = "root";
-        description = "Admin username.";
+        description = lib.mdDoc "Admin username.";
       };
       adminpassFile = mkOption {
         type = types.str;
-        description = ''
+        description = lib.mdDoc ''
           The full path to a file that contains the admin's password. Must be
-          readable by user <literal>nextcloud</literal>.
+          readable by user `nextcloud`.
         '';
       };
 
       extraTrustedDomains = mkOption {
         type = types.listOf types.str;
         default = [];
-        description = ''
+        description = lib.mdDoc ''
           Trusted domains, from which the nextcloud installation will be
           acessible.  You don't need to add
-          <literal>services.nextcloud.hostname</literal> here.
+          `services.nextcloud.hostname` here.
         '';
       };
 
       trustedProxies = mkOption {
         type = types.listOf types.str;
         default = [];
-        description = ''
+        description = lib.mdDoc ''
           Trusted proxies, to provide if the nextcloud installation is being
           proxied to secure against e.g. spoofing.
         '';
@@ -331,10 +351,10 @@ in {
         default = null;
         example = "https";
 
-        description = ''
+        description = lib.mdDoc ''
           Force Nextcloud to always use HTTPS i.e. for link generation. Nextcloud
           uses the currently used protocol by default, but when behind a reverse-proxy,
-          it may use <literal>http</literal> for everything although Nextcloud
+          it may use `http` for everything although Nextcloud
           may be served via HTTPS.
         '';
       };
@@ -371,50 +391,50 @@ in {
           bucket = mkOption {
             type = types.str;
             example = "nextcloud";
-            description = ''
+            description = lib.mdDoc ''
               The name of the S3 bucket.
             '';
           };
           autocreate = mkOption {
             type = types.bool;
-            description = ''
+            description = lib.mdDoc ''
               Create the objectstore if it does not exist.
             '';
           };
           key = mkOption {
             type = types.str;
             example = "EJ39ITYZEUH5BGWDRUFY";
-            description = ''
+            description = lib.mdDoc ''
               The access key for the S3 bucket.
             '';
           };
           secretFile = mkOption {
             type = types.str;
             example = "/var/nextcloud-objectstore-s3-secret";
-            description = ''
+            description = lib.mdDoc ''
               The full path to a file that contains the access secret. Must be
-              readable by user <literal>nextcloud</literal>.
+              readable by user `nextcloud`.
             '';
           };
           hostname = mkOption {
             type = types.nullOr types.str;
             default = null;
             example = "example.com";
-            description = ''
+            description = lib.mdDoc ''
               Required for some non-Amazon implementations.
             '';
           };
           port = mkOption {
             type = types.nullOr types.port;
             default = null;
-            description = ''
+            description = lib.mdDoc ''
               Required for some non-Amazon implementations.
             '';
           };
           useSsl = mkOption {
             type = types.bool;
             default = true;
-            description = ''
+            description = lib.mdDoc ''
               Use SSL for objectstore access.
             '';
           };
@@ -422,20 +442,20 @@ in {
             type = types.nullOr types.str;
             default = null;
             example = "REGION";
-            description = ''
+            description = lib.mdDoc ''
               Required for some non-Amazon implementations.
             '';
           };
           usePathStyle = mkOption {
             type = types.bool;
             default = false;
-            description = ''
+            description = lib.mdDoc ''
               Required for some non-Amazon S3 implementations.
 
               Ordinarily, requests will be made with
-              <literal>http://bucket.hostname.domain/</literal>, but with path style
+              `http://bucket.hostname.domain/`, but with path style
               enabled requests are made with
-              <literal>http://hostname.domain/bucket</literal> instead.
+              `http://hostname.domain/bucket` instead.
             '';
           };
         };
@@ -447,7 +467,7 @@ in {
         This is used by the theming app and for generating previews of certain images (e.g. SVG and HEIF).
         You may want to disable it for increased security. In that case, previews will still be available
         for some images (e.g. JPEG and PNG).
-        See <link xlink:href="https://github.com/nextcloud/server/issues/13099" />.
+        See <link xlink:href="https://github.com/nextcloud/server/issues/13099"/>.
     '' // {
       default = true;
     };
@@ -456,14 +476,14 @@ in {
       apcu = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Whether to load the APCu module into PHP.
         '';
       };
       redis = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Whether to load the Redis module into PHP.
           You still need to enable Redis in your config.php.
           See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html
@@ -472,7 +492,7 @@ in {
       memcached = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Whether to load the Memcached module into PHP.
           You still need to enable Memcached in your config.php.
           See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html
@@ -483,7 +503,7 @@ in {
       enable = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Run regular auto update of all apps installed from the nextcloud app store.
         '';
       };
@@ -505,17 +525,80 @@ in {
         The nextcloud-occ program preconfigured to target this Nextcloud instance.
       '';
     };
+    globalProfiles = mkEnableOption "global profiles" // {
+      description = ''
+        Makes user-profiles globally available under <literal>nextcloud.tld/u/user.name</literal>.
+        Even though it's enabled by default in Nextcloud, it must be explicitly enabled
+        here because it has the side-effect that personal information is even accessible to
+        unauthenticated users by default.
+
+        By default, the following properties are set to <quote>Show to everyone</quote>
+        if this flag is enabled:
+        <itemizedlist>
+        <listitem><para>About</para></listitem>
+        <listitem><para>Full name</para></listitem>
+        <listitem><para>Headline</para></listitem>
+        <listitem><para>Organisation</para></listitem>
+        <listitem><para>Profile picture</para></listitem>
+        <listitem><para>Role</para></listitem>
+        <listitem><para>Twitter</para></listitem>
+        <listitem><para>Website</para></listitem>
+        </itemizedlist>
+
+        Only has an effect in Nextcloud 23 and later.
+      '';
+    };
 
-    nginx.recommendedHttpHeaders = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Enable additional recommended HTTP response headers";
+    extraOptions = mkOption {
+      type = jsonFormat.type;
+      default = {};
+      description = lib.mdDoc ''
+        Extra options which should be appended to nextcloud's config.php file.
+      '';
+      example = literalExpression '' {
+        redis = {
+          host = "/run/redis/redis.sock";
+          port = 0;
+          dbindex = 0;
+          password = "secret";
+          timeout = 1.5;
+        };
+      } '';
+    };
+
+    secretFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Secret options which will be appended to nextcloud's config.php file (written as JSON, in the same
+        form as the <xref linkend="opt-services.nextcloud.extraOptions"/> option), for example
+        <programlisting>{"redis":{"password":"secret"}}</programlisting>.
+      '';
+    };
+
+    nginx = {
+      recommendedHttpHeaders = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Enable additional recommended HTTP response headers";
+      };
+      hstsMaxAge = mkOption {
+        type = types.ints.positive;
+        default = 15552000;
+        description = lib.mdDoc ''
+          Value for the `max-age` directive of the HTTP
+          `Strict-Transport-Security` header.
+
+          See section 6.1.1 of IETF RFC 6797 for detailed information on this
+          directive and header.
+        '';
+      };
     };
   };
 
   config = mkIf cfg.enable (mkMerge [
     { warnings = let
-        latest = 23;
+        latest = 24;
         upgradeWarning = major: nixos:
           ''
             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
@@ -551,6 +634,7 @@ in {
         ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05"))
         ++ (optional (versionOlder cfg.package.version "22") (upgradeWarning 21 "21.11"))
         ++ (optional (versionOlder cfg.package.version "23") (upgradeWarning 22 "22.05"))
+        ++ (optional (versionOlder cfg.package.version "24") (upgradeWarning 23 "22.05"))
         ++ (optional isUnsupportedMariadb ''
             You seem to be using MariaDB at an unsupported version (i.e. at least 10.6)!
             Please note that this isn't supported officially by Nextcloud. You can either
@@ -571,20 +655,30 @@ in {
               nextcloud defined in an overlay, please set `services.nextcloud.package` to
               `pkgs.nextcloud`.
             ''
-          else if versionOlder stateVersion "21.11" then nextcloud21
           else if versionOlder stateVersion "22.05" then nextcloud22
-          else nextcloud23
+          else nextcloud24
         );
 
-      services.nextcloud.datadir = mkOptionDefault config.services.nextcloud.home;
-
       services.nextcloud.phpPackage =
-        if versionOlder cfg.package.version "21" then pkgs.php74
+        if versionOlder cfg.package.version "24" then pkgs.php80
+        # FIXME: Use PHP 8.1 with Nextcloud 24 and higher, once issues like this one are fixed:
+        #
+        # https://github.com/nextcloud/twofactor_totp/issues/1192
+        #
+        # else if versionOlder cfg.package.version "24" then pkgs.php80
+        # else pkgs.php81;
         else pkgs.php80;
     }
 
+    { assertions = [
+      { assertion = cfg.database.createLocally -> cfg.config.dbtype == "mysql";
+        message = ''services.nextcloud.config.dbtype must be set to mysql if services.nextcloud.database.createLocally is set to true.'';
+      }
+    ]; }
+
     { systemd.timers.nextcloud-cron = {
         wantedBy = [ "timers.target" ];
+        after = [ "nextcloud-setup.service" ];
         timerConfig.OnBootSec = "5m";
         timerConfig.OnUnitActiveSec = "5m";
         timerConfig.Unit = "nextcloud-cron.service";
@@ -627,6 +721,8 @@ in {
               if x == null then "false"
               else boolToString x;
 
+          nextcloudGreaterOrEqualThan = req: versionAtLeast cfg.package.version req;
+
           overrideConfig = pkgs.writeText "nextcloud-config.php" ''
             <?php
             ${optionalString requiresReadSecretFunction ''
@@ -639,10 +735,20 @@ in {
                     $file
                   ));
                 }
-
                 return trim(file_get_contents($file));
+              }''}
+            function nix_decode_json_file($file, $error) {
+              if (!file_exists($file)) {
+                throw new \RuntimeException(sprintf($error, $file));
               }
-            ''}
+              $decoded = json_decode(file_get_contents($file), true);
+
+              if (json_last_error() !== JSON_ERROR_NONE) {
+                throw new \RuntimeException(sprintf("Cannot decode %s, because: %s", $file, json_last_error_msg()));
+              }
+
+              return $decoded;
+            }
             $CONFIG = [
               'apps_paths' => [
                 ${optionalString (cfg.extraApps != { }) "[ 'path' => '${cfg.home}/nix-apps', 'url' => '/nix-apps', 'writable' => false ],"}
@@ -654,20 +760,38 @@ in {
               'skeletondirectory' => '${cfg.skeletonDirectory}',
               ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
               'log_type' => 'syslog',
-              'log_level' => '${builtins.toString cfg.logLevel}',
+              'loglevel' => '${builtins.toString cfg.logLevel}',
               ${optionalString (c.overwriteProtocol != null) "'overwriteprotocol' => '${c.overwriteProtocol}',"}
               ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"}
               ${optionalString (c.dbhost != null) "'dbhost' => '${c.dbhost}',"}
               ${optionalString (c.dbport != null) "'dbport' => '${toString c.dbport}',"}
               ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"}
               ${optionalString (c.dbtableprefix != null) "'dbtableprefix' => '${toString c.dbtableprefix}',"}
-              ${optionalString (c.dbpassFile != null) "'dbpassword' => nix_read_secret('${c.dbpassFile}'),"}
+              ${optionalString (c.dbpassFile != null) ''
+                  'dbpassword' => nix_read_secret(
+                    "${c.dbpassFile}"
+                  ),
+                ''
+              }
               'dbtype' => '${c.dbtype}',
               'trusted_domains' => ${writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains)},
               'trusted_proxies' => ${writePhpArrary (c.trustedProxies)},
               ${optionalString (c.defaultPhoneRegion != null) "'default_phone_region' => '${c.defaultPhoneRegion}',"}
+              ${optionalString (nextcloudGreaterOrEqualThan "23") "'profile.enabled' => ${boolToString cfg.globalProfiles},"}
               ${objectstoreConfig}
             ];
+
+            $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)"
+            ));
+
+            ${optionalString (cfg.secretFile != null) ''
+              $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
+                "${cfg.secretFile}",
+                "Cannot start Nextcloud, secrets file %s set by NixOS doesn't exist!"
+              ));
+            ''}
           '';
           occInstallCmd = let
             mkExport = { arg, value }: "export ${arg}=${value}";
@@ -762,7 +886,7 @@ in {
             ${occ}/bin/nextcloud-occ config:system:delete trusted_domains
 
             ${optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) ''
-                # Try to enable apps (don't fail when one of them cannot be enabled , eg. due to incompatible version)
+                # Try to enable apps
                 ${occ}/bin/nextcloud-occ app:enable ${concatStringsSep " " (attrNames cfg.extraApps)}
             ''}
 
@@ -772,12 +896,14 @@ in {
           serviceConfig.User = "nextcloud";
         };
         nextcloud-cron = {
+          after = [ "nextcloud-setup.service" ];
           environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
           serviceConfig.Type = "oneshot";
           serviceConfig.User = "nextcloud";
           serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${cfg.package}/cron.php";
         };
         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";
@@ -811,6 +937,32 @@ in {
 
       environment.systemPackages = [ occ ];
 
+      services.mysql = lib.mkIf cfg.database.createLocally {
+        enable = true;
+        package = lib.mkDefault pkgs.mariadb;
+        ensureDatabases = [ cfg.config.dbname ];
+        ensureUsers = [{
+          name = cfg.config.dbuser;
+          ensurePermissions = { "${cfg.config.dbname}.*" = "ALL PRIVILEGES"; };
+        }];
+        # FIXME(@Ma27) Nextcloud isn't compatible with mariadb 10.6,
+        # this is a workaround.
+        # See https://help.nextcloud.com/t/update-to-next-cloud-21-0-2-has-get-an-error/117028/22
+        settings = mkIf (versionOlder cfg.package.version "24") {
+          mysqld = {
+            innodb_read_only_compressed = 0;
+          };
+        };
+        initialScript = pkgs.writeText "mysql-init" ''
+          CREATE USER '${cfg.config.dbname}'@'localhost' IDENTIFIED BY '${builtins.readFile( cfg.config.dbpassFile )}';
+          CREATE DATABASE IF NOT EXISTS ${cfg.config.dbname};
+          GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
+            CREATE TEMPORARY TABLES ON ${cfg.config.dbname}.* TO '${cfg.config.dbuser}'@'localhost'
+            IDENTIFIED BY '${builtins.readFile( cfg.config.dbpassFile )}';
+          FLUSH privileges;
+        '';
+      };
+
       services.nginx.enable = mkDefault true;
 
       services.nginx.virtualHosts.${cfg.hostName} = {
@@ -820,7 +972,6 @@ in {
             priority = 100;
             extraConfig = ''
               allow all;
-              log_not_found off;
               access_log off;
             '';
           };
@@ -908,7 +1059,9 @@ in {
             add_header X-Permitted-Cross-Domain-Policies none;
             add_header X-Frame-Options sameorigin;
             add_header Referrer-Policy no-referrer;
-            add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
+          ''}
+          ${optionalString (cfg.https) ''
+            add_header Strict-Transport-Security "max-age=${toString cfg.nginx.hstsMaxAge}; includeSubDomains" always;
           ''}
           client_max_body_size ${cfg.maxUploadSize};
           fastcgi_buffers 64 4K;
diff --git a/nixpkgs/nixos/modules/services/web-apps/nextcloud.xml b/nixpkgs/nixos/modules/services/web-apps/nextcloud.xml
index 8f55086a2bd1..b46f34420a70 100644
--- a/nixpkgs/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixpkgs/nixos/modules/services/web-apps/nextcloud.xml
@@ -11,7 +11,7 @@
   desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
  </para>
  <para>
-  The current default by NixOS is <package>nextcloud23</package> which is also the latest
+  The current default by NixOS is <package>nextcloud24</package> which is also the latest
   major version available.
  </para>
  <section xml:id="module-services-nextcloud-basic-usage">
diff --git a/nixpkgs/nixos/modules/services/web-apps/nexus.nix b/nixpkgs/nixos/modules/services/web-apps/nexus.nix
index dc50a06705f3..cfa137e77d25 100644
--- a/nixpkgs/nixos/modules/services/web-apps/nexus.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/nexus.nix
@@ -17,37 +17,37 @@ in
         type = types.package;
         default = pkgs.nexus;
         defaultText = literalExpression "pkgs.nexus";
-        description = "Package which runs Nexus3";
+        description = lib.mdDoc "Package which runs Nexus3";
       };
 
       user = mkOption {
         type = types.str;
         default = "nexus";
-        description = "User which runs Nexus3.";
+        description = lib.mdDoc "User which runs Nexus3.";
       };
 
       group = mkOption {
         type = types.str;
         default = "nexus";
-        description = "Group which runs Nexus3.";
+        description = lib.mdDoc "Group which runs Nexus3.";
       };
 
       home = mkOption {
         type = types.str;
         default = "/var/lib/sonatype-work";
-        description = "Home directory of the Nexus3 instance.";
+        description = lib.mdDoc "Home directory of the Nexus3 instance.";
       };
 
       listenAddress = mkOption {
         type = types.str;
         default = "127.0.0.1";
-        description = "Address to listen on.";
+        description = lib.mdDoc "Address to listen on.";
       };
 
       listenPort = mkOption {
         type = types.int;
         default = 8081;
-        description = "Port to listen on.";
+        description = lib.mdDoc "Port to listen on.";
       };
 
       jvmOpts = mkOption {
diff --git a/nixpkgs/nixos/modules/services/web-apps/nifi.nix b/nixpkgs/nixos/modules/services/web-apps/nifi.nix
new file mode 100644
index 000000000000..e3f30c710e0c
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/web-apps/nifi.nix
@@ -0,0 +1,318 @@
+{ lib, pkgs, config, options, ... }:
+
+let
+  cfg = config.services.nifi;
+  opt = options.services.nifi;
+
+  env = {
+    NIFI_OVERRIDE_NIFIENV = "true";
+    NIFI_HOME = "/var/lib/nifi";
+    NIFI_PID_DIR = "/run/nifi";
+    NIFI_LOG_DIR = "/var/log/nifi";
+  };
+
+  envFile = pkgs.writeText "nifi.env" (lib.concatMapStrings (s: s + "\n") (
+    (lib.concatLists (lib.mapAttrsToList (name: value:
+      if value != null then [
+        "${name}=\"${toString value}\""
+      ] else []
+    ) env))));
+
+  nifiEnv = pkgs.writeShellScriptBin "nifi-env" ''
+    set -a
+    source "${envFile}"
+    eval -- "\$@"
+  '';
+
+in {
+  options = {
+    services.nifi = {
+      enable = lib.mkEnableOption "Apache NiFi";
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.nifi;
+        defaultText = lib.literalExpression "pkgs.nifi";
+        description = lib.mdDoc "Apache NiFi package to use.";
+      };
+
+      user = lib.mkOption {
+        type = lib.types.str;
+        default = "nifi";
+        description = lib.mdDoc "User account where Apache NiFi runs.";
+      };
+
+      group = lib.mkOption {
+        type = lib.types.str;
+        default = "nifi";
+        description = lib.mdDoc "Group account where Apache NiFi runs.";
+      };
+
+      enableHTTPS = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = lib.mdDoc "Enable HTTPS protocol. Don`t use in production.";
+      };
+
+      listenHost = lib.mkOption {
+        type = lib.types.str;
+        default = if cfg.enableHTTPS then "0.0.0.0" else "127.0.0.1";
+        defaultText = lib.literalExpression ''
+          if config.${opt.enableHTTPS}
+          then "0.0.0.0"
+          else "127.0.0.1"
+        '';
+        description = lib.mdDoc "Bind to an ip for Apache NiFi web-ui.";
+      };
+
+      listenPort = lib.mkOption {
+        type = lib.types.int;
+        default = if cfg.enableHTTPS then 8443 else 8080;
+        defaultText = lib.literalExpression ''
+          if config.${opt.enableHTTPS}
+          then "8443"
+          else "8000"
+        '';
+        description = lib.mdDoc "Bind to a port for Apache NiFi web-ui.";
+      };
+
+      proxyHost = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = if cfg.enableHTTPS then "0.0.0.0" else null;
+        defaultText = lib.literalExpression ''
+          if config.${opt.enableHTTPS}
+          then "0.0.0.0"
+          else null
+        '';
+        description = lib.mdDoc "Allow requests from a specific host.";
+      };
+
+      proxyPort = lib.mkOption {
+        type = lib.types.nullOr lib.types.int;
+        default = if cfg.enableHTTPS then 8443 else null;
+        defaultText = lib.literalExpression ''
+          if config.${opt.enableHTTPS}
+          then "8443"
+          else null
+        '';
+        description = lib.mdDoc "Allow requests from a specific port.";
+      };
+
+      initUser = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = lib.mdDoc "Initial user account for Apache NiFi. Username must be at least 4 characters.";
+      };
+
+      initPasswordFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/run/keys/nifi/password-nifi";
+        description = lib.mdDoc "nitial password for Apache NiFi. Password must be at least 12 characters.";
+      };
+
+      initJavaHeapSize = lib.mkOption {
+        type = lib.types.nullOr lib.types.int;
+        default = null;
+        example = 1024;
+        description = lib.mdDoc "Set the initial heap size for the JVM in MB.";
+      };
+
+      maxJavaHeapSize = lib.mkOption {
+        type = lib.types.nullOr lib.types.int;
+        default = null;
+        example = 2048;
+        description = lib.mdDoc "Set the initial heap size for the JVM in MB.";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.initUser!=null || cfg.initPasswordFile==null;
+          message = ''
+            <option>services.nifi.initUser</option> needs to be set if <option>services.nifi.initPasswordFile</option> enabled.
+          '';
+      }
+      { assertion = cfg.initUser==null || cfg.initPasswordFile!=null;
+          message = ''
+            <option>services.nifi.initPasswordFile</option> needs to be set if <option>services.nifi.initUser</option> enabled.
+          '';
+      }
+      { assertion = cfg.proxyHost==null || cfg.proxyPort!=null;
+          message = ''
+            <option>services.nifi.proxyPort</option> needs to be set if <option>services.nifi.proxyHost</option> value specified.
+          '';
+      }
+      { assertion = cfg.proxyHost!=null || cfg.proxyPort==null;
+          message = ''
+            <option>services.nifi.proxyHost</option> needs to be set if <option>services.nifi.proxyPort</option> value specified.
+          '';
+      }
+      { assertion = cfg.initJavaHeapSize==null || cfg.maxJavaHeapSize!=null;
+          message = ''
+            <option>services.nifi.maxJavaHeapSize</option> needs to be set if <option>services.nifi.initJavaHeapSize</option> value specified.
+          '';
+      }
+      { assertion = cfg.initJavaHeapSize!=null || cfg.maxJavaHeapSize==null;
+          message = ''
+            <option>services.nifi.initJavaHeapSize</option> needs to be set if <option>services.nifi.maxJavaHeapSize</option> value specified.
+          '';
+      }
+    ];
+
+    warnings = lib.optional (cfg.enableHTTPS==false) ''
+      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.services.nifi = {
+      description = "Apache NiFi";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = env;
+      path = [ pkgs.gawk ];
+
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = "/run/nifi/nifi.pid";
+        ExecStartPre = pkgs.writeScript "nifi-pre-start.sh" ''
+          #!/bin/sh
+          umask 077
+          test -f '/var/lib/nifi/conf/authorizers.xml'                      || (cp '${cfg.package}/share/nifi/conf/authorizers.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/authorizers.xml')
+          test -f '/var/lib/nifi/conf/bootstrap.conf'                       || (cp '${cfg.package}/share/nifi/conf/bootstrap.conf' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap.conf')
+          test -f '/var/lib/nifi/conf/bootstrap-hashicorp-vault.conf'       || (cp '${cfg.package}/share/nifi/conf/bootstrap-hashicorp-vault.conf' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap-hashicorp-vault.conf')
+          test -f '/var/lib/nifi/conf/bootstrap-notification-services.xml'  || (cp '${cfg.package}/share/nifi/conf/bootstrap-notification-services.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap-notification-services.xml')
+          test -f '/var/lib/nifi/conf/logback.xml'                          || (cp '${cfg.package}/share/nifi/conf/logback.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/logback.xml')
+          test -f '/var/lib/nifi/conf/login-identity-providers.xml'         || (cp '${cfg.package}/share/nifi/conf/login-identity-providers.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/login-identity-providers.xml')
+          test -f '/var/lib/nifi/conf/nifi.properties'                      || (cp '${cfg.package}/share/nifi/conf/nifi.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/nifi.properties')
+          test -f '/var/lib/nifi/conf/stateless-logback.xml'                || (cp '${cfg.package}/share/nifi/conf/stateless-logback.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/stateless-logback.xml')
+          test -f '/var/lib/nifi/conf/stateless.properties'                 || (cp '${cfg.package}/share/nifi/conf/stateless.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/stateless.properties')
+          test -f '/var/lib/nifi/conf/state-management.xml'                 || (cp '${cfg.package}/share/nifi/conf/state-management.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/state-management.xml')
+          test -f '/var/lib/nifi/conf/zookeeper.properties'                 || (cp '${cfg.package}/share/nifi/conf/zookeeper.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/zookeeper.properties')
+          test -d '/var/lib/nifi/docs/html'                                 || (mkdir -p /var/lib/nifi/docs && cp -r '${cfg.package}/share/nifi/docs/html' '/var/lib/nifi/docs/html')
+          ${lib.optionalString ((cfg.initUser != null) && (cfg.initPasswordFile != null)) ''
+            awk -F'[<|>]' '/property name="Username"/ {if ($3!="") f=1} END{exit !f}' /var/lib/nifi/conf/login-identity-providers.xml || ${cfg.package}/bin/nifi.sh set-single-user-credentials ${cfg.initUser} $(cat ${cfg.initPasswordFile})
+          ''}
+          ${lib.optionalString (cfg.enableHTTPS == false) ''
+            sed -i /var/lib/nifi/conf/nifi.properties \
+              -e 's|nifi.remote.input.secure=.*|nifi.remote.input.secure=false|g' \
+              -e 's|nifi.web.http.host=.*|nifi.web.http.host=${cfg.listenHost}|g' \
+              -e 's|nifi.web.http.port=.*|nifi.web.http.port=${(toString cfg.listenPort)}|g' \
+              -e 's|nifi.web.https.host=.*|nifi.web.https.host=|g' \
+              -e 's|nifi.web.https.port=.*|nifi.web.https.port=|g' \
+              -e 's|nifi.security.keystore=.*|nifi.security.keystore=|g' \
+              -e 's|nifi.security.keystoreType=.*|nifi.security.keystoreType=|g' \
+              -e 's|nifi.security.truststore=.*|nifi.security.truststore=|g' \
+              -e 's|nifi.security.truststoreType=.*|nifi.security.truststoreType=|g' \
+              -e '/nifi.security.keystorePasswd/s|^|#|' \
+              -e '/nifi.security.keyPasswd/s|^|#|' \
+              -e '/nifi.security.truststorePasswd/s|^|#|'
+          ''}
+          ${lib.optionalString (cfg.enableHTTPS == true) ''
+            sed -i /var/lib/nifi/conf/nifi.properties \
+              -e 's|nifi.remote.input.secure=.*|nifi.remote.input.secure=true|g' \
+              -e 's|nifi.web.http.host=.*|nifi.web.http.host=|g' \
+              -e 's|nifi.web.http.port=.*|nifi.web.http.port=|g' \
+              -e 's|nifi.web.https.host=.*|nifi.web.https.host=${cfg.listenHost}|g' \
+              -e 's|nifi.web.https.port=.*|nifi.web.https.port=${(toString cfg.listenPort)}|g' \
+              -e 's|nifi.security.keystore=.*|nifi.security.keystore=./conf/keystore.p12|g' \
+              -e 's|nifi.security.keystoreType=.*|nifi.security.keystoreType=PKCS12|g' \
+              -e 's|nifi.security.truststore=.*|nifi.security.truststore=./conf/truststore.p12|g' \
+              -e 's|nifi.security.truststoreType=.*|nifi.security.truststoreType=PKCS12|g' \
+              -e '/nifi.security.keystorePasswd/s|^#\+||' \
+              -e '/nifi.security.keyPasswd/s|^#\+||' \
+              -e '/nifi.security.truststorePasswd/s|^#\+||'
+          ''}
+          ${lib.optionalString ((cfg.enableHTTPS == true) && (cfg.proxyHost != null) && (cfg.proxyPort != null)) ''
+            sed -i /var/lib/nifi/conf/nifi.properties \
+              -e 's|nifi.web.proxy.host=.*|nifi.web.proxy.host=${cfg.proxyHost}:${(toString cfg.proxyPort)}|g'
+          ''}
+          ${lib.optionalString ((cfg.enableHTTPS == false) || (cfg.proxyHost == null) && (cfg.proxyPort == null)) ''
+            sed -i /var/lib/nifi/conf/nifi.properties \
+              -e 's|nifi.web.proxy.host=.*|nifi.web.proxy.host=|g'
+          ''}
+          ${lib.optionalString ((cfg.initJavaHeapSize != null) && (cfg.maxJavaHeapSize != null))''
+            sed -i /var/lib/nifi/conf/bootstrap.conf \
+              -e 's|java.arg.2=.*|java.arg.2=-Xms${(toString cfg.initJavaHeapSize)}m|g' \
+              -e 's|java.arg.3=.*|java.arg.3=-Xmx${(toString cfg.maxJavaHeapSize)}m|g'
+          ''}
+          ${lib.optionalString ((cfg.initJavaHeapSize == null) && (cfg.maxJavaHeapSize == null))''
+            sed -i /var/lib/nifi/conf/bootstrap.conf \
+              -e 's|java.arg.2=.*|java.arg.2=-Xms512m|g' \
+              -e 's|java.arg.3=.*|java.arg.3=-Xmx512m|g'
+          ''}
+        '';
+        ExecStart = "${cfg.package}/bin/nifi.sh start";
+        ExecStop = "${cfg.package}/bin/nifi.sh stop";
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # Runtime directory and mode
+        RuntimeDirectory = "nifi";
+        RuntimeDirectoryMode = "0750";
+        # State directory and mode
+        StateDirectory = "nifi";
+        StateDirectoryMode = "0750";
+        # Logs directory and mode
+        LogsDirectory = "nifi";
+        LogsDirectoryMode = "0750";
+        # Proc filesystem
+        ProcSubset = "pid";
+        ProtectProc = "invisible";
+        # Access write directories
+        ReadWritePaths = [ cfg.initPasswordFile ];
+        UMask = "0027";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+        PrivateUsers = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET AF_INET6" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute  = false;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @resources @privileged @setuid" "@chown" ];
+      };
+    };
+
+    users.users = lib.mkMerge [
+      (lib.mkIf (cfg.user == "nifi") {
+        nifi = {
+          group = cfg.group;
+          isSystemUser = true;
+          home = cfg.package;
+        };
+      })
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package nifiEnv ])
+    ];
+
+    users.groups = lib.optionalAttrs (cfg.group == "nifi") {
+      nifi = { };
+    };
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/web-apps/node-red.nix b/nixpkgs/nixos/modules/services/web-apps/node-red.nix
index 4512907f027b..e5b0998d3c41 100644
--- a/nixpkgs/nixos/modules/services/web-apps/node-red.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/node-red.nix
@@ -23,13 +23,13 @@ in
       default = pkgs.nodePackages.node-red;
       defaultText = literalExpression "pkgs.nodePackages.node-red";
       type = types.package;
-      description = "Node-RED package to use.";
+      description = lib.mdDoc "Node-RED package to use.";
     };
 
     openFirewall = mkOption {
       type = types.bool;
       default = false;
-      description = ''
+      description = lib.mdDoc ''
         Open ports in the firewall for the server.
       '';
     };
@@ -37,7 +37,7 @@ in
     withNpmAndGcc = mkOption {
       type = types.bool;
       default = false;
-      description = ''
+      description = lib.mdDoc ''
         Give Node-RED access to NPM and GCC at runtime, so 'Nodes' can be
         downloaded and managed imperatively via the 'Palette Manager'.
       '';
@@ -47,10 +47,9 @@ in
       type = types.path;
       default = "${cfg.package}/lib/node_modules/node-red/settings.js";
       defaultText = literalExpression ''"''${package}/lib/node_modules/node-red/settings.js"'';
-      description = ''
+      description = lib.mdDoc ''
         Path to the JavaScript configuration file.
-        See <link
-        xlink:href="https://github.com/node-red/node-red/blob/master/packages/node_modules/node-red/settings.js"/>
+        See <https://github.com/node-red/node-red/blob/master/packages/node_modules/node-red/settings.js>
         for a configuration example.
       '';
     };
@@ -58,13 +57,13 @@ in
     port = mkOption {
       type = types.port;
       default = 1880;
-      description = "Listening port.";
+      description = lib.mdDoc "Listening port.";
     };
 
     user = mkOption {
       type = types.str;
       default = defaultUser;
-      description = ''
+      description = lib.mdDoc ''
         User under which Node-RED runs.If left as the default value this user
         will automatically be created on system activation, otherwise the
         sysadmin is responsible for ensuring the user exists.
@@ -74,7 +73,7 @@ in
     group = mkOption {
       type = types.str;
       default = defaultUser;
-      description = ''
+      description = lib.mdDoc ''
         Group under which Node-RED runs.If left as the default value this group
         will automatically be created on system activation, otherwise the
         sysadmin is responsible for ensuring the group exists.
@@ -84,7 +83,7 @@ in
     userDir = mkOption {
       type = types.path;
       default = "/var/lib/node-red";
-      description = ''
+      description = lib.mdDoc ''
         The directory to store all user data, such as flow and credential files and all library data. If left
         as the default value this directory will automatically be created before the node-red service starts,
         otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership
@@ -95,13 +94,13 @@ in
     safe = mkOption {
       type = types.bool;
       default = false;
-      description = "Whether to launch Node-RED in --safe mode.";
+      description = lib.mdDoc "Whether to launch Node-RED in --safe mode.";
     };
 
     define = mkOption {
       type = types.attrs;
       default = {};
-      description = "List of settings.js overrides to pass via -D to Node-RED.";
+      description = lib.mdDoc "List of settings.js overrides to pass via -D to Node-RED.";
       example = literalExpression ''
         {
           "logging.console.level" = "trace";
diff --git a/nixpkgs/nixos/modules/services/web-apps/onlyoffice.nix b/nixpkgs/nixos/modules/services/web-apps/onlyoffice.nix
new file mode 100644
index 000000000000..15fc3b03a834
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/web-apps/onlyoffice.nix
@@ -0,0 +1,288 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.onlyoffice;
+in
+{
+  options.services.onlyoffice = {
+    enable = mkEnableOption "OnlyOffice DocumentServer";
+
+    enableExampleServer = mkEnableOption "OnlyOffice example server";
+
+    hostname = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = lib.mdDoc "FQDN for the onlyoffice instance.";
+    };
+
+    jwtSecretFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = lib.mdDoc ''
+        Path to a file that contains the secret to sign web requests using JSON Web Tokens.
+        If left at the default value null signing is disabled.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.onlyoffice-documentserver;
+      defaultText = "pkgs.onlyoffice-documentserver";
+      description = lib.mdDoc "Which package to use for the OnlyOffice instance.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8000;
+      description = lib.mdDoc "Port the OnlyOffice DocumentServer should listens on.";
+    };
+
+    examplePort = mkOption {
+      type = types.port;
+      default = null;
+      description = lib.mdDoc "Port the OnlyOffice Example server should listens on.";
+    };
+
+    postgresHost = mkOption {
+      type = types.str;
+      default = "/run/postgresql";
+      description = lib.mdDoc "The Postgresql hostname or socket path OnlyOffice should connect to.";
+    };
+
+    postgresName = mkOption {
+      type = types.str;
+      default = "onlyoffice";
+      description = lib.mdDoc "The name of databse OnlyOffice should user.";
+    };
+
+    postgresPasswordFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = lib.mdDoc ''
+        Path to a file that contains the password OnlyOffice should use to connect to Postgresql.
+        Unused when using socket authentication.
+      '';
+    };
+
+    postgresUser = mkOption {
+      type = types.str;
+      default = "onlyoffice";
+      description = lib.mdDoc ''
+        The username OnlyOffice should use to connect to Postgresql.
+        Unused when using socket authentication.
+      '';
+    };
+
+    rabbitmqUrl = mkOption {
+      type = types.str;
+      default = "amqp://guest:guest@localhost:5672";
+      description = lib.mdDoc "The Rabbitmq in amqp URI style OnlyOffice should connect to.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services = {
+      nginx = {
+        enable = mkDefault true;
+        # misses text/csv, font/ttf, application/x-font-ttf, application/rtf, application/wasm
+        recommendedGzipSettings = mkDefault true;
+        recommendedProxySettings = mkDefault true;
+
+        upstreams = {
+          # /etc/nginx/includes/http-common.conf
+          onlyoffice-docservice = {
+            servers = { "localhost:${toString cfg.port}" = { }; };
+          };
+          onlyoffice-example = lib.mkIf cfg.enableExampleServer {
+            servers = { "localhost:${toString cfg.examplePort}" = { }; };
+          };
+        };
+
+        virtualHosts.${cfg.hostname} = {
+          locations = {
+            # /etc/nginx/includes/ds-docservice.conf
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(web-apps\/apps\/api\/documents\/api\.js)$".extraConfig = ''
+              expires -1;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(web-apps)(\/.*\.json)$".extraConfig = ''
+              expires 365d;
+              error_log /dev/null crit;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(sdkjs-plugins)(\/.*\.json)$".extraConfig = ''
+              expires 365d;
+              error_log /dev/null crit;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(web-apps|sdkjs|sdkjs-plugins|fonts)(\/.*)$".extraConfig = ''
+              expires 365d;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
+            '';
+            "~* ^(\/cache\/files.*)(\/.*)".extraConfig = ''
+              alias /var/lib/onlyoffice/documentserver/App_Data$1;
+              add_header Content-Disposition "attachment; filename*=UTF-8''$arg_filename";
+
+              set $secret_string verysecretstring;
+              secure_link $arg_md5,$arg_expires;
+              secure_link_md5 "$secure_link_expires$uri$secret_string";
+
+              if ($secure_link = "") {
+                return 403;
+              }
+
+              if ($secure_link = "0") {
+                return 410;
+              }
+            '';
+            "~* ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(internal)(\/.*)$".extraConfig = ''
+              allow 127.0.0.1;
+              deny all;
+              proxy_pass http://onlyoffice-docservice/$2$3;
+            '';
+            "~* ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(info)(\/.*)$".extraConfig = ''
+              allow 127.0.0.1;
+              deny all;
+              proxy_pass http://onlyoffice-docservice/$2$3;
+            '';
+            "/".extraConfig = ''
+              proxy_pass http://onlyoffice-docservice;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?(\/doc\/.*)".extraConfig = ''
+              proxy_pass http://onlyoffice-docservice$2;
+              proxy_http_version 1.1;
+            '';
+            "/${cfg.package.version}/".extraConfig = ''
+              proxy_pass http://onlyoffice-docservice/;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(dictionaries)(\/.*)$".extraConfig = ''
+              expires 365d;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
+            '';
+            # /etc/nginx/includes/ds-example.conf
+            "~ ^(\/welcome\/.*)$".extraConfig = ''
+              expires 365d;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver-example$1;
+              index docker.html;
+            '';
+            "/example/".extraConfig = lib.mkIf cfg.enableExampleServer ''
+              proxy_pass http://onlyoffice-example/;
+              proxy_set_header X-Forwarded-Path /example;
+            '';
+          };
+          extraConfig = ''
+            rewrite ^/$ /welcome/ redirect;
+            rewrite ^\/OfficeWeb(\/apps\/.*)$ /${cfg.package.version}/web-apps$1 redirect;
+            rewrite ^(\/web-apps\/apps\/(?!api\/).*)$ /${cfg.package.version}$1 redirect;
+
+            # based on https://github.com/ONLYOFFICE/document-server-package/blob/master/common/documentserver/nginx/includes/http-common.conf.m4#L29-L34
+            # without variable indirection and correct variable names
+            proxy_set_header Host $host;
+            proxy_set_header X-Forwarded-Host $host;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            # required for CSP to take effect
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            # required for websocket
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection $connection_upgrade;
+          '';
+        };
+      };
+
+      rabbitmq.enable = lib.mkDefault true;
+
+      postgresql = {
+        enable = lib.mkDefault true;
+        ensureDatabases = [ "onlyoffice" ];
+        ensureUsers = [{
+          name = "onlyoffice";
+          ensurePermissions = { "DATABASE \"onlyoffice\"" = "ALL PRIVILEGES"; };
+        }];
+      };
+    };
+
+    systemd.services = {
+      onlyoffice-converter = {
+        description = "onlyoffice converter";
+        after = [ "network.target" "onlyoffice-docservice.service" "postgresql.service" ];
+        requires = [ "network.target" "onlyoffice-docservice.service" "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${cfg.package.fhs}/bin/onlyoffice-wrapper FileConverter/converter /run/onlyoffice/config";
+          Group = "onlyoffice";
+          Restart = "always";
+          RuntimeDirectory = "onlyoffice";
+          StateDirectory = "onlyoffice";
+          Type = "simple";
+          User = "onlyoffice";
+        };
+      };
+
+      onlyoffice-docservice =
+        let
+          onlyoffice-prestart = pkgs.writeShellScript "onlyoffice-prestart" ''
+            PATH=$PATH:${lib.makeBinPath (with pkgs; [ jq moreutils config.services.postgresql.package ])}
+            umask 077
+            mkdir -p /run/onlyoffice/config/ /var/lib/onlyoffice/documentserver/sdkjs/{slide/themes,common}/ /var/lib/onlyoffice/documentserver/{fonts,server/FileConverter/bin}/
+            cp -r ${cfg.package}/etc/onlyoffice/documentserver/* /run/onlyoffice/config/
+            chmod u+w /run/onlyoffice/config/default.json
+
+            cp /run/onlyoffice/config/default.json{,.orig}
+
+            # for a mapping of environment variables from the docker container to json options see
+            # https://github.com/ONLYOFFICE/Docker-DocumentServer/blob/master/run-document-server.sh
+            jq '
+              .services.CoAuthoring.server.port = ${toString cfg.port} |
+              .services.CoAuthoring.sql.dbHost = "${cfg.postgresHost}" |
+              .services.CoAuthoring.sql.dbName = "${cfg.postgresName}" |
+            ${lib.optionalString (cfg.postgresPasswordFile != null) ''
+              .services.CoAuthoring.sql.dbPass = "'"$(cat ${cfg.postgresPasswordFile})"'" |
+            ''}
+              .services.CoAuthoring.sql.dbUser = "${cfg.postgresUser}" |
+            ${lib.optionalString (cfg.jwtSecretFile != null) ''
+              .services.CoAuthoring.token.enable.browser = true |
+              .services.CoAuthoring.token.enable.request.inbox = true |
+              .services.CoAuthoring.token.enable.request.outbox = true |
+              .services.CoAuthoring.secret.inbox.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
+              .services.CoAuthoring.secret.outbox.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
+              .services.CoAuthoring.secret.session.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
+            ''}
+              .rabbitmq.url = "${cfg.rabbitmqUrl}"
+              ' /run/onlyoffice/config/default.json | sponge /run/onlyoffice/config/default.json
+
+            if ! psql -d onlyoffice -c "SELECT 'task_result'::regclass;" >/dev/null; then
+              psql -f ${cfg.package}/var/www/onlyoffice/documentserver/server/schema/postgresql/createdb.sql
+            fi
+          '';
+        in
+        {
+          description = "onlyoffice documentserver";
+          after = [ "network.target" "postgresql.service" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            ExecStart = "${cfg.package.fhs}/bin/onlyoffice-wrapper DocService/docservice /run/onlyoffice/config";
+            ExecStartPre = onlyoffice-prestart;
+            Group = "onlyoffice";
+            Restart = "always";
+            RuntimeDirectory = "onlyoffice";
+            StateDirectory = "onlyoffice";
+            Type = "simple";
+            User = "onlyoffice";
+          };
+        };
+    };
+
+    users.users = {
+      onlyoffice = {
+        description = "OnlyOffice Service";
+        group = "onlyoffice";
+        isSystemUser = true;
+      };
+    };
+
+    users.groups.onlyoffice = { };
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/web-apps/openwebrx.nix b/nixpkgs/nixos/modules/services/web-apps/openwebrx.nix
index 9e90c01e0bbb..c409adbc7106 100644
--- a/nixpkgs/nixos/modules/services/web-apps/openwebrx.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/openwebrx.nix
@@ -10,7 +10,7 @@ in
       type = types.package;
       default = pkgs.openwebrx;
       defaultText = literalExpression "pkgs.openwebrx";
-      description = "OpenWebRX package to use for the service";
+      description = lib.mdDoc "OpenWebRX package to use for the service";
     };
   };
 
@@ -19,6 +19,10 @@ in
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [
         csdr
+        digiham
+        codec2
+        js8call
+        m17-cxx-demod
         alsaUtils
         netcat
       ];
diff --git a/nixpkgs/nixos/modules/services/web-apps/peertube.nix b/nixpkgs/nixos/modules/services/web-apps/peertube.nix
index e195e6e6e824..c5a80e2d7d9d 100644
--- a/nixpkgs/nixos/modules/services/web-apps/peertube.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/peertube.nix
@@ -11,6 +11,7 @@ let
     NODE_CONFIG_DIR = "/var/lib/peertube/config";
     NODE_ENV = "production";
     NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt";
+    NPM_CONFIG_CACHE = "/var/cache/peertube/.npm";
     NPM_CONFIG_PREFIX = cfg.package;
     HOME = cfg.package;
   };
@@ -73,51 +74,51 @@ in {
     user = lib.mkOption {
       type = lib.types.str;
       default = "peertube";
-      description = "User account under which Peertube runs.";
+      description = lib.mdDoc "User account under which Peertube runs.";
     };
 
     group = lib.mkOption {
       type = lib.types.str;
       default = "peertube";
-      description = "Group under which Peertube runs.";
+      description = lib.mdDoc "Group under which Peertube runs.";
     };
 
     localDomain = lib.mkOption {
       type = lib.types.str;
       example = "peertube.example.com";
-      description = "The domain serving your PeerTube instance.";
+      description = lib.mdDoc "The domain serving your PeerTube instance.";
     };
 
     listenHttp = lib.mkOption {
       type = lib.types.int;
       default = 9000;
-      description = "listen port for HTTP server.";
+      description = lib.mdDoc "listen port for HTTP server.";
     };
 
     listenWeb = lib.mkOption {
       type = lib.types.int;
       default = 9000;
-      description = "listen port for WEB server.";
+      description = lib.mdDoc "listen port for WEB server.";
     };
 
     enableWebHttps = lib.mkOption {
       type = lib.types.bool;
       default = false;
-      description = "Enable or disable HTTPS protocol.";
+      description = lib.mdDoc "Enable or disable HTTPS protocol.";
     };
 
     dataDirs = lib.mkOption {
       type = lib.types.listOf lib.types.path;
       default = [ ];
       example = [ "/opt/peertube/storage" "/var/cache/peertube" ];
-      description = "Allow access to custom data locations.";
+      description = lib.mdDoc "Allow access to custom data locations.";
     };
 
     serviceEnvironmentFile = lib.mkOption {
       type = lib.types.nullOr lib.types.path;
       default = null;
       example = "/run/keys/peertube/password-init-root";
-      description = ''
+      description = lib.mdDoc ''
         Set environment variables for the service. Mainly useful for setting the initial root password.
         For example write to file:
         PT_INITIAL_ROOT_PASSWORD=changeme
@@ -141,14 +142,14 @@ in {
           };
         }
       '';
-      description = "Configuration for peertube.";
+      description = lib.mdDoc "Configuration for peertube.";
     };
 
     database = {
       createLocally = lib.mkOption {
         type = lib.types.bool;
         default = false;
-        description = "Configure local PostgreSQL database server for PeerTube.";
+        description = lib.mdDoc "Configure local PostgreSQL database server for PeerTube.";
       };
 
       host = lib.mkOption {
@@ -160,32 +161,32 @@ in {
           else null
         '';
         example = "192.168.15.47";
-        description = "Database host address or unix socket.";
+        description = lib.mdDoc "Database host address or unix socket.";
       };
 
       port = lib.mkOption {
         type = lib.types.int;
         default = 5432;
-        description = "Database host port.";
+        description = lib.mdDoc "Database host port.";
       };
 
       name = lib.mkOption {
         type = lib.types.str;
         default = "peertube";
-        description = "Database name.";
+        description = lib.mdDoc "Database name.";
       };
 
       user = lib.mkOption {
         type = lib.types.str;
         default = "peertube";
-        description = "Database user.";
+        description = lib.mdDoc "Database user.";
       };
 
       passwordFile = lib.mkOption {
         type = lib.types.nullOr lib.types.path;
         default = null;
         example = "/run/keys/peertube/password-posgressql-db";
-        description = "Password for PostgreSQL database.";
+        description = lib.mdDoc "Password for PostgreSQL database.";
       };
     };
 
@@ -193,7 +194,7 @@ in {
       createLocally = lib.mkOption {
         type = lib.types.bool;
         default = false;
-        description = "Configure local Redis server for PeerTube.";
+        description = lib.mdDoc "Configure local Redis server for PeerTube.";
       };
 
       host = lib.mkOption {
@@ -204,32 +205,32 @@ in {
           then "127.0.0.1"
           else null
         '';
-        description = "Redis host.";
+        description = lib.mdDoc "Redis host.";
       };
 
       port = lib.mkOption {
         type = lib.types.nullOr lib.types.port;
-        default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 6379;
+        default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 31638;
         defaultText = lib.literalExpression ''
           if config.${opt.redis.createLocally} && config.${opt.redis.enableUnixSocket}
           then null
           else 6379
         '';
-        description = "Redis port.";
+        description = lib.mdDoc "Redis port.";
       };
 
       passwordFile = lib.mkOption {
         type = lib.types.nullOr lib.types.path;
         default = null;
         example = "/run/keys/peertube/password-redis-db";
-        description = "Password for redis database.";
+        description = lib.mdDoc "Password for redis database.";
       };
 
       enableUnixSocket = lib.mkOption {
         type = lib.types.bool;
         default = cfg.redis.createLocally;
         defaultText = lib.literalExpression "config.${opt.redis.createLocally}";
-        description = "Use Unix socket.";
+        description = lib.mdDoc "Use Unix socket.";
       };
     };
 
@@ -237,14 +238,14 @@ in {
       createLocally = lib.mkOption {
         type = lib.types.bool;
         default = false;
-        description = "Configure local Postfix SMTP server for PeerTube.";
+        description = lib.mdDoc "Configure local Postfix SMTP server for PeerTube.";
       };
 
       passwordFile = lib.mkOption {
         type = lib.types.nullOr lib.types.path;
         default = null;
         example = "/run/keys/peertube/password-smtp";
-        description = "Password for smtp server.";
+        description = lib.mdDoc "Password for smtp server.";
       };
     };
 
@@ -252,7 +253,7 @@ in {
       type = lib.types.package;
       default = pkgs.peertube;
       defaultText = lib.literalExpression "pkgs.peertube";
-      description = "Peertube package to use.";
+      description = lib.mdDoc "Peertube package to use.";
     };
   };
 
@@ -344,7 +345,7 @@ in {
           };
         };
       }
-      (lib.mkIf cfg.redis.enableUnixSocket { redis = { socket = "/run/redis/redis.sock"; }; })
+      (lib.mkIf cfg.redis.enableUnixSocket { redis = { socket = "/run/redis-peertube/redis.sock"; }; })
     ];
 
     systemd.tmpfiles.rules = [
@@ -425,6 +426,9 @@ in {
         # State directory and mode
         StateDirectory = "peertube";
         StateDirectoryMode = "0750";
+        # Cache directory and mode
+        CacheDirectory = "peertube";
+        CacheDirectoryMode = "0750";
         # Access write directories
         ReadWritePaths = cfg.dataDirs;
         # Environment
@@ -441,13 +445,17 @@ in {
       enable = true;
     };
 
-    services.redis = lib.mkMerge [
+    services.redis.servers.peertube = lib.mkMerge [
       (lib.mkIf cfg.redis.createLocally {
         enable = true;
       })
+      (lib.mkIf (cfg.redis.createLocally && !cfg.redis.enableUnixSocket) {
+        bind = "127.0.0.1";
+        port = cfg.redis.port;
+      })
       (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {
-        unixSocket = "/run/redis/redis.sock";
-        unixSocketPerm = 770;
+        unixSocket = "/run/redis-peertube/redis.sock";
+        unixSocketPerm = 660;
       })
     ];
 
@@ -465,7 +473,7 @@ in {
         };
       })
       (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs-16_x pkgs.yarn ])
-      (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis" ];})
+      (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis-peertube" ];})
     ];
 
     users.groups = lib.optionalAttrs (cfg.group == "peertube") {
diff --git a/nixpkgs/nixos/modules/services/web-apps/phylactery.nix b/nixpkgs/nixos/modules/services/web-apps/phylactery.nix
new file mode 100644
index 000000000000..d512b48539b8
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/web-apps/phylactery.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let cfg = config.services.phylactery;
+in {
+  options.services.phylactery = {
+    enable = mkEnableOption "Whether to enable Phylactery server";
+
+    host = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = lib.mdDoc "Listen host for Phylactery";
+    };
+
+    port = mkOption {
+      type = types.port;
+      description = lib.mdDoc "Listen port for Phylactery";
+    };
+
+    library = mkOption {
+      type = types.path;
+      description = lib.mdDoc "Path to CBZ library";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.phylactery;
+      defaultText = literalExpression "pkgs.phylactery";
+      description = lib.mdDoc "The Phylactery package to use";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.phylactery = {
+      environment = {
+        PHYLACTERY_ADDRESS = "${cfg.host}:${toString cfg.port}";
+        PHYLACTERY_LIBRARY = "${cfg.library}";
+      };
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ConditionPathExists = cfg.library;
+        DynamicUser = true;
+        ExecStart = "${cfg.package}/bin/phylactery";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ McSinyx ];
+}
diff --git a/nixpkgs/nixos/modules/services/web-apps/pict-rs.nix b/nixpkgs/nixos/modules/services/web-apps/pict-rs.nix
index e1847fbd5314..ab5a9ed07356 100644
--- a/nixpkgs/nixos/modules/services/web-apps/pict-rs.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/pict-rs.nix
@@ -14,21 +14,21 @@ in
     dataDir = mkOption {
       type = types.path;
       default = "/var/lib/pict-rs";
-      description = ''
+      description = lib.mdDoc ''
         The directory where to store the uploaded images.
       '';
     };
     address = mkOption {
       type = types.str;
       default = "127.0.0.1";
-      description = ''
+      description = lib.mdDoc ''
         The IPv4 address to deploy the service to.
       '';
     };
     port = mkOption {
       type = types.port;
       default = 8080;
-      description = ''
+      description = lib.mdDoc ''
         The port which to bind the service to.
       '';
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/plantuml-server.nix b/nixpkgs/nixos/modules/services/web-apps/plantuml-server.nix
index 9ea37b8a4cad..acd9292ceb45 100644
--- a/nixpkgs/nixos/modules/services/web-apps/plantuml-server.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/plantuml-server.nix
@@ -17,7 +17,7 @@ in
         type = types.package;
         default = pkgs.plantuml-server;
         defaultText = literalExpression "pkgs.plantuml-server";
-        description = "PlantUML server package to use";
+        description = lib.mdDoc "PlantUML server package to use";
       };
 
       packages = {
@@ -25,75 +25,75 @@ in
           type = types.package;
           default = pkgs.jdk;
           defaultText = literalExpression "pkgs.jdk";
-          description = "JDK package to use for the server";
+          description = lib.mdDoc "JDK package to use for the server";
         };
         jetty = mkOption {
           type = types.package;
           default = pkgs.jetty;
           defaultText = literalExpression "pkgs.jetty";
-          description = "Jetty package to use for the server";
+          description = lib.mdDoc "Jetty package to use for the server";
         };
       };
 
       user = mkOption {
         type = types.str;
         default = "plantuml";
-        description = "User which runs PlantUML server.";
+        description = lib.mdDoc "User which runs PlantUML server.";
       };
 
       group = mkOption {
         type = types.str;
         default = "plantuml";
-        description = "Group which runs PlantUML server.";
+        description = lib.mdDoc "Group which runs PlantUML server.";
       };
 
       home = mkOption {
         type = types.str;
         default = "/var/lib/plantuml";
-        description = "Home directory of the PlantUML server instance.";
+        description = lib.mdDoc "Home directory of the PlantUML server instance.";
       };
 
       listenHost = mkOption {
         type = types.str;
         default = "127.0.0.1";
-        description = "Host to listen on.";
+        description = lib.mdDoc "Host to listen on.";
       };
 
       listenPort = mkOption {
         type = types.int;
         default = 8080;
-        description = "Port to listen on.";
+        description = lib.mdDoc "Port to listen on.";
       };
 
       plantumlLimitSize = mkOption {
         type = types.int;
         default = 4096;
-        description = "Limits image width and height.";
+        description = lib.mdDoc "Limits image width and height.";
       };
 
       graphvizPackage = mkOption {
         type = types.package;
         default = pkgs.graphviz;
         defaultText = literalExpression "pkgs.graphviz";
-        description = "Package containing the dot executable.";
+        description = lib.mdDoc "Package containing the dot executable.";
       };
 
       plantumlStats = mkOption {
         type = types.bool;
         default = false;
-        description = "Set it to on to enable statistics report (https://plantuml.com/statistics-report).";
+        description = lib.mdDoc "Set it to on to enable statistics report (https://plantuml.com/statistics-report).";
       };
 
       httpAuthorization = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "When calling the proxy endpoint, the value of HTTP_AUTHORIZATION will be used to set the HTTP Authorization header.";
+        description = lib.mdDoc "When calling the proxy endpoint, the value of HTTP_AUTHORIZATION will be used to set the HTTP Authorization header.";
       };
 
       allowPlantumlInclude = mkOption {
         type = types.bool;
         default = false;
-        description = "Enables !include processing which can read files from the server into diagrams. Files are read relative to the current working directory.";
+        description = lib.mdDoc "Enables !include processing which can read files from the server into diagrams. Files are read relative to the current working directory.";
       };
     };
   };
diff --git a/nixpkgs/nixos/modules/services/web-apps/plausible.nix b/nixpkgs/nixos/modules/services/web-apps/plausible.nix
index 5d550ae5ca86..6f098134c922 100644
--- a/nixpkgs/nixos/modules/services/web-apps/plausible.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/plausible.nix
@@ -11,7 +11,7 @@ in {
 
     releaseCookiePath = mkOption {
       type = with types; either str path;
-      description = ''
+      description = lib.mdDoc ''
         The path to the file with release cookie. (used for remote connection to the running node).
       '';
     };
@@ -20,7 +20,7 @@ in {
       name = mkOption {
         default = "admin";
         type = types.str;
-        description = ''
+        description = lib.mdDoc ''
           Name of the admin user that plausible will created on initial startup.
         '';
       };
@@ -28,14 +28,14 @@ in {
       email = mkOption {
         type = types.str;
         example = "admin@localhost";
-        description = ''
+        description = lib.mdDoc ''
           Email-address of the admin-user.
         '';
       };
 
       passwordFile = mkOption {
         type = types.either types.str types.path;
-        description = ''
+        description = lib.mdDoc ''
           Path to the file which contains the password of the admin user.
         '';
       };
@@ -59,7 +59,7 @@ in {
         dbname = mkOption {
           default = "plausible";
           type = types.str;
-          description = ''
+          description = lib.mdDoc ''
             Name of the database to use.
           '';
         };
@@ -77,35 +77,35 @@ in {
       disableRegistration = mkOption {
         default = true;
         type = types.bool;
-        description = ''
+        description = lib.mdDoc ''
           Whether to prohibit creating an account in plausible's UI.
         '';
       };
       secretKeybaseFile = mkOption {
         type = types.either types.path types.str;
-        description = ''
-          Path to the secret used by the <literal>phoenix</literal>-framework. Instructions
+        description = lib.mdDoc ''
+          Path to the secret used by the `phoenix`-framework. Instructions
           how to generate one are documented in the
-          <link xlink:href="https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content">
-          framework docs</link>.
+          [
+          framework docs](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content).
         '';
       };
       port = mkOption {
         default = 8000;
         type = types.port;
-        description = ''
+        description = lib.mdDoc ''
           Port where the service should be available.
         '';
       };
       baseUrl = mkOption {
         type = types.str;
-        description = ''
+        description = lib.mdDoc ''
           Public URL where plausible is available.
 
-          Note that <literal>/path</literal> components are currently ignored:
-          <link xlink:href="https://github.com/plausible/analytics/issues/1182">
+          Note that `/path` components are currently ignored:
+          [
             https://github.com/plausible/analytics/issues/1182
-          </link>.
+          ](https://github.com/plausible/analytics/issues/1182).
         '';
       };
     };
@@ -114,8 +114,8 @@ in {
       email = mkOption {
         default = "hello@plausible.local";
         type = types.str;
-        description = ''
-          The email id to use for as <emphasis>from</emphasis> address of all communications
+        description = lib.mdDoc ''
+          The email id to use for as *from* address of all communications
           from Plausible.
         '';
       };
@@ -123,28 +123,28 @@ in {
         hostAddr = mkOption {
           default = "localhost";
           type = types.str;
-          description = ''
+          description = lib.mdDoc ''
             The host address of your smtp server.
           '';
         };
         hostPort = mkOption {
           default = 25;
           type = types.port;
-          description = ''
+          description = lib.mdDoc ''
             The port of your smtp server.
           '';
         };
         user = mkOption {
           default = null;
           type = types.nullOr types.str;
-          description = ''
+          description = lib.mdDoc ''
             The username/email in case SMTP auth is enabled.
           '';
         };
         passwordFile = mkOption {
           default = null;
           type = with types; nullOr (either str path);
-          description = ''
+          description = lib.mdDoc ''
             The path to the file with the password in case SMTP auth is enabled.
           '';
         };
@@ -152,7 +152,7 @@ in {
         retries = mkOption {
           type = types.ints.unsigned;
           default = 2;
-          description = ''
+          description = lib.mdDoc ''
             Number of retries to make until mailer gives up.
           '';
         };
diff --git a/nixpkgs/nixos/modules/services/web-apps/powerdns-admin.nix b/nixpkgs/nixos/modules/services/web-apps/powerdns-admin.nix
index 4661ba80c5d6..c2d65f59e4dc 100644
--- a/nixpkgs/nixos/modules/services/web-apps/powerdns-admin.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/powerdns-admin.nix
@@ -27,7 +27,7 @@ in
       example = literalExpression ''
         [ "-b" "127.0.0.1:8000" ]
       '';
-      description = ''
+      description = lib.mdDoc ''
         Extra arguments passed to powerdns-admin.
       '';
     };
@@ -40,9 +40,9 @@ in
         PORT = 8000
         SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
       '';
-      description = ''
+      description = lib.mdDoc ''
         Configuration python file.
-        See <link xlink:href="https://github.com/ngoduykhanh/PowerDNS-Admin/blob/v${pkgs.powerdns-admin.version}/configs/development.py">the example configuration</link>
+        See [the example configuration](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/v${pkgs.powerdns-admin.version}/configs/development.py)
         for options.
       '';
     };
@@ -50,7 +50,7 @@ in
     secretKeyFile = mkOption {
       type = types.nullOr types.path;
       example = "/etc/powerdns-admin/secret";
-      description = ''
+      description = lib.mdDoc ''
         The secret used to create cookies.
         This needs to be set, otherwise the default is used and everyone can forge valid login cookies.
         Set this to null to ignore this setting and configure it through another way.
@@ -60,7 +60,7 @@ in
     saltFile = mkOption {
       type = types.nullOr types.path;
       example = "/etc/powerdns-admin/salt";
-      description = ''
+      description = lib.mdDoc ''
         The salt used for serialization.
         This should be set, otherwise the default is used.
         Set this to null to ignore this setting and configure it through another way.
diff --git a/nixpkgs/nixos/modules/services/web-apps/prosody-filer.nix b/nixpkgs/nixos/modules/services/web-apps/prosody-filer.nix
index a901a95fd5f9..1d40809c420c 100644
--- a/nixpkgs/nixos/modules/services/web-apps/prosody-filer.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/prosody-filer.nix
@@ -14,9 +14,9 @@ in {
       enable = mkEnableOption "Prosody Filer XMPP upload file server";
 
       settings = mkOption {
-        description = ''
+        description = lib.mdDoc ''
           Configuration for Prosody Filer.
-          Refer to <link xlink:href="https://github.com/ThomasLeister/prosody-filer#configure-prosody-filer"/> for details on supported values.
+          Refer to <https://github.com/ThomasLeister/prosody-filer#configure-prosody-filer> for details on supported values.
         '';
 
         type = settingsFormat.type;
diff --git a/nixpkgs/nixos/modules/services/web-apps/restya-board.nix b/nixpkgs/nixos/modules/services/web-apps/restya-board.nix
index 4b36cc8754c6..ae80a7866a16 100644
--- a/nixpkgs/nixos/modules/services/web-apps/restya-board.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/restya-board.nix
@@ -30,7 +30,7 @@ in
       dataDir = mkOption {
         type = types.path;
         default = "/var/lib/restya-board";
-        description = ''
+        description = lib.mdDoc ''
           Data of the application.
         '';
       };
@@ -38,7 +38,7 @@ in
       user = mkOption {
         type = types.str;
         default = "restya-board";
-        description = ''
+        description = lib.mdDoc ''
           User account under which the web-application runs.
         '';
       };
@@ -46,7 +46,7 @@ in
       group = mkOption {
         type = types.str;
         default = "nginx";
-        description = ''
+        description = lib.mdDoc ''
           Group account under which the web-application runs.
         '';
       };
@@ -55,7 +55,7 @@ in
         serverName = mkOption {
           type = types.str;
           default = "restya.board";
-          description = ''
+          description = lib.mdDoc ''
             Name of the nginx virtualhost to use.
           '';
         };
@@ -63,7 +63,7 @@ in
         listenHost = mkOption {
           type = types.str;
           default = "localhost";
-          description = ''
+          description = lib.mdDoc ''
             Listen address for the virtualhost to use.
           '';
         };
@@ -71,7 +71,7 @@ in
         listenPort = mkOption {
           type = types.int;
           default = 3000;
-          description = ''
+          description = lib.mdDoc ''
             Listen port for the virtualhost to use.
           '';
         };
@@ -81,7 +81,7 @@ in
         host = mkOption {
           type = types.nullOr types.str;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             Host of the database. Leave 'null' to use a local PostgreSQL database.
             A local PostgreSQL database is initialized automatically.
           '';
@@ -90,7 +90,7 @@ in
         port = mkOption {
           type = types.nullOr types.int;
           default = 5432;
-          description = ''
+          description = lib.mdDoc ''
             The database's port.
           '';
         };
@@ -98,7 +98,7 @@ in
         name = mkOption {
           type = types.str;
           default = "restya_board";
-          description = ''
+          description = lib.mdDoc ''
             Name of the database. The database must exist.
           '';
         };
@@ -106,7 +106,7 @@ in
         user = mkOption {
           type = types.str;
           default = "restya_board";
-          description = ''
+          description = lib.mdDoc ''
             The database user. The user must exist and have access to
             the specified database.
           '';
@@ -115,7 +115,7 @@ in
         passwordFile = mkOption {
           type = types.nullOr types.path;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             The database user's password. 'null' if no password is set.
           '';
         };
@@ -126,7 +126,7 @@ in
           type = types.nullOr types.str;
           default = null;
           example = "localhost";
-          description = ''
+          description = lib.mdDoc ''
             Hostname to send outgoing mail. Null to use the system MTA.
           '';
         };
@@ -134,7 +134,7 @@ in
         port = mkOption {
           type = types.int;
           default = 25;
-          description = ''
+          description = lib.mdDoc ''
             Port used to connect to SMTP server.
           '';
         };
@@ -142,7 +142,7 @@ in
         login = mkOption {
           type = types.str;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             SMTP authentication login used when sending outgoing mail.
           '';
         };
@@ -150,7 +150,7 @@ in
         password = mkOption {
           type = types.str;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             SMTP authentication password used when sending outgoing mail.
 
             ATTENTION: The password is stored world-readable in the nix-store!
@@ -161,7 +161,7 @@ in
       timezone = mkOption {
         type = types.lines;
         default = "GMT";
-        description = ''
+        description = lib.mdDoc ''
           Timezone the web-app runs in.
         '';
       };
@@ -263,8 +263,8 @@ in
       serviceConfig.RemainAfterExit = true;
 
       wantedBy = [ "multi-user.target" ];
-      requires = [ "postgresql.service" ];
-      after = [ "network.target" "postgresql.service" ];
+      requires = if cfg.database.host == null then [] else [ "postgresql.service" ];
+      after = [ "network.target" ] ++ (if cfg.database.host == null then [] else [ "postgresql.service" ]);
 
       script = ''
         rm -rf "${runDir}"
@@ -282,7 +282,7 @@ in
           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 "'file_get_contents(${cfg.database.passwordFile})'"});/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"
@@ -294,7 +294,7 @@ in
         ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img"
 
         chmod g+w "${runDir}/tmp/cache"
-        chown -R "${cfg.user}"."${cfg.group}" "${runDir}"
+        chown -R "${cfg.user}":"${cfg.group}" "${runDir}"
 
 
         mkdir -m 0750 -p "${cfg.dataDir}"
@@ -302,9 +302,9 @@ in
         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"
+        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
diff --git a/nixpkgs/nixos/modules/services/web-apps/rss-bridge.nix b/nixpkgs/nixos/modules/services/web-apps/rss-bridge.nix
index f2b6d9559823..b1a3907d1964 100644
--- a/nixpkgs/nixos/modules/services/web-apps/rss-bridge.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/rss-bridge.nix
@@ -16,7 +16,7 @@ in
       user = mkOption {
         type = types.str;
         default = "nginx";
-        description = ''
+        description = lib.mdDoc ''
           User account under which both the service and the web-application run.
         '';
       };
@@ -24,7 +24,7 @@ in
       group = mkOption {
         type = types.str;
         default = "nginx";
-        description = ''
+        description = lib.mdDoc ''
           Group under which the web-application run.
         '';
       };
@@ -32,7 +32,7 @@ in
       pool = mkOption {
         type = types.str;
         default = poolName;
-        description = ''
+        description = lib.mdDoc ''
           Name of existing phpfpm pool that is used to run web-application.
           If not specified a pool will be created automatically with
           default values.
@@ -42,16 +42,16 @@ in
       dataDir = mkOption {
         type = types.str;
         default = "/var/lib/rss-bridge";
-        description = ''
+        description = lib.mdDoc ''
           Location in which cache directory will be created.
-          You can put <literal>config.ini.php</literal> in here.
+          You can put `config.ini.php` in here.
         '';
       };
 
       virtualHost = mkOption {
         type = types.nullOr types.str;
         default = "rss-bridge";
-        description = ''
+        description = lib.mdDoc ''
           Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
         '';
       };
diff --git a/nixpkgs/nixos/modules/services/web-apps/selfoss.nix b/nixpkgs/nixos/modules/services/web-apps/selfoss.nix
index 899976ac696c..016e053c802b 100644
--- a/nixpkgs/nixos/modules/services/web-apps/selfoss.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/selfoss.nix
@@ -35,7 +35,7 @@ in
         user = mkOption {
           type = types.str;
           default = "nginx";
-          description = ''
+          description = lib.mdDoc ''
             User account under which both the service and the web-application run.
           '';
         };
@@ -43,7 +43,7 @@ in
         pool = mkOption {
           type = types.str;
           default = "${poolName}";
-          description = ''
+          description = lib.mdDoc ''
             Name of existing phpfpm pool that is used to run web-application.
             If not specified a pool will be created automatically with
             default values.
@@ -54,7 +54,7 @@ in
         type = mkOption {
           type = types.enum ["pgsql" "mysql" "sqlite"];
           default = "sqlite";
-          description = ''
+          description = lib.mdDoc ''
             Database to store feeds. Supported are sqlite, pgsql and mysql.
           '';
         };
@@ -62,7 +62,7 @@ in
         host = mkOption {
           type = types.str;
           default = "localhost";
-          description = ''
+          description = lib.mdDoc ''
             Host of the database (has no effect if type is "sqlite").
           '';
         };
@@ -70,7 +70,7 @@ in
         name = mkOption {
           type = types.str;
           default = "tt_rss";
-          description = ''
+          description = lib.mdDoc ''
             Name of the existing database (has no effect if type is "sqlite").
           '';
         };
@@ -78,7 +78,7 @@ in
         user = mkOption {
           type = types.str;
           default = "tt_rss";
-          description = ''
+          description = lib.mdDoc ''
             The database user. The user must exist and has access to
             the specified database (has no effect if type is "sqlite").
           '';
@@ -87,7 +87,7 @@ in
         password = mkOption {
           type = types.nullOr types.str;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             The database user's password (has no effect if type is "sqlite").
           '';
         };
@@ -95,7 +95,7 @@ in
         port = mkOption {
           type = types.nullOr types.int;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             The database's port. If not set, the default ports will be
             provided (5432 and 3306 for pgsql and mysql respectively)
             (has no effect if type is "sqlite").
@@ -105,7 +105,7 @@ in
       extraConfig = mkOption {
         type = types.lines;
         default = "";
-        description = ''
+        description = lib.mdDoc ''
           Extra configuration added to config.ini
         '';
       };
diff --git a/nixpkgs/nixos/modules/services/web-apps/shiori.nix b/nixpkgs/nixos/modules/services/web-apps/shiori.nix
index bb2fc684e83b..494f8587306f 100644
--- a/nixpkgs/nixos/modules/services/web-apps/shiori.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/shiori.nix
@@ -12,13 +12,13 @@ in {
         type = types.package;
         default = pkgs.shiori;
         defaultText = literalExpression "pkgs.shiori";
-        description = "The Shiori package to use.";
+        description = lib.mdDoc "The Shiori package to use.";
       };
 
       address = mkOption {
         type = types.str;
         default = "";
-        description = ''
+        description = lib.mdDoc ''
           The IP address on which Shiori will listen.
           If empty, listens on all interfaces.
         '';
@@ -27,7 +27,7 @@ in {
       port = mkOption {
         type = types.port;
         default = 8080;
-        description = "The port of the Shiori web application";
+        description = lib.mdDoc "The port of the Shiori web application";
       };
     };
   };
diff --git a/nixpkgs/nixos/modules/services/web-apps/snipe-it.nix b/nixpkgs/nixos/modules/services/web-apps/snipe-it.nix
new file mode 100644
index 000000000000..c0d29b048a33
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/web-apps/snipe-it.nix
@@ -0,0 +1,494 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.snipe-it;
+  snipe-it = pkgs.snipe-it.override {
+    dataDir = cfg.dataDir;
+  };
+  db = cfg.database;
+  mail = cfg.mail;
+
+  user = cfg.user;
+  group = cfg.group;
+
+  tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
+
+  # shell script for local administration
+  artisan = pkgs.writeScriptBin "snipe-it" ''
+    #! ${pkgs.runtimeShell}
+    cd ${snipe-it}
+    sudo=exec
+    if [[ "$USER" != ${user} ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u ${user}'
+    fi
+    $sudo ${pkgs.php}/bin/php artisan $*
+  '';
+in {
+  options.services.snipe-it = {
+
+    enable = mkEnableOption "A free open source IT asset/license management system";
+
+    user = mkOption {
+      default = "snipeit";
+      description = lib.mdDoc "User snipe-it runs as.";
+      type = types.str;
+    };
+
+    group = mkOption {
+      default = "snipeit";
+      description = lib.mdDoc "Group snipe-it runs as.";
+      type = types.str;
+    };
+
+    appKeyFile = mkOption {
+      description = lib.mdDoc ''
+        A file containing the Laravel APP_KEY - a 32 character long,
+        base64 encoded key used for encryption where needed. Can be
+        generated with `head -c 32 /dev/urandom | base64`.
+      '';
+      example = "/run/keys/snipe-it/appkey";
+      type = types.path;
+    };
+
+    hostName = lib.mkOption {
+      type = lib.types.str;
+      default = if config.networking.domain != null then
+                  config.networking.fqdn
+                else
+                  config.networking.hostName;
+      defaultText = lib.literalExpression "config.networking.fqdn";
+      example = "snipe-it.example.com";
+      description = lib.mdDoc ''
+        The hostname to serve Snipe-IT on.
+      '';
+    };
+
+    appURL = mkOption {
+      description = lib.mdDoc ''
+        The root URL that you want to host Snipe-IT on. All URLs in Snipe-IT will be generated using this value.
+        If you change this in the future you may need to run a command to update stored URLs in the database.
+        Command example: `snipe-it snipe-it:update-url https://old.example.com https://new.example.com`
+      '';
+      default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostName}";
+      defaultText = ''
+        http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostName}
+      '';
+      example = "https://example.com";
+      type = types.str;
+    };
+
+    dataDir = mkOption {
+      description = lib.mdDoc "snipe-it data directory";
+      default = "/var/lib/snipe-it";
+      type = types.path;
+    };
+
+    database = {
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = lib.mdDoc "Database host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 3306;
+        description = lib.mdDoc "Database host port.";
+      };
+      name = mkOption {
+        type = types.str;
+        default = "snipeit";
+        description = lib.mdDoc "Database name.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = user;
+        defaultText = literalExpression "user";
+        description = lib.mdDoc "Database username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/snipe-it/dbpassword";
+        description = lib.mdDoc ''
+          A file containing the password corresponding to
+          {option}`database.user`.
+        '';
+      };
+      createLocally = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc "Create the database and database user locally.";
+      };
+    };
+
+    mail = {
+      driver = mkOption {
+        type = types.enum [ "smtp" "sendmail" ];
+        default = "smtp";
+        description = lib.mdDoc "Mail driver to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = lib.mdDoc "Mail host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 1025;
+        description = lib.mdDoc "Mail host port.";
+      };
+      encryption = mkOption {
+        type = with types; nullOr (enum [ "tls" "ssl" ]);
+        default = null;
+        description = lib.mdDoc "SMTP encryption mechanism to use.";
+      };
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "snipeit";
+        description = lib.mdDoc "Mail username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/snipe-it/mailpassword";
+        description = lib.mdDoc ''
+          A file containing the password corresponding to
+          {option}`mail.user`.
+        '';
+      };
+      backupNotificationAddress = mkOption {
+        type = types.str;
+        default = "backup@example.com";
+        description = lib.mdDoc "Email Address to send Backup Notifications to.";
+      };
+      from = {
+        name = mkOption {
+          type = types.str;
+          default = "Snipe-IT Asset Management";
+          description = lib.mdDoc "Mail \"from\" name.";
+        };
+        address = mkOption {
+          type = types.str;
+          default = "mail@example.com";
+          description = lib.mdDoc "Mail \"from\" address.";
+        };
+      };
+      replyTo = {
+        name = mkOption {
+          type = types.str;
+          default = "Snipe-IT Asset Management";
+          description = lib.mdDoc "Mail \"reply-to\" name.";
+        };
+        address = mkOption {
+          type = types.str;
+          default = "mail@example.com";
+          description = lib.mdDoc "Mail \"reply-to\" address.";
+        };
+      };
+    };
+
+    maxUploadSize = mkOption {
+      type = types.str;
+      default = "18M";
+      example = "1G";
+      description = lib.mdDoc "The maximum size for uploads (e.g. images).";
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = lib.mdDoc ''
+        Options for the snipe-it PHP pool. See the documentation on `php-fpm.conf`
+        for details on configuration directives.
+      '';
+    };
+
+    nginx = mkOption {
+      type = types.submodule (
+        recursiveUpdate
+          (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
+      );
+      default = {};
+      example = literalExpression ''
+        {
+          serverAliases = [
+            "snipe-it.''${config.networking.domain}"
+          ];
+          # To enable encryption and let let's encrypt take care of certificate
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
+      description = lib.mdDoc ''
+        With this option, you can customize the nginx virtualHost settings.
+      '';
+    };
+
+    config = mkOption {
+      type = with types;
+        attrsOf
+          (nullOr
+            (either
+              (oneOf [
+                bool
+                int
+                port
+                path
+                str
+              ])
+              (submodule {
+                options = {
+                  _secret = mkOption {
+                    type = nullOr (oneOf [ str path ]);
+                    description = ''
+                      The path to a file containing the value the
+                      option should be set to in the final
+                      configuration file.
+                    '';
+                  };
+                };
+              })));
+      default = {};
+      example = literalExpression ''
+        {
+          ALLOWED_IFRAME_HOSTS = "https://example.com";
+          WKHTMLTOPDF = "''${pkgs.wkhtmltopdf}/bin/wkhtmltopdf";
+          AUTH_METHOD = "oidc";
+          OIDC_NAME = "MyLogin";
+          OIDC_DISPLAY_NAME_CLAIMS = "name";
+          OIDC_CLIENT_ID = "snipe-it";
+          OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
+          OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
+          OIDC_ISSUER_DISCOVER = true;
+        }
+      '';
+      description = lib.mdDoc ''
+        Snipe-IT configuration options to set in the
+        {file}`.env` file.
+        Refer to <https://snipe-it.readme.io/docs/configuration>
+        for details on supported values.
+
+        Settings containing secret data should be set to an attribute
+        set containing the attribute `_secret` - a
+        string pointing to a file containing the value the option
+        should be set to. See the example to get a better picture of
+        this: in the resulting {file}`.env` file, the
+        `OIDC_CLIENT_SECRET` key will be set to the
+        contents of the {file}`/run/keys/oidc_secret`
+        file.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = db.createLocally -> db.user == user;
+        message = "services.snipe-it.database.user must be set to ${user} if services.snipe-it.database.createLocally is set true.";
+      }
+      { assertion = db.createLocally -> db.passwordFile == null;
+        message = "services.snipe-it.database.passwordFile cannot be specified if services.snipe-it.database.createLocally is set to true.";
+      }
+    ];
+
+    environment.systemPackages = [ artisan ];
+
+    services.snipe-it.config = {
+      APP_ENV = "production";
+      APP_KEY._secret = cfg.appKeyFile;
+      APP_URL = cfg.appURL;
+      DB_HOST = db.host;
+      DB_PORT = db.port;
+      DB_DATABASE = db.name;
+      DB_USERNAME = db.user;
+      DB_PASSWORD._secret = db.passwordFile;
+      MAIL_DRIVER = mail.driver;
+      MAIL_FROM_NAME = mail.from.name;
+      MAIL_FROM_ADDR = mail.from.address;
+      MAIL_REPLYTO_NAME = mail.from.name;
+      MAIL_REPLYTO_ADDR = mail.from.address;
+      MAIL_BACKUP_NOTIFICATION_ADDRESS = mail.backupNotificationAddress;
+      MAIL_HOST = mail.host;
+      MAIL_PORT = mail.port;
+      MAIL_USERNAME = mail.user;
+      MAIL_ENCRYPTION = mail.encryption;
+      MAIL_PASSWORD._secret = mail.passwordFile;
+      APP_SERVICES_CACHE = "/run/snipe-it/cache/services.php";
+      APP_PACKAGES_CACHE = "/run/snipe-it/cache/packages.php";
+      APP_CONFIG_CACHE = "/run/snipe-it/cache/config.php";
+      APP_ROUTES_CACHE = "/run/snipe-it/cache/routes-v7.php";
+      APP_EVENTS_CACHE = "/run/snipe-it/cache/events.php";
+      SESSION_SECURE_COOKIE = tlsEnabled;
+    };
+
+    services.mysql = mkIf db.createLocally {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ db.name ];
+      ensureUsers = [
+        { name = db.user;
+          ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.phpfpm.pools.snipe-it = {
+      inherit user group;
+      phpPackage = pkgs.php81;
+      phpOptions = ''
+        post_max_size = ${cfg.maxUploadSize}
+        upload_max_filesize = ${cfg.maxUploadSize}
+      '';
+      settings = {
+        "listen.mode" = "0660";
+        "listen.owner" = user;
+        "listen.group" = group;
+      } // cfg.poolConfig;
+    };
+
+    services.nginx = {
+      enable = mkDefault true;
+      virtualHosts."${cfg.hostName}" = mkMerge [ cfg.nginx {
+        root = mkForce "${snipe-it}/public";
+        extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
+        locations = {
+          "/" = {
+            index = "index.php";
+            extraConfig = ''try_files $uri $uri/ /index.php?$query_string;'';
+          };
+          "~ \.php$" = {
+            extraConfig = ''
+              try_files $uri $uri/ /index.php?$query_string;
+              include ${config.services.nginx.package}/conf/fastcgi_params;
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param REDIRECT_STATUS 200;
+              fastcgi_pass unix:${config.services.phpfpm.pools."snipe-it".socket};
+              ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
+            '';
+          };
+          "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
+            extraConfig = "expires 365d;";
+          };
+        };
+      }];
+    };
+
+    systemd.services.snipe-it-setup = {
+      description = "Preperation tasks for snipe-it";
+      before = [ "phpfpm-snipe-it.service" ];
+      after = optional db.createLocally "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        User = user;
+        WorkingDirectory = snipe-it;
+        RuntimeDirectory = "snipe-it/cache";
+        RuntimeDirectoryMode = 0700;
+      };
+      path = [ pkgs.replace-secret ];
+      script =
+        let
+          isSecret  = v: isAttrs v && v ? _secret && (isString v._secret || builtins.isPath v._secret);
+          snipeITEnvVars = lib.generators.toKeyValue {
+            mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
+              mkValueString = v: with builtins;
+                if isInt             v then toString v
+                else if isString     v then "\"${v}\""
+                else if true  ==     v then "true"
+                else if false ==     v then "false"
+                else if isSecret     v then
+                  if (isString v._secret) then
+                    hashString "sha256" v._secret
+                  else
+                    hashString "sha256" (builtins.readFile v._secret)
+                else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+            };
+          };
+          secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
+          mkSecretReplacement = file: ''
+            replace-secret ${escapeShellArgs [
+              (
+                if (isString file) then
+                  builtins.hashString "sha256" file
+                else
+                  builtins.hashString "sha256" (builtins.readFile file)
+              )
+              file
+              "${cfg.dataDir}/.env"
+            ]}
+          '';
+          secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
+          filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
+          snipeITEnv = pkgs.writeText "snipeIT.env" (snipeITEnvVars filteredConfig);
+        in ''
+          # error handling
+          set -euo pipefail
+
+          # set permissions
+          umask 077
+
+          # create .env file
+          install -T -m 0600 -o ${user} ${snipeITEnv} "${cfg.dataDir}/.env"
+
+          # replace secrets
+          ${secretReplacements}
+
+          # prepend `base64:` if it does not exist in APP_KEY
+          if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
+              sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
+          fi
+
+          # purge cache
+          rm "${cfg.dataDir}"/bootstrap/cache/*.php || true
+
+          # migrate db
+          ${pkgs.php}/bin/php artisan migrate --force
+        '';
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.dataDir}                            0710 ${user} ${group} - -"
+      "d ${cfg.dataDir}/bootstrap                  0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/bootstrap/cache            0750 ${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} - -"
+      "d ${cfg.dataDir}/storage/private_uploads    0700 ${user} ${group} - -"
+    ];
+
+    users = {
+      users = mkIf (user == "snipeit") {
+        snipeit = {
+          inherit group;
+          isSystemUser = true;
+        };
+        "${config.services.nginx.user}".extraGroups = [ group ];
+      };
+      groups = mkIf (group == "snipeit") {
+        snipeit = {};
+      };
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ yayayayaka ];
+}
diff --git a/nixpkgs/nixos/modules/services/web-apps/sogo.nix b/nixpkgs/nixos/modules/services/web-apps/sogo.nix
index 4610bb96cb5e..a134282de83a 100644
--- a/nixpkgs/nixos/modules/services/web-apps/sogo.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/sogo.nix
@@ -21,31 +21,31 @@ in {
     enable = mkEnableOption "SOGo groupware";
 
     vhostName = mkOption {
-      description = "Name of the nginx vhost";
+      description = lib.mdDoc "Name of the nginx vhost";
       type = str;
       default = "sogo";
     };
 
     timezone = mkOption {
-      description = "Timezone of your SOGo instance";
+      description = lib.mdDoc "Timezone of your SOGo instance";
       type = str;
       example = "America/Montreal";
     };
 
     language = mkOption {
-      description = "Language of SOGo";
+      description = lib.mdDoc "Language of SOGo";
       type = str;
       default = "English";
     };
 
     ealarmsCredFile = mkOption {
-      description = "Optional path to a credentials file for email alarms";
+      description = lib.mdDoc "Optional path to a credentials file for email alarms";
       type = nullOr str;
       default = null;
     };
 
     configReplaces = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         Replacement-filepath mapping for sogo.conf.
         Every key is replaced with the contents of the file specified as value.
 
@@ -60,7 +60,7 @@ in {
     };
 
     extraConfig = mkOption {
-      description = "Extra sogo.conf configuration lines";
+      description = lib.mdDoc "Extra sogo.conf configuration lines";
       type = lines;
       default = "";
     };
diff --git a/nixpkgs/nixos/modules/services/web-apps/timetagger.nix b/nixpkgs/nixos/modules/services/web-apps/timetagger.nix
deleted file mode 100644
index 373f4fcd52f8..000000000000
--- a/nixpkgs/nixos/modules/services/web-apps/timetagger.nix
+++ /dev/null
@@ -1,80 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-let
-  inherit (lib) mkEnableOption mkIf mkOption types literalExpression;
-
-  cfg = config.services.timetagger;
-in {
-
-  options = {
-    services.timetagger = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Tag your time, get the insight
-
-          <note><para>
-            This app does not do authentication.
-            You must setup authentication yourself or run it in an environment where
-            only allowed users have access.
-          </para></note>
-        '';
-      };
-
-      bindAddr = mkOption {
-        description = "Address to bind to.";
-        type = types.str;
-        default = "127.0.0.1";
-      };
-
-      port = mkOption {
-        description = "Port to bind to.";
-        type = types.port;
-        default = 8080;
-      };
-
-      package = mkOption {
-        description = ''
-          Use own package for starting timetagger web application.
-
-          The ${literalExpression ''pkgs.timetagger''} package only provides a
-          "run.py" script for the actual package
-          ${literalExpression ''pkgs.python3Packages.timetagger''}.
-
-          If you want to provide a "run.py" script for starting timetagger
-          yourself, you can do so with this option.
-          If you do so, the 'bindAddr' and 'port' options are ignored.
-        '';
-
-        default = pkgs.timetagger.override { addr = cfg.bindAddr; port = cfg.port; };
-        defaultText = literalExpression ''
-          pkgs.timetagger.override {
-            addr = ${cfg.bindAddr};
-            port = ${cfg.port};
-          };
-        '';
-        type = types.package;
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services.timetagger = {
-      description = "Timetagger service";
-      wantedBy = [ "multi-user.target" ];
-
-      serviceConfig = {
-        User = "timetagger";
-        Group = "timetagger";
-        StateDirectory = "timetagger";
-
-        ExecStart = "${cfg.package}/bin/timetagger";
-
-        Restart = "on-failure";
-        RestartSec = 1;
-      };
-    };
-  };
-}
-
diff --git a/nixpkgs/nixos/modules/services/web-apps/trilium.nix b/nixpkgs/nixos/modules/services/web-apps/trilium.nix
index 35383c992fe8..bb1061cf278e 100644
--- a/nixpkgs/nixos/modules/services/web-apps/trilium.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/trilium.nix
@@ -10,6 +10,7 @@ let
     # Disable automatically generating desktop icon
     noDesktopIcon=true
     noBackup=${lib.boolToString cfg.noBackup}
+    noAuthentication=${lib.boolToString cfg.noAuthentication}
 
     [Network]
     # host setting is relevant only for web deployments - set the host on which the server will listen
@@ -28,7 +29,7 @@ in
     dataDir = mkOption {
       type = types.str;
       default = "/var/lib/trilium";
-      description = ''
+      description = lib.mdDoc ''
         The directory storing the notes database and the configuration.
       '';
     };
@@ -36,7 +37,7 @@ in
     instanceName = mkOption {
       type = types.str;
       default = "Trilium";
-      description = ''
+      description = lib.mdDoc ''
         Instance name used to distinguish between different instances
       '';
     };
@@ -44,15 +45,23 @@ in
     noBackup = mkOption {
       type = types.bool;
       default = false;
-      description = ''
+      description = lib.mdDoc ''
         Disable periodic database backups.
       '';
     };
 
+    noAuthentication = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        If set to true, no password is required to access the web frontend.
+      '';
+    };
+
     host = mkOption {
       type = types.str;
       default = "127.0.0.1";
-      description = ''
+      description = lib.mdDoc ''
         The host address to bind to (defaults to localhost).
       '';
     };
@@ -60,14 +69,14 @@ in
     port = mkOption {
       type = types.int;
       default = 8080;
-      description = ''
+      description = lib.mdDoc ''
         The port number to bind to.
       '';
     };
 
     nginx = mkOption {
       default = {};
-      description = ''
+      description = lib.mdDoc ''
         Configuration for nginx reverse proxy.
       '';
 
@@ -76,14 +85,14 @@ in
           enable = mkOption {
             type = types.bool;
             default = false;
-            description = ''
+            description = lib.mdDoc ''
               Configure the nginx reverse proxy settings.
             '';
           };
 
           hostName = mkOption {
             type = types.str;
-            description = ''
+            description = lib.mdDoc ''
               The hostname use to setup the virtualhost configuration
             '';
           };
diff --git a/nixpkgs/nixos/modules/services/web-apps/tt-rss.nix b/nixpkgs/nixos/modules/services/web-apps/tt-rss.nix
index 9aa38ab25c9a..f105b0aa3f72 100644
--- a/nixpkgs/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/tt-rss.nix
@@ -126,7 +126,7 @@ let
       root = mkOption {
         type = types.path;
         default = "/var/lib/tt-rss";
-        description = ''
+        description = lib.mdDoc ''
           Root of the application.
         '';
       };
@@ -134,7 +134,7 @@ let
       user = mkOption {
         type = types.str;
         default = "tt_rss";
-        description = ''
+        description = lib.mdDoc ''
           User account under which both the update daemon and the web-application run.
         '';
       };
@@ -142,7 +142,7 @@ let
       pool = mkOption {
         type = types.str;
         default = "${poolName}";
-        description = ''
+        description = lib.mdDoc ''
           Name of existing phpfpm pool that is used to run web-application.
           If not specified a pool will be created automatically with
           default values.
@@ -152,7 +152,7 @@ let
       virtualHost = mkOption {
         type = types.nullOr types.str;
         default = "tt-rss";
-        description = ''
+        description = lib.mdDoc ''
           Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
         '';
       };
@@ -161,7 +161,7 @@ let
         type = mkOption {
           type = types.enum ["pgsql" "mysql"];
           default = "pgsql";
-          description = ''
+          description = lib.mdDoc ''
             Database to store feeds. Supported are pgsql and mysql.
           '';
         };
@@ -169,7 +169,7 @@ let
         host = mkOption {
           type = types.nullOr types.str;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             Host of the database. Leave null to use Unix domain socket.
           '';
         };
@@ -177,7 +177,7 @@ let
         name = mkOption {
           type = types.str;
           default = "tt_rss";
-          description = ''
+          description = lib.mdDoc ''
             Name of the existing database.
           '';
         };
@@ -185,7 +185,7 @@ let
         user = mkOption {
           type = types.str;
           default = "tt_rss";
-          description = ''
+          description = lib.mdDoc ''
             The database user. The user must exist and has access to
             the specified database.
           '';
@@ -194,7 +194,7 @@ let
         password = mkOption {
           type = types.nullOr types.str;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             The database user's password.
           '';
         };
@@ -202,7 +202,7 @@ let
         passwordFile = mkOption {
           type = types.nullOr types.str;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             The database user's password.
           '';
         };
@@ -210,7 +210,7 @@ let
         port = mkOption {
           type = types.nullOr types.int;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             The database's port. If not set, the default ports will be provided (5432
             and 3306 for pgsql and mysql respectively).
           '';
@@ -219,7 +219,7 @@ let
         createLocally = mkOption {
           type = types.bool;
           default = true;
-          description = "Create the database and database user locally.";
+          description = lib.mdDoc "Create the database and database user locally.";
         };
       };
 
@@ -227,7 +227,7 @@ let
         autoCreate = mkOption {
           type = types.bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             Allow authentication modules to auto-create users in tt-rss internal
             database when authenticated successfully.
           '';
@@ -236,7 +236,7 @@ let
         autoLogin = mkOption {
           type = types.bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             Automatically login user on remote or other kind of externally supplied
             authentication, otherwise redirect to login form as normal.
             If set to true, users won't be able to set application language
@@ -249,7 +249,7 @@ let
         hub = mkOption {
           type = types.str;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             URL to a PubSubHubbub-compatible hub server. If defined, "Published
             articles" generated feed would automatically become PUSH-enabled.
           '';
@@ -258,7 +258,7 @@ let
         enable = mkOption {
           type = types.bool;
           default = false;
-          description = ''
+          description = lib.mdDoc ''
             Enable client PubSubHubbub support in tt-rss. When disabled, tt-rss
             won't try to subscribe to PUSH feed updates.
           '';
@@ -269,7 +269,7 @@ let
         server = mkOption {
           type = types.str;
           default = "localhost:9312";
-          description = ''
+          description = lib.mdDoc ''
             Hostname:port combination for the Sphinx server.
           '';
         };
@@ -277,7 +277,7 @@ let
         index = mkOption {
           type = types.listOf types.str;
           default = ["ttrss" "delta"];
-          description = ''
+          description = lib.mdDoc ''
             Index names in Sphinx configuration. Example configuration
             files are available on tt-rss wiki.
           '';
@@ -288,7 +288,7 @@ let
         enable = mkOption {
           type = types.bool;
           default = false;
-          description = ''
+          description = lib.mdDoc ''
             Allow users to register themselves. Please be aware that allowing
             random people to access your tt-rss installation is a security risk
             and potentially might lead to data loss or server exploit. Disabled
@@ -299,7 +299,7 @@ let
         notifyAddress = mkOption {
           type = types.str;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             Email address to send new user notifications to.
           '';
         };
@@ -307,7 +307,7 @@ let
         maxUsers = mkOption {
           type = types.int;
           default = 0;
-          description = ''
+          description = lib.mdDoc ''
             Maximum amount of users which will be allowed to register on this
             system. 0 - no limit.
           '';
@@ -319,7 +319,7 @@ let
           type = types.str;
           default = "";
           example = "localhost:25";
-          description = ''
+          description = lib.mdDoc ''
             Hostname:port combination to send outgoing mail. Blank - use system
             MTA.
           '';
@@ -328,7 +328,7 @@ let
         login = mkOption {
           type = types.str;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             SMTP authentication login used when sending outgoing mail.
           '';
         };
@@ -336,7 +336,7 @@ let
         password = mkOption {
           type = types.str;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             SMTP authentication password used when sending outgoing mail.
           '';
         };
@@ -344,7 +344,7 @@ let
         security = mkOption {
           type = types.enum ["" "ssl" "tls"];
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             Used to select a secure SMTP connection. Allowed values: ssl, tls,
             or empty.
           '';
@@ -353,7 +353,7 @@ let
         fromName = mkOption {
           type = types.str;
           default = "Tiny Tiny RSS";
-          description = ''
+          description = lib.mdDoc ''
             Name for sending outgoing mail. This applies to password reset
             notifications, digest emails and any other mail.
           '';
@@ -362,7 +362,7 @@ let
         fromAddress = mkOption {
           type = types.str;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             Address for sending outgoing mail. This applies to password reset
             notifications, digest emails and any other mail.
           '';
@@ -371,7 +371,7 @@ let
         digestSubject = mkOption {
           type = types.str;
           default = "[tt-rss] New headlines for last 24 hours";
-          description = ''
+          description = lib.mdDoc ''
             Subject line for email digests.
           '';
         };
@@ -380,7 +380,7 @@ let
       sessionCookieLifetime = mkOption {
         type = types.int;
         default = 86400;
-        description = ''
+        description = lib.mdDoc ''
           Default lifetime of a session (e.g. login) cookie. In seconds,
           0 means cookie will be deleted when browser closes.
         '';
@@ -388,7 +388,7 @@ let
 
       selfUrlPath = mkOption {
         type = types.str;
-        description = ''
+        description = lib.mdDoc ''
           Full URL of your tt-rss installation. This should be set to the
           location of tt-rss directory, e.g. http://example.org/tt-rss/
           You need to set this option correctly otherwise several features
@@ -400,7 +400,7 @@ let
       feedCryptKey = mkOption {
         type = types.str;
         default = "";
-        description = ''
+        description = lib.mdDoc ''
           Key used for encryption of passwords for password-protected feeds
           in the database. A string of 24 random characters. If left blank, encryption
           is not used. Requires mcrypt functions.
@@ -413,7 +413,7 @@ let
         type = types.bool;
         default = false;
 
-        description = ''
+        description = lib.mdDoc ''
           Operate in single user mode, disables all functionality related to
           multiple users and authentication. Enabling this assumes you have
           your tt-rss directory protected by other means (e.g. http auth).
@@ -423,7 +423,7 @@ let
       simpleUpdateMode = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Enables fallback update mode where tt-rss tries to update feeds in
           background while tt-rss is open in your browser.
           If you don't have a lot of feeds and don't want to or can't run
@@ -437,7 +437,7 @@ let
       forceArticlePurge = mkOption {
         type = types.int;
         default = 0;
-        description = ''
+        description = lib.mdDoc ''
           When this option is not 0, users ability to control feed purging
           intervals is disabled and all articles (which are not starred)
           older than this amount of days are purged.
@@ -447,7 +447,7 @@ let
       enableGZipOutput = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = lib.mdDoc ''
           Selectively gzip output to improve wire performance. This requires
           PHP Zlib extension on the server.
           Enabling this can break tt-rss in several httpd/php configurations,
@@ -459,7 +459,7 @@ let
       plugins = mkOption {
         type = types.listOf types.str;
         default = ["auth_internal" "note"];
-        description = ''
+        description = lib.mdDoc ''
           List of plugins to load automatically for all users.
           System plugins have to be specified here. Please enable at least one
           authentication plugin here (auth_*).
@@ -473,27 +473,27 @@ let
       pluginPackages = mkOption {
         type = types.listOf types.package;
         default = [];
-        description = ''
+        description = lib.mdDoc ''
           List of plugins to install. The list elements are expected to
           be derivations. All elements in this derivation are automatically
-          copied to the <literal>plugins.local</literal> directory.
+          copied to the `plugins.local` directory.
         '';
       };
 
       themePackages = mkOption {
         type = types.listOf types.package;
         default = [];
-        description = ''
+        description = lib.mdDoc ''
           List of themes to install. The list elements are expected to
           be derivations. All elements in this derivation are automatically
-          copied to the <literal>themes.local</literal> directory.
+          copied to the `themes.local` directory.
         '';
       };
 
       logDestination = mkOption {
         type = types.enum ["" "sql" "syslog"];
         default = "sql";
-        description = ''
+        description = lib.mdDoc ''
           Log destination to use. Possible values: sql (uses internal logging
           you can read in Preferences -> System), syslog - logs to system log.
           Setting this to blank uses PHP logging (usually to http server
@@ -504,8 +504,8 @@ let
       extraConfig = mkOption {
         type = types.lines;
         default = "";
-        description = ''
-          Additional lines to append to <literal>config.php</literal>.
+        description = lib.mdDoc ''
+          Additional lines to append to `config.php`.
         '';
       };
     };
@@ -534,6 +534,7 @@ let
     services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
       ${poolName} = {
         inherit (cfg) user;
+        phpPackage = pkgs.php80;
         settings = mapAttrs (name: mkDefault) {
           "listen.owner" = "nginx";
           "listen.group" = "nginx";
diff --git a/nixpkgs/nixos/modules/services/web-apps/vikunja.nix b/nixpkgs/nixos/modules/services/web-apps/vikunja.nix
index 7575e96ca815..7db610159809 100644
--- a/nixpkgs/nixos/modules/services/web-apps/vikunja.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/vikunja.nix
@@ -15,18 +15,18 @@ in {
       default = pkgs.vikunja-api;
       type = types.package;
       defaultText = literalExpression "pkgs.vikunja-api";
-      description = "vikunja-api derivation to use.";
+      description = lib.mdDoc "vikunja-api derivation to use.";
     };
     package-frontend = mkOption {
       default = pkgs.vikunja-frontend;
       type = types.package;
       defaultText = literalExpression "pkgs.vikunja-frontend";
-      description = "vikunja-frontend derivation to use.";
+      description = lib.mdDoc "vikunja-frontend derivation to use.";
     };
     environmentFiles = mkOption {
       type = types.listOf types.path;
       default = [ ];
-      description = ''
+      description = lib.mdDoc ''
         List of environment files set in the vikunja systemd service.
         For example passwords should be set in one of these files.
       '';
@@ -35,34 +35,34 @@ in {
       type = types.bool;
       default = config.services.nginx.enable;
       defaultText = literalExpression "config.services.nginx.enable";
-      description = ''
+      description = lib.mdDoc ''
         Whether to setup NGINX.
         Further nginx configuration can be done by changing
-        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;</option>.
+        {option}`services.nginx.virtualHosts.<frontendHostname>`.
         This does not enable TLS or ACME by default. To enable this, set the
-        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;.enableACME</option> to
-        <literal>true</literal> and if appropriate do the same for
-        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;.forceSSL</option>.
+        {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 = ''
+      description = lib.mdDoc ''
         Whether the site is available via http or https.
         This does not configure https or ACME in nginx!
       '';
     };
     frontendHostname = mkOption {
       type = types.str;
-      description = "The Hostname under which the frontend is running.";
+      description = lib.mdDoc "The Hostname under which the frontend is running.";
     };
 
     settings = mkOption {
       type = format.type;
       default = {};
-      description = ''
+      description = lib.mdDoc ''
         Vikunja configuration. Refer to
-        <link xlink:href="https://vikunja.io/docs/config-options/"/>
+        <https://vikunja.io/docs/config-options/>
         for details on supported values.
         '';
     };
@@ -71,27 +71,27 @@ in {
         type = types.enum [ "sqlite" "mysql" "postgres" ];
         example = "postgres";
         default = "sqlite";
-        description = "Database engine to use.";
+        description = lib.mdDoc "Database engine to use.";
       };
       host = mkOption {
         type = types.str;
         default = "localhost";
-        description = "Database host address. Can also be a socket.";
+        description = lib.mdDoc "Database host address. Can also be a socket.";
       };
       user = mkOption {
         type = types.str;
         default = "vikunja";
-        description = "Database user.";
+        description = lib.mdDoc "Database user.";
       };
       database = mkOption {
         type = types.str;
         default = "vikunja";
-        description = "Database name.";
+        description = lib.mdDoc "Database name.";
       };
       path = mkOption {
         type = types.str;
         default = "/var/lib/vikunja/vikunja.db";
-        description = "Path to the sqlite3 database file.";
+        description = lib.mdDoc "Path to the sqlite3 database file.";
       };
     };
   };
diff --git a/nixpkgs/nixos/modules/services/web-apps/virtlyst.nix b/nixpkgs/nixos/modules/services/web-apps/virtlyst.nix
deleted file mode 100644
index 37bdbb0e3b42..000000000000
--- a/nixpkgs/nixos/modules/services/web-apps/virtlyst.nix
+++ /dev/null
@@ -1,73 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.virtlyst;
-  stateDir = "/var/lib/virtlyst";
-
-  ini = pkgs.writeText "virtlyst-config.ini" ''
-    [wsgi]
-    master = true
-    threads = auto
-    http-socket = ${cfg.httpSocket}
-    application = ${pkgs.virtlyst}/lib/libVirtlyst.so
-    chdir2 = ${stateDir}
-    static-map = /static=${pkgs.virtlyst}/root/static
-
-    [Cutelyst]
-    production = true
-    DatabasePath = virtlyst.sqlite
-    TemplatePath = ${pkgs.virtlyst}/root/src
-
-    [Rules]
-    cutelyst.* = true
-    virtlyst.* = true
-  '';
-
-in
-
-{
-
-  options.services.virtlyst = {
-    enable = mkEnableOption "Virtlyst libvirt web interface";
-
-    adminPassword = mkOption {
-      type = types.str;
-      description = ''
-        Initial admin password with which the database will be seeded.
-      '';
-    };
-
-    httpSocket = mkOption {
-      type = types.str;
-      default = "localhost:3000";
-      description = ''
-        IP and/or port to which to bind the http socket.
-      '';
-    };
-  };
-
-  config = mkIf cfg.enable {
-    users.users.virtlyst = {
-      home = stateDir;
-      createHome = true;
-      group = mkIf config.virtualisation.libvirtd.enable "libvirtd";
-      isSystemUser = true;
-    };
-
-    systemd.services.virtlyst = {
-      wantedBy = [ "multi-user.target" ];
-      environment = {
-        VIRTLYST_ADMIN_PASSWORD = cfg.adminPassword;
-      };
-      serviceConfig = {
-        ExecStart = "${pkgs.cutelyst}/bin/cutelyst-wsgi2 --ini ${ini}";
-        User = "virtlyst";
-        WorkingDirectory = stateDir;
-      };
-    };
-  };
-
-}
diff --git a/nixpkgs/nixos/modules/services/web-apps/whitebophir.nix b/nixpkgs/nixos/modules/services/web-apps/whitebophir.nix
index f9db6fe379b0..c4dee3c6eec7 100644
--- a/nixpkgs/nixos/modules/services/web-apps/whitebophir.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/whitebophir.nix
@@ -13,19 +13,19 @@ in {
         default = pkgs.whitebophir;
         defaultText = literalExpression "pkgs.whitebophir";
         type = types.package;
-        description = "Whitebophir package to use.";
+        description = lib.mdDoc "Whitebophir package to use.";
       };
 
       listenAddress = mkOption {
         type = types.str;
         default = "0.0.0.0";
-        description = "Address to listen on (use 0.0.0.0 to allow access from any address).";
+        description = lib.mdDoc "Address to listen on (use 0.0.0.0 to allow access from any address).";
       };
 
       port = mkOption {
         type = types.port;
         default = 5001;
-        description = "Port to bind to.";
+        description = lib.mdDoc "Port to bind to.";
       };
     };
   };
diff --git a/nixpkgs/nixos/modules/services/web-apps/wiki-js.nix b/nixpkgs/nixos/modules/services/web-apps/wiki-js.nix
index 1a6259dffeef..5dc0bb73259b 100644
--- a/nixpkgs/nixos/modules/services/web-apps/wiki-js.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/wiki-js.nix
@@ -16,7 +16,7 @@ in {
       type = types.nullOr types.path;
       default = null;
       example = "/root/wiki-js.env";
-      description = ''
+      description = lib.mdDoc ''
         Environment fiel to inject e.g. secrets into the configuration.
       '';
     };
@@ -24,8 +24,8 @@ in {
     stateDirectoryName = mkOption {
       default = "wiki-js";
       type = types.str;
-      description = ''
-        Name of the directory in <filename>/var/lib</filename>.
+      description = lib.mdDoc ''
+        Name of the directory in {file}`/var/lib`.
       '';
     };
 
@@ -37,7 +37,7 @@ in {
           port = mkOption {
             type = types.port;
             default = 3000;
-            description = ''
+            description = lib.mdDoc ''
               TCP port the process should listen to.
             '';
           };
@@ -45,7 +45,7 @@ in {
           bindIP = mkOption {
             default = "0.0.0.0";
             type = types.str;
-            description = ''
+            description = lib.mdDoc ''
               IPs the service should listen to.
             '';
           };
@@ -64,14 +64,14 @@ in {
             host = mkOption {
               type = types.str;
               example = "/run/postgresql";
-              description = ''
+              description = lib.mdDoc ''
                 Hostname or socket-path to connect to.
               '';
             };
             db = mkOption {
               default = "wiki";
               type = types.str;
-              description = ''
+              description = lib.mdDoc ''
                 Name of the database to use.
               '';
             };
@@ -80,7 +80,7 @@ in {
           logLevel = mkOption {
             default = "info";
             type = types.enum [ "error" "warn" "info" "verbose" "debug" "silly" ];
-            description = ''
+            description = lib.mdDoc ''
               Define how much detail is supposed to be logged at runtime.
             '';
           };
@@ -95,12 +95,11 @@ in {
       };
       description = ''
         Settings to configure <package>wiki-js</package>. This directly
-        corresponds to <link xlink:href="https://docs.requarks.io/install/config">the upstream
-        configuration options</link>.
+        corresponds to <link xlink:href="https://docs.requarks.io/install/config">the upstream configuration options</link>.
 
         Secrets can be injected via the environment by
         <itemizedlist>
-          <listitem><para>specifying <xref linkend="opt-services.wiki-js.environmentFile" />
+          <listitem><para>specifying <xref linkend="opt-services.wiki-js.environmentFile"/>
           to contain secrets</para></listitem>
           <listitem><para>and setting sensitive values to <literal>$(ENVIRONMENT_VAR)</literal>
           with this value defined in the environment-file.</para></listitem>
diff --git a/nixpkgs/nixos/modules/services/web-apps/wordpress.nix b/nixpkgs/nixos/modules/services/web-apps/wordpress.nix
index 59471a739cbb..c841ded353e7 100644
--- a/nixpkgs/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/wordpress.nix
@@ -82,13 +82,13 @@ let
           type = types.package;
           default = pkgs.wordpress;
           defaultText = literalExpression "pkgs.wordpress";
-          description = "Which WordPress package to use.";
+          description = lib.mdDoc "Which WordPress package to use.";
         };
 
         uploadsDir = mkOption {
           type = types.path;
           default = "/var/lib/wordpress/${name}/uploads";
-          description = ''
+          description = lib.mdDoc ''
             This directory is used for uploads of pictures. The directory passed here is automatically
             created and permissions adjusted as required.
           '';
@@ -152,47 +152,47 @@ let
           host = mkOption {
             type = types.str;
             default = "localhost";
-            description = "Database host address.";
+            description = lib.mdDoc "Database host address.";
           };
 
           port = mkOption {
             type = types.port;
             default = 3306;
-            description = "Database host port.";
+            description = lib.mdDoc "Database host port.";
           };
 
           name = mkOption {
             type = types.str;
             default = "wordpress";
-            description = "Database name.";
+            description = lib.mdDoc "Database name.";
           };
 
           user = mkOption {
             type = types.str;
             default = "wordpress";
-            description = "Database user.";
+            description = lib.mdDoc "Database user.";
           };
 
           passwordFile = mkOption {
             type = types.nullOr types.path;
             default = null;
             example = "/run/keys/wordpress-dbpassword";
-            description = ''
+            description = lib.mdDoc ''
               A file containing the password corresponding to
-              <option>database.user</option>.
+              {option}`database.user`.
             '';
           };
 
           tablePrefix = mkOption {
             type = types.str;
             default = "wp_";
-            description = ''
+            description = lib.mdDoc ''
               The $table_prefix is the value placed in the front of your database tables.
               Change the value if you want to use something other than wp_ for your database
               prefix. Typically this is changed if you are installing multiple WordPress blogs
               in the same database.
 
-              See <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php#table_prefix'/>.
+              See <https://codex.wordpress.org/Editing_wp-config.php#table_prefix>.
             '';
           };
 
@@ -200,13 +200,13 @@ let
             type = types.nullOr types.path;
             default = null;
             defaultText = literalExpression "/run/mysqld/mysqld.sock";
-            description = "Path to the unix socket file to use for authentication.";
+            description = lib.mdDoc "Path to the unix socket file to use for authentication.";
           };
 
           createLocally = mkOption {
             type = types.bool;
             default = true;
-            description = "Create the database and database user locally.";
+            description = lib.mdDoc "Create the database and database user locally.";
           };
         };
 
@@ -219,8 +219,8 @@ let
               enableACME = true;
             }
           '';
-          description = ''
-            Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
+          description = lib.mdDoc ''
+            Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
           '';
         };
 
@@ -234,8 +234,8 @@ let
             "pm.max_spare_servers" = 4;
             "pm.max_requests" = 500;
           };
-          description = ''
-            Options for the WordPress PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+          description = lib.mdDoc ''
+            Options for the WordPress PHP pool. See the documentation on `php-fpm.conf`
             for details on configuration directives.
           '';
         };
@@ -243,10 +243,10 @@ let
         extraConfig = mkOption {
           type = types.lines;
           default = "";
-          description = ''
+          description = lib.mdDoc ''
             Any additional text to be appended to the wp-config.php
             configuration file. This is a PHP script. For configuration
-            settings, see <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php'/>.
+            settings, see <https://codex.wordpress.org/Editing_wp-config.php>.
           '';
           example = ''
             define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds
@@ -265,20 +265,20 @@ in
       sites = mkOption {
         type = types.attrsOf (types.submodule siteOpts);
         default = {};
-        description = "Specification of one or more WordPress sites to serve";
+        description = lib.mdDoc "Specification of one or more WordPress sites to serve";
       };
 
       webserver = mkOption {
         type = types.enum [ "httpd" "nginx" "caddy" ];
         default = "httpd";
-        description = ''
+        description = lib.mdDoc ''
           Whether to use apache2 or nginx for virtual host management.
 
-          Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
-          See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+          Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
+          See [](#opt-services.nginx.virtualHosts) for further information.
 
-          Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
-          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+          Further apache2 configuration can be done by adapting `services.httpd.virtualHosts.<name>`.
+          See [](#opt-services.httpd.virtualHosts) for further information.
         '';
       };
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/youtrack.nix b/nixpkgs/nixos/modules/services/web-apps/youtrack.nix
index b83265ffeab6..789880d61f61 100644
--- a/nixpkgs/nixos/modules/services/web-apps/youtrack.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/youtrack.nix
@@ -24,7 +24,7 @@ in
     enable = mkEnableOption "YouTrack service";
 
     address = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         The interface youtrack will listen on.
       '';
       default = "127.0.0.1";
@@ -32,7 +32,7 @@ in
     };
 
     baseUrl = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         Base URL for youtrack. Will be auto-detected and stored in database.
       '';
       type = types.nullOr types.str;
@@ -41,7 +41,7 @@ in
 
     extraParams = mkOption {
       default = {};
-      description = ''
+      description = lib.mdDoc ''
         Extra parameters to pass to youtrack. See
         https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Java-Start-Parameters.html
         for more information.
@@ -55,7 +55,7 @@ in
     };
 
     package = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         Package to use.
       '';
       type = types.package;
@@ -64,7 +64,7 @@ in
     };
 
     port = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         The port youtrack will listen on.
       '';
       default = 8080;
@@ -72,7 +72,7 @@ in
     };
 
     statePath = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         Where to keep the youtrack database.
       '';
       type = types.path;
@@ -80,7 +80,7 @@ in
     };
 
     virtualHost = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         Name of the nginx virtual host to use and setup.
         If null, do not setup anything.
       '';
@@ -89,7 +89,7 @@ in
     };
 
     jvmOpts = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         Extra options to pass to the JVM.
         See https://www.jetbrains.com/help/youtrack/standalone/Configure-JVM-Options.html
         for more information.
@@ -100,7 +100,7 @@ in
     };
 
     maxMemory = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         Maximum Java heap size
       '';
       type = types.str;
@@ -108,7 +108,7 @@ in
     };
 
     maxMetaspaceSize = mkOption {
-      description = ''
+      description = lib.mdDoc ''
         Maximum java Metaspace memory.
       '';
       type = types.str;
diff --git a/nixpkgs/nixos/modules/services/web-apps/zabbix.nix b/nixpkgs/nixos/modules/services/web-apps/zabbix.nix
index 538dac0d5be2..c6ac809a73b0 100644
--- a/nixpkgs/nixos/modules/services/web-apps/zabbix.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/zabbix.nix
@@ -46,19 +46,19 @@ in
         type = types.package;
         default = pkgs.zabbix.web;
         defaultText = literalExpression "zabbix.web";
-        description = "Which Zabbix package to use.";
+        description = lib.mdDoc "Which Zabbix package to use.";
       };
 
       server = {
         port = mkOption {
           type = types.int;
-          description = "The port of the Zabbix server to connect to.";
+          description = lib.mdDoc "The port of the Zabbix server to connect to.";
           default = 10051;
         };
 
         address = mkOption {
           type = types.str;
-          description = "The IP address or hostname of the Zabbix server to connect to.";
+          description = lib.mdDoc "The IP address or hostname of the Zabbix server to connect to.";
           default = "localhost";
         };
       };
@@ -68,13 +68,13 @@ in
           type = types.enum [ "mysql" "pgsql" "oracle" ];
           example = "mysql";
           default = "pgsql";
-          description = "Database engine to use.";
+          description = lib.mdDoc "Database engine to use.";
         };
 
         host = mkOption {
           type = types.str;
           default = "";
-          description = "Database host address.";
+          description = lib.mdDoc "Database host address.";
         };
 
         port = mkOption {
@@ -88,28 +88,28 @@ in
             else if config.${opt.database.type} == "pgsql" then config.${options.services.postgresql.port}
             else 1521
           '';
-          description = "Database host port.";
+          description = lib.mdDoc "Database host port.";
         };
 
         name = mkOption {
           type = types.str;
           default = "zabbix";
-          description = "Database name.";
+          description = lib.mdDoc "Database name.";
         };
 
         user = mkOption {
           type = types.str;
           default = "zabbix";
-          description = "Database user.";
+          description = lib.mdDoc "Database user.";
         };
 
         passwordFile = mkOption {
           type = types.nullOr types.path;
           default = null;
           example = "/run/keys/zabbix-dbpassword";
-          description = ''
+          description = lib.mdDoc ''
             A file containing the password corresponding to
-            <option>database.user</option>.
+            {option}`database.user`.
           '';
         };
 
@@ -117,7 +117,7 @@ in
           type = types.nullOr types.path;
           default = null;
           example = "/run/postgresql";
-          description = "Path to the unix socket file to use for authentication.";
+          description = lib.mdDoc "Path to the unix socket file to use for authentication.";
         };
       };
 
@@ -131,9 +131,9 @@ in
             enableACME = true;
           }
         '';
-        description = ''
-          Apache configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
-          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+        description = lib.mdDoc ''
+          Apache configuration can be done by adapting `services.httpd.virtualHosts.<name>`.
+          See [](#opt-services.httpd.virtualHosts) for further information.
         '';
       };
 
@@ -147,16 +147,16 @@ in
           "pm.max_spare_servers" = 4;
           "pm.max_requests" = 500;
         };
-        description = ''
-          Options for the Zabbix PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
+        description = lib.mdDoc ''
+          Options for the Zabbix PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
         '';
       };
 
       extraConfig = mkOption {
         type = types.lines;
         default = "";
-        description = ''
-          Additional configuration to be copied verbatim into <filename>zabbix.conf.php</filename>.
+        description = lib.mdDoc ''
+          Additional configuration to be copied verbatim into {file}`zabbix.conf.php`.
         '';
       };