about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/web-apps
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/modules/services/web-apps')
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/akkoma.md284
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/c2fmzq-server.md4
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/discourse.md228
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/gotosocial.md52
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/grocy.md6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/jitsi-meet.md4
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/kavita.nix63
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/keycloak.md24
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/lemmy.md14
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/nextcloud.md6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/peertube.nix292
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/pict-rs.md4
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/plausible.md2
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/pretix.nix2
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/slskd.nix326
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/suwayomi-server.md2
16 files changed, 747 insertions, 566 deletions
diff --git a/nixpkgs/nixos/modules/services/web-apps/akkoma.md b/nixpkgs/nixos/modules/services/web-apps/akkoma.md
index 83dd1a8b35f2..13b074b228a4 100644
--- a/nixpkgs/nixos/modules/services/web-apps/akkoma.md
+++ b/nixpkgs/nixos/modules/services/web-apps/akkoma.md
@@ -19,21 +19,23 @@ be run behind a HTTP proxy on `fediverse.example.com`.
 
 
 ```nix
-services.akkoma.enable = true;
-services.akkoma.config = {
-  ":pleroma" = {
-    ":instance" = {
-      name = "My Akkoma instance";
-      description = "More detailed description";
-      email = "admin@example.com";
-      registration_open = false;
-    };
-
-    "Pleroma.Web.Endpoint" = {
-      url.host = "fediverse.example.com";
+{
+  services.akkoma.enable = true;
+  services.akkoma.config = {
+    ":pleroma" = {
+      ":instance" = {
+        name = "My Akkoma instance";
+        description = "More detailed description";
+        email = "admin@example.com";
+        registration_open = false;
+      };
+
+      "Pleroma.Web.Endpoint" = {
+        url.host = "fediverse.example.com";
+      };
     };
   };
-};
+}
 ```
 
 Please refer to the [configuration cheat sheet](https://docs.akkoma.dev/stable/configuration/cheatsheet/)
@@ -55,19 +57,21 @@ Although it is possible to expose Akkoma directly, it is common practice to oper
 HTTP reverse proxy such as nginx.
 
 ```nix
-services.akkoma.nginx = {
-  enableACME = true;
-  forceSSL = true;
-};
-
-services.nginx = {
-  enable = true;
-
-  clientMaxBodySize = "16m";
-  recommendedTlsSettings = true;
-  recommendedOptimisation = true;
-  recommendedGzipSettings = true;
-};
+{
+  services.akkoma.nginx = {
+    enableACME = true;
+    forceSSL = true;
+  };
+
+  services.nginx = {
+    enable = true;
+
+    clientMaxBodySize = "16m";
+    recommendedTlsSettings = true;
+    recommendedOptimisation = true;
+    recommendedGzipSettings = true;
+  };
+}
 ```
 
 Please refer to [](#module-security-acme) for details on how to provision an SSL/TLS certificate.
@@ -78,51 +82,53 @@ Without the media proxy function, Akkoma does not store any remote media like pi
 locally, and clients have to fetch them directly from the source server.
 
 ```nix
-# Enable nginx slice module distributed with Tengine
-services.nginx.package = pkgs.tengine;
-
-# Enable media proxy
-services.akkoma.config.":pleroma".":media_proxy" = {
-  enabled = true;
-  proxy_opts.redirect_on_failure = true;
-};
-
-# Adjust the persistent cache size as needed:
-#  Assuming an average object size of 128 KiB, around 1 MiB
-#  of memory is required for the key zone per GiB of cache.
-# Ensure that the cache directory exists and is writable by nginx.
-services.nginx.commonHttpConfig = ''
-  proxy_cache_path /var/cache/nginx/cache/akkoma-media-cache
-    levels= keys_zone=akkoma_media_cache:16m max_size=16g
-    inactive=1y use_temp_path=off;
-'';
-
-services.akkoma.nginx = {
-  locations."/proxy" = {
-    proxyPass = "http://unix:/run/akkoma/socket";
-
-    extraConfig = ''
-      proxy_cache akkoma_media_cache;
-
-      # Cache objects in slices of 1 MiB
-      slice 1m;
-      proxy_cache_key $host$uri$is_args$args$slice_range;
-      proxy_set_header Range $slice_range;
-
-      # Decouple proxy and upstream responses
-      proxy_buffering on;
-      proxy_cache_lock on;
-      proxy_ignore_client_abort on;
-
-      # Default cache times for various responses
-      proxy_cache_valid 200 1y;
-      proxy_cache_valid 206 301 304 1h;
-
-      # Allow serving of stale items
-      proxy_cache_use_stale error timeout invalid_header updating;
-    '';
+{
+  # Enable nginx slice module distributed with Tengine
+  services.nginx.package = pkgs.tengine;
+
+  # Enable media proxy
+  services.akkoma.config.":pleroma".":media_proxy" = {
+    enabled = true;
+    proxy_opts.redirect_on_failure = true;
   };
-};
+
+  # Adjust the persistent cache size as needed:
+  #  Assuming an average object size of 128 KiB, around 1 MiB
+  #  of memory is required for the key zone per GiB of cache.
+  # Ensure that the cache directory exists and is writable by nginx.
+  services.nginx.commonHttpConfig = ''
+    proxy_cache_path /var/cache/nginx/cache/akkoma-media-cache
+      levels= keys_zone=akkoma_media_cache:16m max_size=16g
+      inactive=1y use_temp_path=off;
+  '';
+
+  services.akkoma.nginx = {
+    locations."/proxy" = {
+      proxyPass = "http://unix:/run/akkoma/socket";
+
+      extraConfig = ''
+        proxy_cache akkoma_media_cache;
+
+        # Cache objects in slices of 1 MiB
+        slice 1m;
+        proxy_cache_key $host$uri$is_args$args$slice_range;
+        proxy_set_header Range $slice_range;
+
+        # Decouple proxy and upstream responses
+        proxy_buffering on;
+        proxy_cache_lock on;
+        proxy_ignore_client_abort on;
+
+        # Default cache times for various responses
+        proxy_cache_valid 200 1y;
+        proxy_cache_valid 206 301 304 1h;
+
+        # Allow serving of stale items
+        proxy_cache_use_stale error timeout invalid_header updating;
+      '';
+    };
+  };
+}
 ```
 
 #### Prefetch remote media {#modules-services-akkoma-prefetch-remote-media}
@@ -132,10 +138,12 @@ fetches all media associated with a post through the media proxy, as soon as the
 received by the instance.
 
 ```nix
-services.akkoma.config.":pleroma".":mrf".policies =
-  map (pkgs.formats.elixirConf { }).lib.mkRaw [
-    "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy"
-];
+{
+  services.akkoma.config.":pleroma".":mrf".policies =
+    map (pkgs.formats.elixirConf { }).lib.mkRaw [
+      "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy"
+  ];
+}
 ```
 
 #### Media previews {#modules-services-akkoma-media-previews}
@@ -143,11 +151,13 @@ services.akkoma.config.":pleroma".":mrf".policies =
 Akkoma can generate previews for media.
 
 ```nix
-services.akkoma.config.":pleroma".":media_preview_proxy" = {
-  enabled = true;
-  thumbnail_max_width = 1920;
-  thumbnail_max_height = 1080;
-};
+{
+  services.akkoma.config.":pleroma".":media_preview_proxy" = {
+    enabled = true;
+    thumbnail_max_width = 1920;
+    thumbnail_max_height = 1080;
+  };
+}
 ```
 
 ## Frontend management {#modules-services-akkoma-frontend-management}
@@ -160,29 +170,31 @@ The following example overrides the primary frontend’s default configuration u
 derivation.
 
 ```nix
-services.akkoma.frontends.primary.package = pkgs.runCommand "akkoma-fe" {
-  config = builtins.toJSON {
-    expertLevel = 1;
-    collapseMessageWithSubject = false;
-    stopGifs = false;
-    replyVisibility = "following";
-    webPushHideIfCW = true;
-    hideScopeNotice = true;
-    renderMisskeyMarkdown = false;
-    hideSiteFavicon = true;
-    postContentType = "text/markdown";
-    showNavShortcuts = false;
-  };
-  nativeBuildInputs = with pkgs; [ jq xorg.lndir ];
-  passAsFile = [ "config" ];
-} ''
-  mkdir $out
-  lndir ${pkgs.akkoma-frontends.akkoma-fe} $out
-
-  rm $out/static/config.json
-  jq -s add ${pkgs.akkoma-frontends.akkoma-fe}/static/config.json ${config} \
-    >$out/static/config.json
-'';
+{
+  services.akkoma.frontends.primary.package = pkgs.runCommand "akkoma-fe" {
+    config = builtins.toJSON {
+      expertLevel = 1;
+      collapseMessageWithSubject = false;
+      stopGifs = false;
+      replyVisibility = "following";
+      webPushHideIfCW = true;
+      hideScopeNotice = true;
+      renderMisskeyMarkdown = false;
+      hideSiteFavicon = true;
+      postContentType = "text/markdown";
+      showNavShortcuts = false;
+    };
+    nativeBuildInputs = with pkgs; [ jq xorg.lndir ];
+    passAsFile = [ "config" ];
+  } ''
+    mkdir $out
+    lndir ${pkgs.akkoma-frontends.akkoma-fe} $out
+
+    rm $out/static/config.json
+    jq -s add ${pkgs.akkoma-frontends.akkoma-fe}/static/config.json ${config} \
+      >$out/static/config.json
+  '';
+}
 ```
 
 ## Federation policies {#modules-services-akkoma-federation-policies}
@@ -198,28 +210,30 @@ of the fediverse and providing a pleasant experience to the users of an instance
 
 
 ```nix
-services.akkoma.config.":pleroma" = with (pkgs.formats.elixirConf { }).lib; {
-  ":mrf".policies = map mkRaw [
-    "Pleroma.Web.ActivityPub.MRF.SimplePolicy"
-  ];
-
-  ":mrf_simple" = {
-    # Tag all media as sensitive
-    media_nsfw = mkMap {
-      "nsfw.weird.kinky" = "Untagged NSFW content";
-    };
-
-    # Reject all activities except deletes
-    reject = mkMap {
-      "kiwifarms.cc" = "Persistent harassment of users, no moderation";
-    };
-
-    # Force posts to be visible by followers only
-    followers_only = mkMap {
-      "beta.birdsite.live" = "Avoid polluting timelines with Twitter posts";
+{
+  services.akkoma.config.":pleroma" = with (pkgs.formats.elixirConf { }).lib; {
+    ":mrf".policies = map mkRaw [
+      "Pleroma.Web.ActivityPub.MRF.SimplePolicy"
+    ];
+
+    ":mrf_simple" = {
+      # Tag all media as sensitive
+      media_nsfw = mkMap {
+        "nsfw.weird.kinky" = "Untagged NSFW content";
+      };
+
+      # Reject all activities except deletes
+      reject = mkMap {
+        "kiwifarms.cc" = "Persistent harassment of users, no moderation";
+      };
+
+      # Force posts to be visible by followers only
+      followers_only = mkMap {
+        "beta.birdsite.live" = "Avoid polluting timelines with Twitter posts";
+      };
     };
   };
-};
+}
 ```
 
 ## Upload filters {#modules-services-akkoma-upload-filters}
@@ -228,12 +242,14 @@ This example strips GPS and location metadata from uploads, deduplicates them an
 the file name.
 
 ```nix
-services.akkoma.config.":pleroma"."Pleroma.Upload".filters =
-  map (pkgs.formats.elixirConf { }).lib.mkRaw [
-    "Pleroma.Upload.Filter.Exiftool"
-    "Pleroma.Upload.Filter.Dedupe"
-    "Pleroma.Upload.Filter.AnonymizeFilename"
-  ];
+{
+  services.akkoma.config.":pleroma"."Pleroma.Upload".filters =
+    map (pkgs.formats.elixirConf { }).lib.mkRaw [
+      "Pleroma.Upload.Filter.Exiftool"
+      "Pleroma.Upload.Filter.Dedupe"
+      "Pleroma.Upload.Filter.AnonymizeFilename"
+    ];
+}
 ```
 
 ## Migration from Pleroma {#modules-services-akkoma-migration-pleroma}
@@ -286,9 +302,11 @@ To re‐use the Pleroma data in place, disable Pleroma and enable Akkoma, pointi
 Pleroma database and upload directory.
 
 ```nix
-# Adjust these settings according to the database name and upload directory path used by Pleroma
-services.akkoma.config.":pleroma"."Pleroma.Repo".database = "pleroma";
-services.akkoma.config.":pleroma".":instance".upload_dir = "/var/lib/pleroma/uploads";
+{
+  # Adjust these settings according to the database name and upload directory path used by Pleroma
+  services.akkoma.config.":pleroma"."Pleroma.Repo".database = "pleroma";
+  services.akkoma.config.":pleroma".":instance".upload_dir = "/var/lib/pleroma/uploads";
+}
 ```
 
 Please keep in mind that after the Akkoma service has been started, any migrations applied by
@@ -304,7 +322,9 @@ details.
 The Akkoma systemd service may be confined to a chroot with
 
 ```nix
-services.systemd.akkoma.confinement.enable = true;
+{
+  services.systemd.akkoma.confinement.enable = true;
+}
 ```
 
 Confinement of services is not generally supported in NixOS and therefore disabled by default.
diff --git a/nixpkgs/nixos/modules/services/web-apps/c2fmzq-server.md b/nixpkgs/nixos/modules/services/web-apps/c2fmzq-server.md
index 236953bd4ff7..d8e59b3ad210 100644
--- a/nixpkgs/nixos/modules/services/web-apps/c2fmzq-server.md
+++ b/nixpkgs/nixos/modules/services/web-apps/c2fmzq-server.md
@@ -4,7 +4,7 @@ c2FmZQ is an application that can securely encrypt, store, and share files,
 including but not limited to pictures and videos.
 
 The service `c2fmzq-server` can be enabled by setting
-```
+```nix
 {
   services.c2fmzq-server.enable = true;
 }
@@ -17,7 +17,7 @@ In principle the server can be exposed directly on a public interface and there
 are command line options to manage HTTPS certificates directly, but the module
 is designed to be served behind a reverse proxy or only accessed via localhost.
 
-```
+```nix
 {
   services.c2fmzq-server = {
     enable = true;
diff --git a/nixpkgs/nixos/modules/services/web-apps/discourse.md b/nixpkgs/nixos/modules/services/web-apps/discourse.md
index 35180bea87d9..d4b9c93c4ead 100644
--- a/nixpkgs/nixos/modules/services/web-apps/discourse.md
+++ b/nixpkgs/nixos/modules/services/web-apps/discourse.md
@@ -6,20 +6,22 @@ modern and open source discussion platform.
 ## Basic usage {#module-services-discourse-basic-usage}
 
 A minimal configuration using Let's Encrypt for TLS certificates looks like this:
-```
-services.discourse = {
-  enable = true;
-  hostname = "discourse.example.com";
-  admin = {
-    email = "admin@example.com";
-    username = "admin";
-    fullName = "Administrator";
-    passwordFile = "/path/to/password_file";
+```nix
+{
+  services.discourse = {
+    enable = true;
+    hostname = "discourse.example.com";
+    admin = {
+      email = "admin@example.com";
+      username = "admin";
+      fullName = "Administrator";
+      passwordFile = "/path/to/password_file";
+    };
+    secretKeyBaseFile = "/path/to/secret_key_base_file";
   };
-  secretKeyBaseFile = "/path/to/secret_key_base_file";
-};
-security.acme.email = "me@example.com";
-security.acme.acceptTerms = true;
+  security.acme.email = "me@example.com";
+  security.acme.acceptTerms = true;
+}
 ```
 
 Provided a proper DNS setup, you'll be able to connect to the
@@ -34,20 +36,22 @@ the [](#opt-services.discourse.sslCertificate)
 and [](#opt-services.discourse.sslCertificateKey)
 options:
 
-```
-services.discourse = {
-  enable = true;
-  hostname = "discourse.example.com";
-  sslCertificate = "/path/to/ssl_certificate";
-  sslCertificateKey = "/path/to/ssl_certificate_key";
-  admin = {
-    email = "admin@example.com";
-    username = "admin";
-    fullName = "Administrator";
-    passwordFile = "/path/to/password_file";
+```nix
+{
+  services.discourse = {
+    enable = true;
+    hostname = "discourse.example.com";
+    sslCertificate = "/path/to/ssl_certificate";
+    sslCertificateKey = "/path/to/ssl_certificate_key";
+    admin = {
+      email = "admin@example.com";
+      username = "admin";
+      fullName = "Administrator";
+      passwordFile = "/path/to/password_file";
+    };
+    secretKeyBaseFile = "/path/to/secret_key_base_file";
   };
-  secretKeyBaseFile = "/path/to/secret_key_base_file";
-};
+}
 ```
 
 ## Database access {#module-services-discourse-database}
@@ -80,27 +84,29 @@ A basic setup which assumes you want to use your configured
 [hostname](#opt-services.discourse.hostname) as
 email domain can be done like this:
 
-```
-services.discourse = {
-  enable = true;
-  hostname = "discourse.example.com";
-  sslCertificate = "/path/to/ssl_certificate";
-  sslCertificateKey = "/path/to/ssl_certificate_key";
-  admin = {
-    email = "admin@example.com";
-    username = "admin";
-    fullName = "Administrator";
-    passwordFile = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    serverAddress = "smtp.emailprovider.com";
-    port = 587;
-    username = "user@emailprovider.com";
-    passwordFile = "/path/to/smtp_password_file";
+```nix
+{
+  services.discourse = {
+    enable = true;
+    hostname = "discourse.example.com";
+    sslCertificate = "/path/to/ssl_certificate";
+    sslCertificateKey = "/path/to/ssl_certificate_key";
+    admin = {
+      email = "admin@example.com";
+      username = "admin";
+      fullName = "Administrator";
+      passwordFile = "/path/to/password_file";
+    };
+    mail.outgoing = {
+      serverAddress = "smtp.emailprovider.com";
+      port = 587;
+      username = "user@emailprovider.com";
+      passwordFile = "/path/to/smtp_password_file";
+    };
+    mail.incoming.enable = true;
+    secretKeyBaseFile = "/path/to/secret_key_base_file";
   };
-  mail.incoming.enable = true;
-  secretKeyBaseFile = "/path/to/secret_key_base_file";
-};
+}
 ```
 
 This assumes you have set up an MX record for the address you've
@@ -162,44 +168,46 @@ The following example sets the title and description of the
 Discourse instance and enables
 GitHub login in the site settings,
 and changes a few request limits in the backend settings:
-```
-services.discourse = {
-  enable = true;
-  hostname = "discourse.example.com";
-  sslCertificate = "/path/to/ssl_certificate";
-  sslCertificateKey = "/path/to/ssl_certificate_key";
-  admin = {
-    email = "admin@example.com";
-    username = "admin";
-    fullName = "Administrator";
-    passwordFile = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    serverAddress = "smtp.emailprovider.com";
-    port = 587;
-    username = "user@emailprovider.com";
-    passwordFile = "/path/to/smtp_password_file";
-  };
-  mail.incoming.enable = true;
-  siteSettings = {
-    required = {
-      title = "My Cats";
-      site_description = "Discuss My Cats (and be nice plz)";
+```nix
+{
+  services.discourse = {
+    enable = true;
+    hostname = "discourse.example.com";
+    sslCertificate = "/path/to/ssl_certificate";
+    sslCertificateKey = "/path/to/ssl_certificate_key";
+    admin = {
+      email = "admin@example.com";
+      username = "admin";
+      fullName = "Administrator";
+      passwordFile = "/path/to/password_file";
     };
-    login = {
-      enable_github_logins = true;
-      github_client_id = "a2f6dfe838cb3206ce20";
-      github_client_secret._secret = /run/keys/discourse_github_client_secret;
+    mail.outgoing = {
+      serverAddress = "smtp.emailprovider.com";
+      port = 587;
+      username = "user@emailprovider.com";
+      passwordFile = "/path/to/smtp_password_file";
     };
+    mail.incoming.enable = true;
+    siteSettings = {
+      required = {
+        title = "My Cats";
+        site_description = "Discuss My Cats (and be nice plz)";
+      };
+      login = {
+        enable_github_logins = true;
+        github_client_id = "a2f6dfe838cb3206ce20";
+        github_client_secret._secret = /run/keys/discourse_github_client_secret;
+      };
+    };
+    backendSettings = {
+      max_reqs_per_ip_per_minute = 300;
+      max_reqs_per_ip_per_10_seconds = 60;
+      max_asset_reqs_per_ip_per_10_seconds = 250;
+      max_reqs_per_ip_mode = "warn+block";
+    };
+    secretKeyBaseFile = "/path/to/secret_key_base_file";
   };
-  backendSettings = {
-    max_reqs_per_ip_per_minute = 300;
-    max_reqs_per_ip_per_10_seconds = 60;
-    max_asset_reqs_per_ip_per_10_seconds = 250;
-    max_reqs_per_ip_mode = "warn+block";
-  };
-  secretKeyBaseFile = "/path/to/secret_key_base_file";
-};
+}
 ```
 
 In the resulting site settings file, the
@@ -253,34 +261,36 @@ and [discourse-solved](https://github.com/discourse/discourse-solved)
 plugins, and disable `discourse-spoiler-alert`
 by default:
 
-```
-services.discourse = {
-  enable = true;
-  hostname = "discourse.example.com";
-  sslCertificate = "/path/to/ssl_certificate";
-  sslCertificateKey = "/path/to/ssl_certificate_key";
-  admin = {
-    email = "admin@example.com";
-    username = "admin";
-    fullName = "Administrator";
-    passwordFile = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    serverAddress = "smtp.emailprovider.com";
-    port = 587;
-    username = "user@emailprovider.com";
-    passwordFile = "/path/to/smtp_password_file";
-  };
-  mail.incoming.enable = true;
-  plugins = with config.services.discourse.package.plugins; [
-    discourse-spoiler-alert
-    discourse-solved
-  ];
-  siteSettings = {
-    plugins = {
-      spoiler_enabled = false;
+```nix
+{
+  services.discourse = {
+    enable = true;
+    hostname = "discourse.example.com";
+    sslCertificate = "/path/to/ssl_certificate";
+    sslCertificateKey = "/path/to/ssl_certificate_key";
+    admin = {
+      email = "admin@example.com";
+      username = "admin";
+      fullName = "Administrator";
+      passwordFile = "/path/to/password_file";
+    };
+    mail.outgoing = {
+      serverAddress = "smtp.emailprovider.com";
+      port = 587;
+      username = "user@emailprovider.com";
+      passwordFile = "/path/to/smtp_password_file";
+    };
+    mail.incoming.enable = true;
+    plugins = with config.services.discourse.package.plugins; [
+      discourse-spoiler-alert
+      discourse-solved
+    ];
+    siteSettings = {
+      plugins = {
+        spoiler_enabled = false;
+      };
     };
+    secretKeyBaseFile = "/path/to/secret_key_base_file";
   };
-  secretKeyBaseFile = "/path/to/secret_key_base_file";
-};
+}
 ```
diff --git a/nixpkgs/nixos/modules/services/web-apps/gotosocial.md b/nixpkgs/nixos/modules/services/web-apps/gotosocial.md
index a290d7d1893a..b3540f0d5811 100644
--- a/nixpkgs/nixos/modules/services/web-apps/gotosocial.md
+++ b/nixpkgs/nixos/modules/services/web-apps/gotosocial.md
@@ -8,17 +8,19 @@ The following configuration sets up the PostgreSQL as database backend and binds
 GoToSocial to `127.0.0.1:8080`, expecting to be run behind a HTTP proxy on `gotosocial.example.com`.
 
 ```nix
-services.gotosocial = {
-  enable = true;
-  setupPostgresqlDB = true;
-  settings = {
-    application-name = "My GoToSocial";
-    host = "gotosocial.example.com";
-    protocol = "https";
-    bind-address = "127.0.0.1";
-    port = 8080;
+{
+  services.gotosocial = {
+    enable = true;
+    setupPostgresqlDB = true;
+    settings = {
+      application-name = "My GoToSocial";
+      host = "gotosocial.example.com";
+      protocol = "https";
+      bind-address = "127.0.0.1";
+      port = 8080;
+    };
   };
-};
+}
 ```
 
 Please refer to the [GoToSocial Documentation](https://docs.gotosocial.org/en/latest/configuration/general/)
@@ -30,24 +32,26 @@ Although it is possible to expose GoToSocial directly, it is common practice to
 HTTP reverse proxy such as nginx.
 
 ```nix
-networking.firewall.allowedTCPPorts = [ 80 443 ];
-services.nginx = {
-  enable = true;
-  clientMaxBodySize = "40M";
-  virtualHosts = with config.services.gotosocial.settings; {
-    "${host}" = {
-      enableACME = true;
-      forceSSL = true;
-      locations = {
-        "/" = {
-          recommendedProxySettings = true;
-          proxyWebsockets = true;
-          proxyPass = "http://${bind-address}:${toString port}";
+{
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  services.nginx = {
+    enable = true;
+    clientMaxBodySize = "40M";
+    virtualHosts = with config.services.gotosocial.settings; {
+      "${host}" = {
+        enableACME = true;
+        forceSSL = true;
+        locations = {
+          "/" = {
+            recommendedProxySettings = true;
+            proxyWebsockets = true;
+            proxyPass = "http://${bind-address}:${toString port}";
+          };
         };
       };
     };
   };
-};
+}
 ```
 
 Please refer to [](#module-security-acme) for details on how to provision an SSL/TLS certificate.
diff --git a/nixpkgs/nixos/modules/services/web-apps/grocy.md b/nixpkgs/nixos/modules/services/web-apps/grocy.md
index 62aad4b103df..f4b5769c2479 100644
--- a/nixpkgs/nixos/modules/services/web-apps/grocy.md
+++ b/nixpkgs/nixos/modules/services/web-apps/grocy.md
@@ -6,7 +6,7 @@
 ## Basic usage {#module-services-grocy-basic-usage}
 
 A very basic configuration may look like this:
-```
+```nix
 { pkgs, ... }:
 {
   services.grocy = {
@@ -29,7 +29,7 @@ of the application.
 
 The configuration for `grocy` is located at `/etc/grocy/config.php`.
 By default, the following settings can be defined in the NixOS-configuration:
-```
+```nix
 { pkgs, ... }:
 {
   services.grocy.settings = {
@@ -56,7 +56,7 @@ By default, the following settings can be defined in the NixOS-configuration:
 
 If you want to alter the configuration file on your own, you can do this manually with
 an expression like this:
-```
+```nix
 { lib, ... }:
 {
   environment.etc."grocy/config.php".text = lib.mkAfter ''
diff --git a/nixpkgs/nixos/modules/services/web-apps/jitsi-meet.md b/nixpkgs/nixos/modules/services/web-apps/jitsi-meet.md
index 060ef9752650..577f82e315be 100644
--- a/nixpkgs/nixos/modules/services/web-apps/jitsi-meet.md
+++ b/nixpkgs/nixos/modules/services/web-apps/jitsi-meet.md
@@ -6,7 +6,7 @@ private, self-hosted video conferencing solution.
 ## Basic usage {#module-services-jitsi-basic-usage}
 
 A minimal configuration using Let's Encrypt for TLS certificates looks like this:
-```
+```nix
 {
   services.jitsi-meet = {
     enable = true;
@@ -22,7 +22,7 @@ A minimal configuration using Let's Encrypt for TLS certificates looks like this
 ## Configuration {#module-services-jitsi-configuration}
 
 Here is the minimal configuration with additional configurations:
-```
+```nix
 {
   services.jitsi-meet = {
     enable = true;
diff --git a/nixpkgs/nixos/modules/services/web-apps/kavita.nix b/nixpkgs/nixos/modules/services/web-apps/kavita.nix
index c3e39f0b5476..c90697bcfa8b 100644
--- a/nixpkgs/nixos/modules/services/web-apps/kavita.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/kavita.nix
@@ -2,7 +2,18 @@
 
 let
   cfg = config.services.kavita;
-in {
+  settingsFormat = pkgs.formats.json { };
+  appsettings = settingsFormat.generate "appsettings.json" ({ TokenKey = "@TOKEN@"; } // cfg.settings);
+in
+{
+  imports = [
+    (lib.mkChangedOptionModule [ "services" "kavita" "ipAdresses" ] [ "services" "kavita" "settings" "IpAddresses" ] (config:
+      let value = lib.getAttrFromPath [ "services" "kavita" "ipAdresses" ] config; in
+      lib.concatStringsSep "," value
+    ))
+    (lib.mkRenamedOptionModule [ "services" "kavita" "port" ] [ "services" "kavita" "settings" "Port" ])
+  ];
+
   options.services.kavita = {
     enable = lib.mkEnableOption (lib.mdDoc "Kavita reading server");
 
@@ -27,16 +38,31 @@ in {
         It can be generated with `head -c 32 /dev/urandom | base64`.
       '';
     };
-    port = lib.mkOption {
-      default = 5000;
-      type = lib.types.port;
-      description = lib.mdDoc "Port to bind to.";
-    };
-    ipAdresses = lib.mkOption {
-      default = ["0.0.0.0" "::"];
-      type = lib.types.listOf lib.types.str;
-      description = lib.mdDoc "IP Addresses to bind to. The default is to bind
-      to all IPv4 and IPv6 addresses.";
+
+    settings = lib.mkOption {
+      default = { };
+      description = lib.mdDoc ''
+        Kavita configuration options, as configured in {file}`appsettings.json`.
+      '';
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+
+        options = {
+          Port = lib.mkOption {
+            default = 5000;
+            type = lib.types.port;
+            description = lib.mdDoc "Port to bind to.";
+          };
+
+          IpAddresses = lib.mkOption {
+            default = "0.0.0.0,::";
+            type = lib.types.commas;
+            description = lib.mdDoc ''
+              IP Addresses to bind to. The default is to bind to all IPv4 and IPv6 addresses.
+            '';
+          };
+        };
+      };
     };
   };
 
@@ -46,18 +72,15 @@ in {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       preStart = ''
-        umask u=rwx,g=rx,o=
-        cat > "${cfg.dataDir}/config/appsettings.json" <<EOF
-        {
-          "TokenKey": "$(cat ${cfg.tokenKeyFile})",
-          "Port": ${toString cfg.port},
-          "IpAddresses": "${lib.concatStringsSep "," cfg.ipAdresses}"
-        }
-        EOF
+        install -m600 ${appsettings} ${lib.escapeShellArg cfg.dataDir}/config/appsettings.json
+        ${pkgs.replace-secret}/bin/replace-secret '@TOKEN@' \
+          ''${CREDENTIALS_DIRECTORY}/token \
+          '${cfg.dataDir}/config/appsettings.json'
       '';
       serviceConfig = {
         WorkingDirectory = cfg.dataDir;
-        ExecStart = "${lib.getExe cfg.package}";
+        LoadCredential = [ "token:${cfg.tokenKeyFile}" ];
+        ExecStart = lib.getExe cfg.package;
         Restart = "always";
         User = cfg.user;
       };
diff --git a/nixpkgs/nixos/modules/services/web-apps/keycloak.md b/nixpkgs/nixos/modules/services/web-apps/keycloak.md
index aa8de40d642b..020bee400348 100644
--- a/nixpkgs/nixos/modules/services/web-apps/keycloak.md
+++ b/nixpkgs/nixos/modules/services/web-apps/keycloak.md
@@ -126,16 +126,18 @@ should be set to. See the description of
 ## Example configuration {#module-services-keycloak-example-config}
 
 A basic configuration with some custom settings could look like this:
-```
-services.keycloak = {
-  enable = true;
-  settings = {
-    hostname = "keycloak.example.com";
-    hostname-strict-backchannel = true;
+```nix
+{
+  services.keycloak = {
+    enable = true;
+    settings = {
+      hostname = "keycloak.example.com";
+      hostname-strict-backchannel = true;
+    };
+    initialAdminPassword = "e6Wcm0RrtegMEHl";  # change on first login
+    sslCertificate = "/run/keys/ssl_cert";
+    sslCertificateKey = "/run/keys/ssl_key";
+    database.passwordFile = "/run/keys/db_password";
   };
-  initialAdminPassword = "e6Wcm0RrtegMEHl";  # change on first login
-  sslCertificate = "/run/keys/ssl_cert";
-  sslCertificateKey = "/run/keys/ssl_key";
-  database.passwordFile = "/run/keys/db_password";
-};
+}
 ```
diff --git a/nixpkgs/nixos/modules/services/web-apps/lemmy.md b/nixpkgs/nixos/modules/services/web-apps/lemmy.md
index faafe096d138..0ed23607d00b 100644
--- a/nixpkgs/nixos/modules/services/web-apps/lemmy.md
+++ b/nixpkgs/nixos/modules/services/web-apps/lemmy.md
@@ -7,13 +7,15 @@ Lemmy is a federated alternative to reddit in rust.
 the minimum to start lemmy is
 
 ```nix
-services.lemmy = {
-  enable = true;
-  settings = {
-    hostname = "lemmy.union.rocks";
-    database.createLocally = true;
+{
+  services.lemmy = {
+    enable = true;
+    settings = {
+      hostname = "lemmy.union.rocks";
+      database.createLocally = true;
+    };
+    caddy.enable = true;
   };
-  caddy.enable = true;
 }
 ```
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/nextcloud.md b/nixpkgs/nixos/modules/services/web-apps/nextcloud.md
index 5db83d7e4463..06a8712b0b8a 100644
--- a/nixpkgs/nixos/modules/services/web-apps/nextcloud.md
+++ b/nixpkgs/nixos/modules/services/web-apps/nextcloud.md
@@ -25,7 +25,7 @@ to `true`, Nextcloud will automatically be configured to connect to it through
 socket.
 
 A very basic configuration may look like this:
-```
+```nix
 { pkgs, ... }:
 {
   services.nextcloud = {
@@ -130,7 +130,7 @@ settings `listen.owner` &amp; `listen.group` in the
 [corresponding `phpfpm` pool](#opt-services.phpfpm.pools).
 
 An exemplary configuration may look like this:
-```
+```nix
 { config, lib, pkgs, ... }: {
   services.nginx.enable = false;
   services.nextcloud = {
@@ -205,7 +205,7 @@ If major-releases will be abandoned by upstream, we should check first if those
 in NixOS for a safe upgrade-path before removing those. In that case we should keep those
 packages, but mark them as insecure in an expression like this (in
 `<nixpkgs/pkgs/servers/nextcloud/default.nix>`):
-```
+```nix
 /* ... */
 {
   nextcloud17 = generic {
diff --git a/nixpkgs/nixos/modules/services/web-apps/peertube.nix b/nixpkgs/nixos/modules/services/web-apps/peertube.nix
index 39c02c81c423..76f869913592 100644
--- a/nixpkgs/nixos/modules/services/web-apps/peertube.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/peertube.nix
@@ -61,18 +61,16 @@ let
     eval -- "\$@"
   '';
 
-  peertubeCli = pkgs.writeShellScriptBin "peertube" ''
-    node ~/dist/server/tools/peertube.js $@
+  nginxCommonHeaders = lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.forceSSL ''
+    add_header Strict-Transport-Security 'max-age=31536000';
+  '' + lib.optionalString (config.services.nginx.virtualHosts.${cfg.localDomain}.quic && config.services.nginx.virtualHosts.${cfg.localDomain}.http3) ''
+    add_header Alt-Svc 'h3=":$server_port"; ma=604800';
   '';
 
-  nginxCommonHeaders = lib.optionalString cfg.enableWebHttps ''
-    add_header Strict-Transport-Security      'max-age=63072000; includeSubDomains';
-  '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
-    add_header Alt-Svc                        'h3=":443"; ma=86400';
-  '' + ''
-    add_header Access-Control-Allow-Origin    '*';
-    add_header Access-Control-Allow-Methods   'GET, OPTIONS';
-    add_header Access-Control-Allow-Headers   'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+  nginxCommonHeadersExtra = ''
+    add_header Access-Control-Allow-Origin '*';
+    add_header Access-Control-Allow-Methods 'GET, OPTIONS';
+    add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
   '';
 
 in {
@@ -330,6 +328,8 @@ in {
       }
     ];
 
+    environment.systemPackages = [ cfg.package.cli ];
+
     services.peertube.settings = lib.mkMerge [
       {
         listen = {
@@ -355,12 +355,13 @@ in {
           tmp_persistent = lib.mkDefault "/var/lib/peertube/storage/tmp_persistent/";
           bin = lib.mkDefault "/var/lib/peertube/storage/bin/";
           avatars = lib.mkDefault "/var/lib/peertube/storage/avatars/";
-          videos = lib.mkDefault "/var/lib/peertube/storage/videos/";
+          web_videos = lib.mkDefault "/var/lib/peertube/storage/web-videos/";
           streaming_playlists = lib.mkDefault "/var/lib/peertube/storage/streaming-playlists/";
           redundancy = lib.mkDefault "/var/lib/peertube/storage/redundancy/";
           logs = lib.mkDefault "/var/lib/peertube/storage/logs/";
           previews = lib.mkDefault "/var/lib/peertube/storage/previews/";
           thumbnails = lib.mkDefault "/var/lib/peertube/storage/thumbnails/";
+          storyboards = lib.mkDefault "/var/lib/peertube/storage/storyboards/";
           torrents = lib.mkDefault "/var/lib/peertube/storage/torrents/";
           captions = lib.mkDefault "/var/lib/peertube/storage/captions/";
           cache = lib.mkDefault "/var/lib/peertube/storage/cache/";
@@ -428,7 +429,7 @@ in {
 
       environment = env;
 
-      path = with pkgs; [ bashInteractive ffmpeg nodejs_18 openssl yarn python3 ];
+      path = with pkgs; [ nodejs_18 yarn ffmpeg-headless openssl ];
 
       script = ''
         #!/bin/sh
@@ -456,7 +457,7 @@ in {
         ln -sf ${cfg.package}/config/default.yaml /var/lib/peertube/config/default.yaml
         ln -sf ${cfg.package}/client/dist -T /var/lib/peertube/www/client
         ln -sf ${cfg.settings.storage.client_overrides} -T /var/lib/peertube/www/client-overrides
-        npm start
+        node dist/server
       '';
       serviceConfig = {
         Type = "simple";
@@ -488,6 +489,9 @@ in {
 
     services.nginx = lib.mkIf cfg.configureNginx {
       enable = true;
+      upstreams."peertube".servers = {
+        "127.0.0.1:${toString cfg.listenHttp}".fail_timeout = "0";
+      };
       virtualHosts."${cfg.localDomain}" = {
         root = "/var/lib/peertube/www";
 
@@ -497,14 +501,14 @@ in {
           priority = 1110;
         };
 
-        locations."= /api/v1/videos/upload-resumable" = {
+        locations."~ ^/api/v1/videos/(upload-resumable|([^/]+/source/replace-resumable))$" = {
           tryFiles = "/dev/null @api";
           priority = 1120;
 
           extraConfig = ''
-            client_max_body_size                        0;
-            proxy_request_buffering                     off;
-          '';
+            client_max_body_size 0;
+            proxy_request_buffering off;
+          '' + nginxCommonHeaders;
         };
 
         locations."~ ^/api/v1/videos/(upload|([^/]+/studio/edit))$" = {
@@ -513,13 +517,11 @@ in {
           priority = 1130;
 
           extraConfig = ''
-            client_max_body_size                        12G;
-            add_header X-File-Maximum-Size              8G always;
-          '' + lib.optionalString cfg.enableWebHttps ''
-            add_header Strict-Transport-Security        'max-age=63072000; includeSubDomains';
-          '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
-            add_header Alt-Svc                          'h3=":443"; ma=86400';
-          '';
+            limit_except POST HEAD { deny all; }
+
+            client_max_body_size 12G;
+            add_header X-File-Maximum-Size 8G always;
+          '' + nginxCommonHeaders;
         };
 
         locations."~ ^/api/v1/runners/jobs/[^/]+/(update|success)$" = {
@@ -528,13 +530,9 @@ in {
           priority = 1135;
 
           extraConfig = ''
-            client_max_body_size                        12G;
-            add_header X-File-Maximum-Size              8G always;
-          '' + lib.optionalString cfg.enableWebHttps ''
-            add_header Strict-Transport-Security        'max-age=63072000; includeSubDomains';
-          '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
-            add_header Alt-Svc                          'h3=":443"; ma=86400';
-          '';
+            client_max_body_size 12G;
+            add_header X-File-Maximum-Size 8G always;
+          '' + nginxCommonHeaders;
         };
 
         locations."~ ^/api/v1/(videos|video-playlists|video-channels|users/me)" = {
@@ -542,32 +540,28 @@ in {
           priority = 1140;
 
           extraConfig = ''
-            client_max_body_size                        6M;
-            add_header X-File-Maximum-Size              4M always;
-          '' + lib.optionalString cfg.enableWebHttps ''
-            add_header Strict-Transport-Security        'max-age=63072000; includeSubDomains';
-          '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
-            add_header Alt-Svc                          'h3=":443"; ma=86400';
-          '';
+            client_max_body_size 6M;
+            add_header X-File-Maximum-Size 4M always;
+          '' + nginxCommonHeaders;
         };
 
         locations."@api" = {
-          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          proxyPass = "http://peertube";
           priority = 1150;
 
           extraConfig = ''
-            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
-            proxy_set_header Host                       $host;
-            proxy_set_header X-Real-IP                  $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
-            proxy_connect_timeout                       10m;
+            proxy_connect_timeout 10m;
 
-            proxy_send_timeout                          10m;
-            proxy_read_timeout                          10m;
+            proxy_send_timeout 10m;
+            proxy_read_timeout 10m;
 
-            client_max_body_size                        100k;
-            send_timeout                                10m;
-          '';
+            client_max_body_size 100k;
+            send_timeout 10m;
+          ''+ nginxCommonHeaders;
         };
 
         # Websocket
@@ -581,7 +575,7 @@ in {
           priority = 1220;
 
           extraConfig = ''
-            proxy_read_timeout                          15m;
+            proxy_read_timeout 15m;
           '';
         };
 
@@ -591,84 +585,82 @@ in {
         };
 
         locations."@api_websocket" = {
-          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          proxyPass = "http://peertube";
           priority = 1240;
 
           extraConfig = ''
-            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
-            proxy_set_header Host                       $host;
-            proxy_set_header X-Real-IP                  $remote_addr;
-            proxy_set_header Upgrade                    $http_upgrade;
-            proxy_set_header Connection                 'upgrade';
-
-            proxy_http_version                          1.1;
-          '';
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection 'upgrade';
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+          '' + nginxCommonHeaders;
         };
 
         # Bypass PeerTube for performance reasons.
         locations."~ ^/client/(assets/images/(icons/icon-36x36\.png|icons/icon-48x48\.png|icons/icon-72x72\.png|icons/icon-96x96\.png|icons/icon-144x144\.png|icons/icon-192x192\.png|icons/icon-512x512\.png|logo\.svg|favicon\.png|default-playlist\.jpg|default-avatar-account\.png|default-avatar-account-48x48\.png|default-avatar-video-channel\.png|default-avatar-video-channel-48x48\.png))$" = {
           tryFiles = "/client-overrides/$1 /client/$1 $1";
           priority = 1310;
+
+          extraConfig = nginxCommonHeaders;
         };
 
         locations."~ ^/client/(.*\.(js|css|png|svg|woff2|otf|ttf|woff|eot))$" = {
           alias = "${cfg.package}/client/dist/$1";
           priority = 1320;
           extraConfig = ''
-            add_header Cache-Control                    'public, max-age=604800, immutable';
-          '' + lib.optionalString cfg.enableWebHttps ''
-            add_header Strict-Transport-Security        'max-age=63072000; includeSubDomains';
-          '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
-            add_header Alt-Svc                          'h3=":443"; ma=86400';
-          '';
+            add_header Cache-Control 'public, max-age=604800, immutable';
+          '' + nginxCommonHeaders;
         };
 
         locations."^~ /download/" = {
-          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          proxyPass = "http://peertube";
           priority = 1410;
           extraConfig = ''
-            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
-            proxy_set_header Host                       $host;
-            proxy_set_header X-Real-IP                  $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
-            proxy_limit_rate                            5M;
-          '';
+            proxy_limit_rate 5M;
+          '' + nginxCommonHeaders;
         };
 
-        locations."^~ /static/streaming-playlists/private/" = {
-          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+        locations."^~ /static/streaming-playlists/hls/private/" = {
+          proxyPass = "http://peertube";
           priority = 1420;
           extraConfig = ''
-            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
-            proxy_set_header Host                       $host;
-            proxy_set_header X-Real-IP                  $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
-            proxy_limit_rate                            5M;
-          '';
+            proxy_limit_rate 5M;
+          '' + nginxCommonHeaders;
         };
 
         locations."^~ /static/web-videos/private/" = {
-          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          proxyPass = "http://peertube";
           priority = 1430;
           extraConfig = ''
-            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
-            proxy_set_header Host                       $host;
-            proxy_set_header X-Real-IP                  $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
-            proxy_limit_rate                            5M;
-          '';
+            proxy_limit_rate 5M;
+          '' + nginxCommonHeaders;
         };
 
         locations."^~ /static/webseed/private/" = {
-          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          proxyPass = "http://peertube";
           priority = 1440;
           extraConfig = ''
-            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
-            proxy_set_header Host                       $host;
-            proxy_set_header X-Real-IP                  $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
-            proxy_limit_rate                            5M;
-          '';
+            proxy_limit_rate 5M;
+          '' + nginxCommonHeaders;
         };
 
         locations."^~ /static/redundancy/" = {
@@ -676,33 +668,35 @@ in {
           root = cfg.settings.storage.redundancy;
           priority = 1450;
           extraConfig = ''
-            set $peertube_limit_rate                    800k;
+            set $peertube_limit_rate 800k;
 
             if ($request_uri ~ -fragmented.mp4$) {
-              set $peertube_limit_rate                  5M;
+              set $peertube_limit_rate 5M;
             }
 
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
+              ${nginxCommonHeadersExtra}
+              add_header Access-Control-Max-Age 1728000;
+              add_header Content-Type 'text/plain charset=UTF-8';
+              add_header Content-Length 0;
+              return 204;
             }
             if ($request_method = 'GET') {
               ${nginxCommonHeaders}
+              ${nginxCommonHeadersExtra}
 
-              access_log                                off;
+              access_log off;
             }
 
-            aio                                         threads;
-            sendfile                                    on;
-            sendfile_max_chunk                          1M;
+            aio threads;
+            sendfile on;
+            sendfile_max_chunk 1M;
 
-            limit_rate                                  $peertube_limit_rate;
-            limit_rate_after                            5M;
+            limit_rate $peertube_limit_rate;
+            limit_rate_after 5M;
 
-            rewrite ^/static/redundancy/(.*)$           /$1 break;
+            rewrite ^/static/redundancy/(.*)$ /$1 break;
           '';
         };
 
@@ -711,109 +705,111 @@ in {
           root = cfg.settings.storage.streaming_playlists;
           priority = 1460;
           extraConfig = ''
-            set $peertube_limit_rate                    800k;
+            set $peertube_limit_rate 800k;
 
             if ($request_uri ~ -fragmented.mp4$) {
-              set $peertube_limit_rate                  5M;
+              set $peertube_limit_rate 5M;
             }
 
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
+              ${nginxCommonHeadersExtra}
+              add_header Access-Control-Max-Age 1728000;
+              add_header Content-Type 'text/plain charset=UTF-8';
+              add_header Content-Length 0;
+              return 204;
             }
             if ($request_method = 'GET') {
               ${nginxCommonHeaders}
+              ${nginxCommonHeadersExtra}
 
-              access_log                                off;
+              access_log off;
             }
 
-            aio                                         threads;
-            sendfile                                    on;
-            sendfile_max_chunk                          1M;
+            aio threads;
+            sendfile on;
+            sendfile_max_chunk 1M;
 
-            limit_rate                                  $peertube_limit_rate;
-            limit_rate_after                            5M;
+            limit_rate $peertube_limit_rate;
+            limit_rate_after 5M;
 
-            rewrite ^/static/streaming-playlists/(.*)$  /$1 break;
+            rewrite ^/static/streaming-playlists/(.*)$ /$1 break;
           '';
         };
 
         locations."^~ /static/web-videos/" = {
           tryFiles = "$uri @api";
-          root = cfg.settings.storage.streaming_playlists;
+          root = cfg.settings.storage.web_videos;
           priority = 1470;
           extraConfig = ''
-            set $peertube_limit_rate                    800k;
+            set $peertube_limit_rate 800k;
 
             if ($request_uri ~ -fragmented.mp4$) {
-              set $peertube_limit_rate                  5M;
+              set $peertube_limit_rate 5M;
             }
 
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
+              ${nginxCommonHeadersExtra}
+              add_header Access-Control-Max-Age 1728000;
+              add_header Content-Type 'text/plain charset=UTF-8';
+              add_header Content-Length 0;
+              return 204;
             }
             if ($request_method = 'GET') {
               ${nginxCommonHeaders}
+              ${nginxCommonHeadersExtra}
 
-              access_log                                off;
+              access_log off;
             }
 
-            aio                                         threads;
-            sendfile                                    on;
-            sendfile_max_chunk                          1M;
+            aio threads;
+            sendfile on;
+            sendfile_max_chunk 1M;
 
-            limit_rate                                  $peertube_limit_rate;
-            limit_rate_after                            5M;
+            limit_rate $peertube_limit_rate;
+            limit_rate_after 5M;
 
-            rewrite ^/static/streaming-playlists/(.*)$  /$1 break;
+            rewrite ^/static/web-videos/(.*)$ /$1 break;
           '';
         };
 
         locations."^~ /static/webseed/" = {
           tryFiles = "$uri @api";
-          root = cfg.settings.storage.videos;
+          root = cfg.settings.storage.web_videos;
           priority = 1480;
           extraConfig = ''
-            set $peertube_limit_rate                    800k;
+            set $peertube_limit_rate 800k;
 
             if ($request_uri ~ -fragmented.mp4$) {
-              set $peertube_limit_rate                  5M;
+              set $peertube_limit_rate 5M;
             }
 
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
+              ${nginxCommonHeadersExtra}
+              add_header Access-Control-Max-Age 1728000;
+              add_header Content-Type 'text/plain charset=UTF-8';
+              add_header Content-Length 0;
+              return 204;
             }
             if ($request_method = 'GET') {
               ${nginxCommonHeaders}
+              ${nginxCommonHeadersExtra}
 
-              access_log                                off;
+              access_log off;
             }
 
-            aio                                         threads;
-            sendfile                                    on;
-            sendfile_max_chunk                          1M;
+            aio threads;
+            sendfile on;
+            sendfile_max_chunk 1M;
 
-            limit_rate                                  $peertube_limit_rate;
-            limit_rate_after                            5M;
+            limit_rate $peertube_limit_rate;
+            limit_rate_after 5M;
 
-            rewrite ^/static/webseed/(.*)$              /$1 break;
+            rewrite ^/static/webseed/(.*)$ /web-videos/$1 break;
           '';
         };
-
-        extraConfig = lib.optionalString cfg.enableWebHttps ''
-          add_header Strict-Transport-Security          'max-age=63072000; includeSubDomains';
-        '';
       };
     };
 
@@ -848,7 +844,7 @@ in {
           home = cfg.package;
         };
       })
-      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs_18 pkgs.yarn ])
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ peertubeEnv pkgs.nodejs_18 pkgs.yarn pkgs.ffmpeg-headless ])
       (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis-peertube" ];})
     ];
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/pict-rs.md b/nixpkgs/nixos/modules/services/web-apps/pict-rs.md
index 2fa6bb3aebce..56c51e0d7259 100644
--- a/nixpkgs/nixos/modules/services/web-apps/pict-rs.md
+++ b/nixpkgs/nixos/modules/services/web-apps/pict-rs.md
@@ -7,7 +7,9 @@ pict-rs is a  a simple image hosting service.
 the minimum to start pict-rs is
 
 ```nix
-services.pict-rs.enable = true;
+{
+  services.pict-rs.enable = true;
+}
 ```
 
 this will start the http server on port 8080 by default.
diff --git a/nixpkgs/nixos/modules/services/web-apps/plausible.md b/nixpkgs/nixos/modules/services/web-apps/plausible.md
index 1328ce69441a..d3673eabddd4 100644
--- a/nixpkgs/nixos/modules/services/web-apps/plausible.md
+++ b/nixpkgs/nixos/modules/services/web-apps/plausible.md
@@ -11,7 +11,7 @@ $ openssl rand -base64 64
 ```
 
 After that, `plausible` can be deployed like this:
-```
+```nix
 {
   services.plausible = {
     enable = true;
diff --git a/nixpkgs/nixos/modules/services/web-apps/pretix.nix b/nixpkgs/nixos/modules/services/web-apps/pretix.nix
index 2355f8c450a1..22ee9769aa92 100644
--- a/nixpkgs/nixos/modules/services/web-apps/pretix.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/pretix.nix
@@ -63,7 +63,7 @@ in
   };
 
   options.services.pretix = {
-    enable = mkEnableOption "pretix";
+    enable = mkEnableOption "Pretix, a ticket shop application for conferences, festivals, concerts, etc.";
 
     package = mkPackageOption pkgs "pretix" { };
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/slskd.nix b/nixpkgs/nixos/modules/services/web-apps/slskd.nix
index 580f66ec3ac9..15a5fd1177ad 100644
--- a/nixpkgs/nixos/modules/services/web-apps/slskd.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/slskd.nix
@@ -2,120 +2,248 @@
 
 let
   settingsFormat = pkgs.formats.yaml {};
+  defaultUser = "slskd";
 in {
   options.services.slskd = with lib; with types; {
     enable = mkEnableOption "enable slskd";
 
-    rotateLogs = mkEnableOption "enable an unit and timer that will rotate logs in /var/slskd/logs";
+    package = mkPackageOptionMD pkgs "slskd" { };
 
-    package = mkPackageOption pkgs "slskd" { };
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "User account under which slskd runs.";
+    };
 
-    nginx = mkOption {
-      description = lib.mdDoc "options for nginx";
-      example = {
-        enable = true;
-        domain = "example.com";
-        contextPath = "/slskd";
-      };
-      type = submodule ({name, config, ...}: {
-        options = {
-          enable = mkEnableOption "enable nginx as a reverse proxy";
+    group = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "Group under which slskd runs.";
+    };
 
-          domainName = mkOption {
-            type = str;
-            description = "Domain you want to use";
-          };
-          contextPath = mkOption {
-            type = types.path;
-            default = "/";
-            description = lib.mdDoc ''
-              The context path, i.e., the last part of the slskd
-              URL. Typically '/' or '/slskd'. Default '/'
-            '';
-          };
-        };
-      });
+    domain = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        If non-null, enables an nginx reverse proxy virtual host at this FQDN,
+        at the path configurated with `services.slskd.web.url_base`.
+      '';
+      example = "slskd.example.com";
+    };
+
+    nginx = mkOption {
+      type = types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
+      default = {};
+      example = lib.literalExpression ''
+        {
+          enableACME = true;
+          forceHttps = true;
+        }
+      '';
+      description = ''
+        This option customizes the nginx virtual host set up for slskd.
+      '';
     };
 
     environmentFile = mkOption {
       type = path;
       description = ''
-        Path to a file containing secrets.
-        It must at least contain the variable `SLSKD_SLSK_PASSWORD`
+        Path to the environment file sourced on startup.
+        It must at least contain the variables `SLSKD_SLSK_USERNAME` and `SLSKD_SLSK_PASSWORD`.
+        Web interface credentials should also be set here in `SLSKD_USERNAME` and `SLSKD_PASSWORD`.
+        Other, optional credentials like SOCKS5 with `SLSKD_SLSK_PROXY_USERNAME` and `SLSKD_SLSK_PROXY_PASSWORD`
+        should all reside here instead of in the world-readable nix store.
+        Variables are documented at https://github.com/slskd/slskd/blob/master/docs/config.md
       '';
     };
 
     openFirewall = mkOption {
       type = bool;
-      description = ''
-        Whether to open the firewall for services.slskd.settings.listen_port";
-      '';
+      description = "Whether to open the firewall for the soulseek network listen port (not the web interface port).";
       default = false;
     };
 
     settings = mkOption {
-      description = lib.mdDoc ''
-        Configuration for slskd, see
-        [available options](https://github.com/slskd/slskd/blob/master/docs/config.md)
-        `APP_DIR` is set to /var/lib/slskd, where default download & incomplete directories,
-        log and databases will be created.
+      description = ''
+        Application configuration for slskd. See
+        [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md).
       '';
       default = {};
       type = submodule {
         freeformType = settingsFormat.type;
         options = {
+          remote_file_management = mkEnableOption "modification of share contents through the web ui";
+
+          flags = {
+            force_share_scan = mkOption {
+              type = bool;
+              description = "Force a rescan of shares on every startup.";
+            };
+            no_version_check = mkOption {
+              type = bool;
+              default = true;
+              visible = false;
+              description = "Don't perform a version check on startup.";
+            };
+          };
+
+          directories = {
+            incomplete = mkOption {
+              type = nullOr path;
+              description = "Directory where incomplete downloading files are stored.";
+              defaultText = "/var/lib/slskd/incomplete";
+              default = null;
+            };
+            downloads = mkOption {
+              type = nullOr path;
+              description = "Directory where downloaded files are stored.";
+              defaultText = "/var/lib/slskd/downloads";
+              default = null;
+            };
+          };
+
+          shares = {
+            directories = mkOption {
+              type = listOf str;
+              description = ''
+                Paths to shared directories. See
+                [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories)
+                for advanced usage.
+              '';
+              example = lib.literalExpression ''[ "/home/John/Music" "!/home/John/Music/Recordings" "[Music Drive]/mnt" ]'';
+            };
+            filters = mkOption {
+              type = listOf str;
+              example = lib.literalExpression ''[ "\.ini$" "Thumbs.db$" "\.DS_Store$" ]'';
+              description = "Regular expressions of files to exclude from sharing.";
+            };
+          };
+
+          rooms = mkOption {
+            type = listOf str;
+            description = "Chat rooms to join on startup.";
+          };
 
           soulseek = {
-            username = mkOption {
+            description = mkOption {
               type = str;
-              description = "Username on the Soulseek Network";
+              description = "The user description for the Soulseek network.";
+              defaultText = "A slskd user. https://github.com/slskd/slskd";
             };
             listen_port = mkOption {
               type = port;
-              description = "Port to use for communication on the Soulseek Network";
-              default = 50000;
+              description = "The port on which to listen for incoming connections.";
+              default = 50300;
             };
           };
 
+          global = {
+            # TODO speed units
+            upload = {
+              slots = mkOption {
+                type = ints.unsigned;
+                description = "Limit of the number of concurrent upload slots.";
+              };
+              speed_limit = mkOption {
+                type = ints.unsigned;
+                description = "Total upload speed limit.";
+              };
+            };
+            download = {
+              slots = mkOption {
+                type = ints.unsigned;
+                description = "Limit of the number of concurrent download slots.";
+              };
+              speed_limit = mkOption {
+                type = ints.unsigned;
+                description = "Total upload download limit";
+              };
+            };
+          };
+
+          filters.search.request = mkOption {
+            type = listOf str;
+            example = lib.literalExpression ''[ "^.{1,2}$" ]'';
+            description = "Incoming search requests which match this filter are ignored.";
+          };
+
           web = {
             port = mkOption {
               type = port;
-              default = 5001;
-              description = "The HTTP listen port";
+              default = 5030;
+              description = "The HTTP listen port.";
             };
             url_base = mkOption {
               type = path;
-              default = config.services.slskd.nginx.contextPath;
-              defaultText = "config.services.slskd.nginx.contextPath";
-              description = lib.mdDoc ''
-                The context path, i.e., the last part of the slskd URL
-              '';
+              default = "/";
+              description = "The base path in the url for web requests.";
+            };
+            # Users should use a reverse proxy instead for https
+            https.disabled = mkOption {
+              type = bool;
+              default = true;
+              description = "Disable the built-in HTTPS server";
             };
           };
 
-          shares = {
-            directories = mkOption {
-              type = listOf str;
-              description = lib.mdDoc ''
-                Paths to your shared directories. See
-                [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories)
-                for advanced usage
-              '';
+          retention = {
+            transfers = {
+              upload = {
+                succeeded = mkOption {
+                  type = ints.unsigned;
+                  description = "Lifespan of succeeded upload tasks.";
+                  defaultText = "(indefinite)";
+                };
+                errored = mkOption {
+                  type = ints.unsigned;
+                  description = "Lifespan of errored upload tasks.";
+                  defaultText = "(indefinite)";
+                };
+                cancelled = mkOption {
+                  type = ints.unsigned;
+                  description = "Lifespan of cancelled upload tasks.";
+                  defaultText = "(indefinite)";
+                };
+              };
+              download = {
+                succeeded = mkOption {
+                  type = ints.unsigned;
+                  description = "Lifespan of succeeded download tasks.";
+                  defaultText = "(indefinite)";
+                };
+                errored = mkOption {
+                  type = ints.unsigned;
+                  description = "Lifespan of errored download tasks.";
+                  defaultText = "(indefinite)";
+                };
+                cancelled = mkOption {
+                  type = ints.unsigned;
+                  description = "Lifespan of cancelled download tasks.";
+                  defaultText = "(indefinite)";
+                };
+              };
+            };
+            files = {
+              complete = mkOption {
+                type = ints.unsigned;
+                description = "Lifespan of completely downloaded files in minutes.";
+                example = 20160;
+                defaultText = "(indefinite)";
+              };
+              incomplete = mkOption {
+                type = ints.unsigned;
+                description = "Lifespan of incomplete downloading files in minutes.";
+                defaultText = "(indefinite)";
+              };
             };
           };
 
-          directories = {
-            incomplete = mkOption {
-              type = nullOr path;
-              description = "Directory where downloading files are stored";
-              defaultText = "<APP_DIR>/incomplete";
-              default = null;
-            };
-            downloads = mkOption {
-              type = nullOr path;
-              description = "Directory where downloaded files are stored";
-              defaultText = "<APP_DIR>/downloads";
-              default = null;
+          logger = {
+            # Disable by default, journald already retains as needed
+            disk = mkOption {
+              type = bool;
+              description = "Whether to log to the application directory.";
+              default = false;
+              visible = false;
             };
           };
         };
@@ -126,51 +254,42 @@ in {
   config = let
     cfg = config.services.slskd;
 
-    confWithoutNullValues = (lib.filterAttrs (key: value: value != null) cfg.settings);
+    confWithoutNullValues = (lib.filterAttrsRecursive (key: value: (builtins.tryEval value).success && value != null) cfg.settings);
 
     configurationYaml = settingsFormat.generate "slskd.yml" confWithoutNullValues;
 
   in lib.mkIf cfg.enable {
 
-    users = {
-      users.slskd = {
+    # Force off, configuration file is in nix store and is immutable
+    services.slskd.settings.remote_configuration = lib.mkForce false;
+
+    users.users = lib.optionalAttrs (cfg.user == defaultUser) {
+      "${defaultUser}" = {
+        group = cfg.group;
         isSystemUser = true;
-        group = "slskd";
       };
-      groups.slskd = {};
     };
 
-    # Reverse proxy configuration
-    services.nginx.enable = true;
-    services.nginx.virtualHosts."${cfg.nginx.domainName}" = {
-      forceSSL = true;
-      enableACME = true;
-      locations = {
-        "${cfg.nginx.contextPath}" = {
-          proxyPass = "http://localhost:${toString cfg.settings.web.port}";
-          proxyWebsockets = true;
-        };
-      };
+    users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
+      "${defaultUser}" = {};
     };
 
-    # Hide state & logs
-    systemd.tmpfiles.rules = [
-      "d /var/lib/slskd/data 0750 slskd slskd - -"
-      "d /var/lib/slskd/logs 0750 slskd slskd - -"
-    ];
-
     systemd.services.slskd = {
       description = "A modern client-server application for the Soulseek file sharing network";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         Type = "simple";
-        User = "slskd";
+        User = cfg.user;
+        Group = cfg.group;
         EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
-        StateDirectory = "slskd";
+        StateDirectory = "slskd";  # Creates /var/lib/slskd and manages permissions
         ExecStart = "${cfg.package}/bin/slskd --app-dir /var/lib/slskd --config ${configurationYaml}";
         Restart = "on-failure";
         ReadOnlyPaths = map (d: builtins.elemAt (builtins.split "[^/]*(/.+)" d) 1) cfg.settings.shares.directories;
+        ReadWritePaths =
+          (lib.optional (cfg.settings.directories.incomplete != null) cfg.settings.directories.incomplete) ++
+          (lib.optional (cfg.settings.directories.downloads != null) cfg.settings.directories.downloads);
         LockPersonality = true;
         NoNewPrivileges = true;
         PrivateDevices = true;
@@ -194,18 +313,21 @@ in {
 
     networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.settings.soulseek.listen_port;
 
-    systemd.services.slskd-rotatelogs = lib.mkIf cfg.rotateLogs {
-      description = "Rotate slskd logs";
-      serviceConfig = {
-        Type = "oneshot";
-        User = "slskd";
-        ExecStart = [
-          "${pkgs.findutils}/bin/find /var/lib/slskd/logs/ -type f -mtime +10 -delete"
-          "${pkgs.findutils}/bin/find /var/lib/slskd/logs/ -type f -mtime +1  -exec ${pkgs.gzip}/bin/gzip -q {} ';'"
-        ];
-      };
-      startAt = "daily";
+    services.nginx = lib.mkIf (cfg.domain != null) {
+      enable = lib.mkDefault true;
+      virtualHosts."${cfg.domain}" = lib.mkMerge [
+        cfg.nginx
+        {
+          locations."${cfg.settings.web.url_base}" = {
+            proxyPass = "http://127.0.0.1:${toString cfg.settings.web.port}";
+            proxyWebsockets = true;
+          };
+        }
+      ];
     };
+  };
 
+  meta = {
+    maintainers = with lib.maintainers; [ ppom melvyn2 ];
   };
 }
diff --git a/nixpkgs/nixos/modules/services/web-apps/suwayomi-server.md b/nixpkgs/nixos/modules/services/web-apps/suwayomi-server.md
index 18e7a631443f..2185556a8721 100644
--- a/nixpkgs/nixos/modules/services/web-apps/suwayomi-server.md
+++ b/nixpkgs/nixos/modules/services/web-apps/suwayomi-server.md
@@ -100,7 +100,7 @@ Not all the configuration options are available directly in this module, but you
       server = {
         port = 4567;
         autoDownloadNewChapters = false;
-        maxSourcesInParallel" = 6;
+        maxSourcesInParallel = 6;
         extensionRepos = [
           "https://raw.githubusercontent.com/MY_ACCOUNT/MY_REPO/repo/index.min.json"
         ];