summary refs log tree commit diff
path: root/nixos/modules
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules')
-rw-r--r--nixos/modules/config/nix-channel.nix3
-rw-r--r--nixos/modules/image/repart.md137
-rw-r--r--nixos/modules/image/repart.nix55
-rw-r--r--nixos/modules/installer/cd-dvd/channel.nix54
-rw-r--r--nixos/modules/module-list.nix8
-rw-r--r--nixos/modules/programs/direnv.nix2
-rw-r--r--nixos/modules/services/backup/postgresql-wal-receiver.nix4
-rw-r--r--nixos/modules/services/continuous-integration/woodpecker/server.nix6
-rw-r--r--nixos/modules/services/databases/postgresql.md32
-rw-r--r--nixos/modules/services/databases/postgresql.nix10
-rw-r--r--nixos/modules/services/development/livebook.md39
-rw-r--r--nixos/modules/services/development/livebook.nix90
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix1
-rw-r--r--nixos/modules/services/misc/paperless.nix25
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix2
-rw-r--r--nixos/modules/services/networking/bitcoind.nix3
-rw-r--r--nixos/modules/services/security/privacyidea.nix458
-rw-r--r--nixos/modules/services/web-apps/peertube.nix131
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix39
19 files changed, 321 insertions, 778 deletions
diff --git a/nixos/modules/config/nix-channel.nix b/nixos/modules/config/nix-channel.nix
index 4abc846b085..a7ca7a5c74a 100644
--- a/nixos/modules/config/nix-channel.nix
+++ b/nixos/modules/config/nix-channel.nix
@@ -98,8 +98,7 @@ in
     nix.settings.nix-path = mkIf (! cfg.channel.enable) (mkDefault "");
 
     systemd.tmpfiles.rules = lib.mkIf cfg.channel.enable [
-      "f /root/.nix-channels -"
-      ''w "/root/.nix-channels" - - - - "${config.system.defaultChannel} nixos\n"''
+      ''f /root/.nix-channels - - - - ${config.system.defaultChannel} nixos\n''
     ];
   };
 }
diff --git a/nixos/modules/image/repart.md b/nixos/modules/image/repart.md
deleted file mode 100644
index 6d0675f21a0..00000000000
--- a/nixos/modules/image/repart.md
+++ /dev/null
@@ -1,137 +0,0 @@
-# Building Images via `systemd-repart` {#sec-image-repart}
-
-You can build disk images in NixOS with the `image.repart` option provided by
-the module [image/repart.nix][]. This module uses `systemd-repart` to build the
-images and exposes it's entire interface via the `repartConfig` option.
-
-[image/repart.nix]: https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/image/repart.nix
-
-An example of how to build an image:
-
-```nix
-{ config, modulesPath, ... }: {
-
-  imports = [ "${modulesPath}/image/repart.nix" ];
-
-  image.repart = {
-    name = "image";
-    partitions = {
-      "esp" = {
-        contents = {
-          ...
-        };
-        repartConfig = {
-          Type = "esp";
-          ...
-        };
-      };
-      "root" = {
-        storePaths = [ config.system.build.toplevel ];
-        repartConfig = {
-          Type = "root";
-          Label = "nixos";
-          ...
-        };
-      };
-    };
-  };
-
-}
-```
-
-## Nix Store Partition {#sec-image-repart-store-partition}
-
-You can define a partition that only contains the Nix store and then mount it
-under `/nix/store`. Because the `/nix/store` part of the paths is already
-determined by the mount point, you have to set `stripNixStorePrefix = true;` so
-that the prefix is stripped from the paths before copying them into the image.
-
-```nix
-fileSystems."/nix/store".device = "/dev/disk/by-partlabel/nix-store"
-
-image.repart.partitions = {
-  "store" = {
-    storePaths = [ config.system.build.toplevel ];
-    stripNixStorePrefix = true;
-    repartConfig = {
-      Type = "linux-generic";
-      Label = "nix-store";
-      ...
-    };
-  };
-};
-```
-
-## Appliance Image {#sec-image-repart-appliance}
-
-The `image/repart.nix` module can also be used to build self-contained [software
-appliances][].
-
-[software appliances]: https://en.wikipedia.org/wiki/Software_appliance
-
-The generation based update mechanism of NixOS is not suited for appliances.
-Updates of appliances are usually either performed by replacing the entire
-image with a new one or by updating partitions via an A/B scheme. See the
-[Chrome OS update process][chrome-os-update] for an example of how to achieve
-this. The appliance image built in the following example does not contain a
-`configuration.nix` and thus you will not be able to call `nixos-rebuild` from
-this system.
-
-[chrome-os-update]: https://chromium.googlesource.com/aosp/platform/system/update_engine/+/HEAD/README.md
-
-```nix
-let
-  pkgs = import <nixpkgs> { };
-  efiArch = pkgs.stdenv.hostPlatform.efiArch;
-in
-(pkgs.nixos [
-  ({ config, lib, pkgs, modulesPath, ... }: {
-
-    imports = [ "${modulesPath}/image/repart.nix" ];
-
-    boot.loader.grub.enable = false;
-
-    fileSystems."/".device = "/dev/disk/by-label/nixos";
-
-    image.repart = {
-      name = "image";
-      partitions = {
-        "esp" = {
-          contents = {
-            "/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source =
-              "${pkgs.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi";
-
-            "/loader/entries/nixos.conf".source = pkgs.writeText "nixos.conf" ''
-              title NixOS
-              linux /EFI/nixos/kernel.efi
-              initrd /EFI/nixos/initrd.efi
-              options init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}
-            '';
-
-            "/EFI/nixos/kernel.efi".source =
-              "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
-
-            "/EFI/nixos/initrd.efi".source =
-              "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
-          };
-          repartConfig = {
-            Type = "esp";
-            Format = "vfat";
-            SizeMinBytes = "96M";
-          };
-        };
-        "root" = {
-          storePaths = [ config.system.build.toplevel ];
-          repartConfig = {
-            Type = "root";
-            Format = "ext4";
-            Label = "nixos";
-            Minimize = "guess";
-          };
-        };
-      };
-    };
-
-  })
-]).image
-```
diff --git a/nixos/modules/image/repart.nix b/nixos/modules/image/repart.nix
index e567485c9d3..41e6110885b 100644
--- a/nixos/modules/image/repart.nix
+++ b/nixos/modules/image/repart.nix
@@ -34,12 +34,13 @@ let
           };
         });
         default = { };
-        example = lib.literalExpression '' {
-          "/EFI/BOOT/BOOTX64.EFI".source =
-            "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
+        example = lib.literalExpression ''
+          {
+            "/EFI/BOOT/BOOTX64.EFI".source =
+              "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
 
-          "/loader/entries/nixos.conf".source = systemdBootEntry;
-        }
+            "/loader/entries/nixos.conf".source = systemdBootEntry;
+          }
         '';
         description = lib.mdDoc "The contents to end up in the filesystem image.";
       };
@@ -90,34 +91,33 @@ in
 
     package = lib.mkPackageOption pkgs "systemd-repart" {
       default = "systemd";
-      example = lib.literalExpression ''
-        pkgs.systemdMinimal.override { withCryptsetup = true; }
-      '';
+      example = "pkgs.systemdMinimal.override { withCryptsetup = true; }";
     };
 
     partitions = lib.mkOption {
       type = with lib.types; attrsOf (submodule partitionOptions);
       default = { };
-      example = lib.literalExpression '' {
-        "10-esp" = {
-          contents = {
-            "/EFI/BOOT/BOOTX64.EFI".source =
-              "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
-          }
-          repartConfig = {
-            Type = "esp";
-            Format = "fat";
+      example = lib.literalExpression ''
+        {
+          "10-esp" = {
+            contents = {
+              "/EFI/BOOT/BOOTX64.EFI".source =
+                "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
+            }
+            repartConfig = {
+              Type = "esp";
+              Format = "fat";
+            };
           };
-        };
-        "20-root" = {
-          storePaths = [ config.system.build.toplevel ];
-          repartConfig = {
-            Type = "root";
-            Format = "ext4";
-            Minimize = "guess";
+          "20-root" = {
+            storePaths = [ config.system.build.toplevel ];
+            repartConfig = {
+              Type = "root";
+              Format = "ext4";
+              Minimize = "guess";
+            };
           };
         };
-      };
       '';
       description = lib.mdDoc ''
         Specify partitions as a set of the names of the partitions with their
@@ -208,10 +208,7 @@ in
           | tee repart-output.json
       '';
 
-    meta = {
-      maintainers = with lib.maintainers; [ nikstur ];
-      doc = ./repart.md;
-    };
+    meta.maintainers = with lib.maintainers; [ nikstur ];
 
   };
 }
diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix
index 8426ba8fac0..bc70dc985fe 100644
--- a/nixos/modules/installer/cd-dvd/channel.nix
+++ b/nixos/modules/installer/cd-dvd/channel.nix
@@ -3,8 +3,6 @@
 
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   # This is copied into the installer image, so it's important that it is filtered
   # to avoid including a large .git directory.
@@ -27,38 +25,40 @@ let
       if [ ! -e $out/nixos/nixpkgs ]; then
         ln -s . $out/nixos/nixpkgs
       fi
-      ${optionalString (config.system.nixos.revision != null) ''
+      ${lib.optionalString (config.system.nixos.revision != null) ''
         echo -n ${config.system.nixos.revision} > $out/nixos/.git-revision
       ''}
       echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
       echo ${config.system.nixos.versionSuffix} | sed -e s/pre// > $out/nixos/svn-revision
     '';
-
 in
 
 {
-  # Pin the nixpkgs flake in the installer to our cleaned up nixpkgs source.
-  # FIXME: this might be surprising and is really only needed for offline installations,
-  # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
-  nix.registry.nixpkgs.to = {
-    type = "path";
-    path = "${channelSources}/nixos";
-  };
+  options.system.installer.channel.enable = (lib.mkEnableOption "bundling NixOS/Nixpkgs channel in the installer") // { default = true; };
+  config = lib.mkIf config.system.installer.channel.enable {
+    # Pin the nixpkgs flake in the installer to our cleaned up nixpkgs source.
+    # FIXME: this might be surprising and is really only needed for offline installations,
+    # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
+    nix.registry.nixpkgs.to = {
+      type = "path";
+      path = "${channelSources}/nixos";
+    };
 
-  # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
-  # for nixos-install.
-  boot.postBootCommands = mkAfter
-    ''
-      if ! [ -e /var/lib/nixos/did-channel-init ]; then
-        echo "unpacking the NixOS/Nixpkgs sources..."
-        mkdir -p /nix/var/nix/profiles/per-user/root
-        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \
-          -i ${channelSources} --quiet --option build-use-substitutes false \
-          ${optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root
-        mkdir -m 0700 -p /root/.nix-defexpr
-        ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels
-        mkdir -m 0755 -p /var/lib/nixos
-        touch /var/lib/nixos/did-channel-init
-      fi
-    '';
+    # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
+    # for nixos-install.
+    boot.postBootCommands = lib.mkAfter
+      ''
+        if ! [ -e /var/lib/nixos/did-channel-init ]; then
+          echo "unpacking the NixOS/Nixpkgs sources..."
+          mkdir -p /nix/var/nix/profiles/per-user/root
+          ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \
+            -i ${channelSources} --quiet --option build-use-substitutes false \
+            ${lib.optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root
+          mkdir -m 0700 -p /root/.nix-defexpr
+          ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels
+          mkdir -m 0755 -p /var/lib/nixos
+          touch /var/lib/nixos/did-channel-init
+        fi
+      '';
+  };
 }
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index bfac651f5a8..2a6ca202024 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -485,6 +485,7 @@
   ./services/development/hoogle.nix
   ./services/development/jupyter/default.nix
   ./services/development/jupyterhub/default.nix
+  ./services/development/livebook.nix
   ./services/development/lorri.nix
   ./services/development/rstudio-server/default.nix
   ./services/development/zammad.nix
@@ -1175,7 +1176,6 @@
   ./services/security/opensnitch.nix
   ./services/security/pass-secret-service.nix
   ./services/security/physlock.nix
-  ./services/security/privacyidea.nix
   ./services/security/shibboleth-sp.nix
   ./services/security/sks.nix
   ./services/security/sshguard.nix
@@ -1530,5 +1530,9 @@
   ./virtualisation/waydroid.nix
   ./virtualisation/xe-guest-utilities.nix
   ./virtualisation/xen-dom0.nix
-  { documentation.nixos.extraModules = [ ./virtualisation/qemu-vm.nix ]; }
+  { documentation.nixos.extraModules = [
+    ./virtualisation/qemu-vm.nix
+    ./image/repart.nix
+    ];
+  }
 ]
diff --git a/nixos/modules/programs/direnv.nix b/nixos/modules/programs/direnv.nix
index 77a6568e73b..2566fa7699b 100644
--- a/nixos/modules/programs/direnv.nix
+++ b/nixos/modules/programs/direnv.nix
@@ -54,7 +54,7 @@ in {
   };
 
   imports = [
-    (lib.mkRemovedOptionModule ["programs" "direnv" "persistDerivations"] "persistDerivations was removed as it is on longer necessary")
+    (lib.mkRemovedOptionModule ["programs" "direnv" "persistDerivations"] "persistDerivations was removed as it is no longer necessary")
   ];
 
   config = lib.mkIf cfg.enable {
diff --git a/nixos/modules/services/backup/postgresql-wal-receiver.nix b/nixos/modules/services/backup/postgresql-wal-receiver.nix
index 01fd57f5c50..773dc0ba447 100644
--- a/nixos/modules/services/backup/postgresql-wal-receiver.nix
+++ b/nixos/modules/services/backup/postgresql-wal-receiver.nix
@@ -7,7 +7,7 @@ let
     options = {
       postgresqlPackage = mkOption {
         type = types.package;
-        example = literalExpression "pkgs.postgresql_11";
+        example = literalExpression "pkgs.postgresql_15";
         description = lib.mdDoc ''
           PostgreSQL package to use.
         '';
@@ -124,7 +124,7 @@ in {
         example = literalExpression ''
           {
             main = {
-              postgresqlPackage = pkgs.postgresql_11;
+              postgresqlPackage = pkgs.postgresql_15;
               directory = /mnt/pg_wal/main/;
               slot = "main_wal_receiver";
               connection = "postgresql://user@somehost";
diff --git a/nixos/modules/services/continuous-integration/woodpecker/server.nix b/nixos/modules/services/continuous-integration/woodpecker/server.nix
index cae5ed7cf11..dc8f1567f1e 100644
--- a/nixos/modules/services/continuous-integration/woodpecker/server.nix
+++ b/nixos/modules/services/continuous-integration/woodpecker/server.nix
@@ -31,8 +31,8 @@ in
         description = lib.mdDoc "woodpecker-server config environment variables, for other options read the [documentation](https://woodpecker-ci.org/docs/administration/server-config)";
       };
       environmentFile = lib.mkOption {
-        type = lib.types.nullOr lib.types.path;
-        default = null;
+        type = with lib.types; coercedTo path (f: [ f ]) (listOf path);
+        default = [ ];
         example = "/root/woodpecker-server.env";
         description = lib.mdDoc ''
           File to load environment variables
@@ -61,7 +61,7 @@ in
           StateDirectoryMode = "0700";
           UMask = "0007";
           ConfigurationDirectory = "woodpecker-server";
-          EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
+          EnvironmentFile = cfg.environmentFile;
           ExecStart = "${cfg.package}/bin/woodpecker-server";
           Restart = "on-failure";
           RestartSec = 15;
diff --git a/nixos/modules/services/databases/postgresql.md b/nixos/modules/services/databases/postgresql.md
index 4d66ee38be4..e4b679a3eee 100644
--- a/nixos/modules/services/databases/postgresql.md
+++ b/nixos/modules/services/databases/postgresql.md
@@ -17,9 +17,9 @@ PostgreSQL is an advanced, free relational database.
 To enable PostgreSQL, add the following to your {file}`configuration.nix`:
 ```
 services.postgresql.enable = true;
-services.postgresql.package = pkgs.postgresql_11;
+services.postgresql.package = pkgs.postgresql_15;
 ```
-Note that you are required to specify the desired version of PostgreSQL (e.g. `pkgs.postgresql_11`). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for [](#opt-services.postgresql.package) such as the most recent release of PostgreSQL.
+Note that you are required to specify the desired version of PostgreSQL (e.g. `pkgs.postgresql_15`). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for [](#opt-services.postgresql.package) such as the most recent release of PostgreSQL.
 
 <!--
 After running {command}`nixos-rebuild`, you can verify
@@ -119,27 +119,27 @@ A complete list of options for the PostgreSQL module may be found [here](#opt-se
 
 ## Plugins {#module-services-postgres-plugins}
 
-Plugins collection for each PostgreSQL version can be accessed with `.pkgs`. For example, for `pkgs.postgresql_11` package, its plugin collection is accessed by `pkgs.postgresql_11.pkgs`:
+Plugins collection for each PostgreSQL version can be accessed with `.pkgs`. For example, for `pkgs.postgresql_15` package, its plugin collection is accessed by `pkgs.postgresql_15.pkgs`:
 ```ShellSession
 $ nix repl '<nixpkgs>'
 
 Loading '<nixpkgs>'...
 Added 10574 variables.
 
-nix-repl> postgresql_11.pkgs.<TAB><TAB>
-postgresql_11.pkgs.cstore_fdw        postgresql_11.pkgs.pg_repack
-postgresql_11.pkgs.pg_auto_failover  postgresql_11.pkgs.pg_safeupdate
-postgresql_11.pkgs.pg_bigm           postgresql_11.pkgs.pg_similarity
-postgresql_11.pkgs.pg_cron           postgresql_11.pkgs.pg_topn
-postgresql_11.pkgs.pg_hll            postgresql_11.pkgs.pgjwt
-postgresql_11.pkgs.pg_partman        postgresql_11.pkgs.pgroonga
+nix-repl> postgresql_15.pkgs.<TAB><TAB>
+postgresql_15.pkgs.cstore_fdw        postgresql_15.pkgs.pg_repack
+postgresql_15.pkgs.pg_auto_failover  postgresql_15.pkgs.pg_safeupdate
+postgresql_15.pkgs.pg_bigm           postgresql_15.pkgs.pg_similarity
+postgresql_15.pkgs.pg_cron           postgresql_15.pkgs.pg_topn
+postgresql_15.pkgs.pg_hll            postgresql_15.pkgs.pgjwt
+postgresql_15.pkgs.pg_partman        postgresql_15.pkgs.pgroonga
 ...
 ```
 
 To add plugins via NixOS configuration, set `services.postgresql.extraPlugins`:
 ```
-services.postgresql.package = pkgs.postgresql_11;
-services.postgresql.extraPlugins = with pkgs.postgresql_11.pkgs; [
+services.postgresql.package = pkgs.postgresql_12;
+services.postgresql.extraPlugins = with pkgs.postgresql_12.pkgs; [
   pg_repack
   postgis
 ];
@@ -148,7 +148,7 @@ services.postgresql.extraPlugins = with pkgs.postgresql_11.pkgs; [
 You can build custom PostgreSQL-with-plugins (to be used outside of NixOS) using function `.withPackages`. For example, creating a custom PostgreSQL package in an overlay can look like:
 ```
 self: super: {
-  postgresql_custom = self.postgresql_11.withPackages (ps: [
+  postgresql_custom = self.postgresql_12.withPackages (ps: [
     ps.pg_repack
     ps.postgis
   ]);
@@ -158,9 +158,9 @@ self: super: {
 Here's a recipe on how to override a particular plugin through an overlay:
 ```
 self: super: {
-  postgresql_11 = super.postgresql_11.override { this = self.postgresql_11; } // {
-    pkgs = super.postgresql_11.pkgs // {
-      pg_repack = super.postgresql_11.pkgs.pg_repack.overrideAttrs (_: {
+  postgresql_15 = super.postgresql_15.override { this = self.postgresql_15; } // {
+    pkgs = super.postgresql_15.pkgs // {
+      pg_repack = super.postgresql_15.pkgs.pg_repack.overrideAttrs (_: {
         name = "pg_repack-v20181024";
         src = self.fetchzip {
           url = "https://github.com/reorg/pg_repack/archive/923fa2f3c709a506e111cc963034bf2fd127aa00.tar.gz";
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 2d4ef056318..21e6a60e32a 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -55,7 +55,7 @@ in
 
       package = mkOption {
         type = types.package;
-        example = literalExpression "pkgs.postgresql_11";
+        example = literalExpression "pkgs.postgresql_15";
         description = lib.mdDoc ''
           PostgreSQL package to use.
         '';
@@ -78,7 +78,7 @@ in
       dataDir = mkOption {
         type = types.path;
         defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
-        example = "/var/lib/postgresql/11";
+        example = "/var/lib/postgresql/15";
         description = lib.mdDoc ''
           The data directory for PostgreSQL. If left as the default value
           this directory will automatically be created before the PostgreSQL server starts, otherwise
@@ -387,7 +387,7 @@ in
       extraPlugins = mkOption {
         type = types.listOf types.path;
         default = [];
-        example = literalExpression "with pkgs.postgresql_11.pkgs; [ postgis pg_repack ]";
+        example = literalExpression "with pkgs.postgresql_15.pkgs; [ postgis pg_repack ]";
         description = lib.mdDoc ''
           List of PostgreSQL plugins. PostgreSQL version for each plugin should
           match version for `services.postgresql.package` value.
@@ -399,7 +399,7 @@ in
         default = {};
         description = lib.mdDoc ''
           PostgreSQL configuration. Refer to
-          <https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
+          <https://www.postgresql.org/docs/15/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
           for an overview of `postgresql.conf`.
 
           ::: {.note}
@@ -461,7 +461,7 @@ in
         base = if versionAtLeast config.system.stateVersion "23.11" then pkgs.postgresql_15
             else if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14
             else if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
-            else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
+            else if versionAtLeast config.system.stateVersion "20.03" then mkThrow "11"
             else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
             else mkThrow "9_5";
     in
diff --git a/nixos/modules/services/development/livebook.md b/nixos/modules/services/development/livebook.md
new file mode 100644
index 00000000000..73ddc57f617
--- /dev/null
+++ b/nixos/modules/services/development/livebook.md
@@ -0,0 +1,39 @@
+# Livebook {#module-services-livebook}
+
+[Livebook](https://livebook.dev/) is a web application for writing
+interactive and collaborative code notebooks.
+
+## Basic Usage {#module-services-livebook-basic-usage}
+
+Enabling the `livebook` service creates a user
+[`systemd`](https://www.freedesktop.org/wiki/Software/systemd/) unit
+which runs the server.
+
+```
+{ ... }:
+
+{
+  services.livebook = {
+    enableUserService = true;
+    port = 20123;
+    # See note below about security
+    environmentFile = pkgs.writeText "livebook.env" ''
+      LIVEBOOK_PASSWORD = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
+    '';
+  };
+}
+```
+
+::: {.note}
+
+The Livebook server has the ability to run any command as the user it
+is running under, so securing access to it with a password is highly
+recommended.
+
+Putting the password in the Nix configuration like above is an easy
+way to get started but it is not recommended in the real world because
+the `livebook.env` file will be added to the world-readable Nix store.
+A better approach would be to put the password in some secure
+user-readable location and set `environmentFile = /home/user/secure/livebook.env`.
+
+:::
diff --git a/nixos/modules/services/development/livebook.nix b/nixos/modules/services/development/livebook.nix
new file mode 100644
index 00000000000..3991a4125ec
--- /dev/null
+++ b/nixos/modules/services/development/livebook.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.livebook;
+in
+{
+  options.services.livebook = {
+    # Since livebook doesn't have a granular permission system (a user
+    # either has access to all the data or none at all), the decision
+    # was made to run this as a user service.  If that changes in the
+    # future, this can be changed to a system service.
+    enableUserService = mkEnableOption "a user service for Livebook";
+
+    environmentFile = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Environment file as defined in {manpage}`systemd.exec(5)` passed to the service.
+
+        This must contain at least `LIVEBOOK_PASSWORD` or
+        `LIVEBOOK_TOKEN_ENABLED=false`.  See `livebook server --help`
+        for other options.'';
+    };
+
+    erlang_node_short_name = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "livebook";
+      description = "A short name for the distributed node.";
+    };
+
+    erlang_node_name = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "livebook@127.0.0.1";
+      description = "The name for the app distributed node.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = "The port to start the web application on.";
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = lib.mdDoc ''
+        The address to start the web application on.  Must be a valid IPv4 or
+        IPv6 address.
+      '';
+    };
+
+    options = mkOption {
+      type = with types; attrsOf str;
+      default = { };
+      description = lib.mdDoc ''
+        Additional options to pass as command-line arguments to the server.
+      '';
+      example = literalExpression ''
+        {
+          cookie = "a value shared by all nodes in this cluster";
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enableUserService {
+    systemd.user.services.livebook = {
+      serviceConfig = {
+        Restart = "always";
+        EnvironmentFile = cfg.environmentFile;
+        ExecStart =
+          let
+            args = lib.cli.toGNUCommandLineShell { } ({
+              inherit (cfg) port;
+              ip = cfg.address;
+              name = cfg.erlang_node_name;
+              sname = cfg.erlang_node_short_name;
+            } // cfg.options);
+          in
+          "${pkgs.livebook}/bin/livebook server ${args}";
+      };
+      path = [ pkgs.bash ];
+      wantedBy = [ "default.target" ];
+    };
+  };
+
+  meta.doc = ./livebook.md;
+}
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index 0e6fa65667a..789b06af19b 100644
--- a/nixos/modules/services/home-automation/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -455,6 +455,7 @@ in {
           "govee_ble"
           "homekit_controller"
           "inkbird"
+          "improv_ble"
           "keymitt_ble"
           "led_ble"
           "medcom_ble"
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 9b8bd62809c..1e0a8d0f928 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -332,12 +332,28 @@ in
       # during migrations
       bindsTo = [ "paperless-scheduler.service" ];
       after = [ "paperless-scheduler.service" ];
+      # Setup PAPERLESS_SECRET_KEY.
+      # If this environment variable is left unset, paperless-ngx defaults
+      # to a well-known value, which is insecure.
+      script = let
+        secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key";
+      in ''
+        if [[ ! -f '${secretKeyFile}' ]]; then
+          (
+            umask 0377
+            tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}'
+          )
+        fi
+        export PAPERLESS_SECRET_KEY=$(cat '${secretKeyFile}')
+        if [[ ! $PAPERLESS_SECRET_KEY ]]; then
+          echo "PAPERLESS_SECRET_KEY is empty, refusing to start."
+          exit 1
+        fi
+        exec ${pkg.python.pkgs.gunicorn}/bin/gunicorn \
+          -c ${pkg}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
+      '';
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
-        ExecStart = ''
-          ${pkg.python.pkgs.gunicorn}/bin/gunicorn \
-            -c ${pkg}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
-        '';
         Restart = "on-failure";
 
         # gunicorn needs setuid, liblapack needs mbind
@@ -349,7 +365,6 @@ in
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
       };
       environment = env // {
-        PATH = mkForce pkg.path;
         PYTHONPATH = "${pkg.python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/paperless-ngx/src";
       };
       # Allow the web interface to access the private /tmp directory of the server.
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
index 36258866646..afe9eb4d7b9 100644
--- a/nixos/modules/services/monitoring/grafana-image-renderer.nix
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -108,7 +108,7 @@ in {
 
     services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
       server_url = "http://localhost:${toString cfg.settings.service.port}/render";
-      callback_url = "http://localhost:${toString config.services.grafana.settings.server.http_port}";
+      callback_url = "http://${config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
     };
 
     services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
diff --git a/nixos/modules/services/networking/bitcoind.nix b/nixos/modules/services/networking/bitcoind.nix
index a86d52b7202..a48066b43b1 100644
--- a/nixos/modules/services/networking/bitcoind.nix
+++ b/nixos/modules/services/networking/bitcoind.nix
@@ -3,8 +3,7 @@
 with lib;
 
 let
-
-  eachBitcoind = config.services.bitcoind;
+  eachBitcoind = filterAttrs (bitcoindName: cfg: cfg.enable) config.services.bitcoind;
 
   rpcUserOpts = { name, ... }: {
     options = {
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
deleted file mode 100644
index 664335cb58e..00000000000
--- a/nixos/modules/services/security/privacyidea.nix
+++ /dev/null
@@ -1,458 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.privacyidea;
-  opt = options.services.privacyidea;
-
-  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python310; };
-  python = uwsgi.python3;
-  penv = python.withPackages (const [ pkgs.privacyidea ]);
-  logCfg = pkgs.writeText "privacyidea-log.cfg" ''
-    [formatters]
-    keys=detail
-
-    [handlers]
-    keys=stream
-
-    [formatter_detail]
-    class=privacyidea.lib.log.SecureFormatter
-    format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
-
-    [handler_stream]
-    class=StreamHandler
-    level=NOTSET
-    formatter=detail
-    args=(sys.stdout,)
-
-    [loggers]
-    keys=root,privacyidea
-
-    [logger_privacyidea]
-    handlers=stream
-    qualname=privacyidea
-    level=INFO
-
-    [logger_root]
-    handlers=stream
-    level=ERROR
-  '';
-
-  piCfgFile = pkgs.writeText "privacyidea.cfg" ''
-    SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
-    SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///privacyidea'
-    SECRET_KEY = '${cfg.secretKey}'
-    PI_PEPPER = '${cfg.pepper}'
-    PI_ENCFILE = '${cfg.encFile}'
-    PI_AUDIT_KEY_PRIVATE = '${cfg.auditKeyPrivate}'
-    PI_AUDIT_KEY_PUBLIC = '${cfg.auditKeyPublic}'
-    PI_LOGCONFIG = '${logCfg}'
-    ${cfg.extraConfig}
-  '';
-
-  renderValue = x:
-    if isList x then concatMapStringsSep "," (x: ''"${x}"'') x
-    else if isString x && hasInfix "," x then ''"${x}"''
-    else x;
-
-  ldapProxyConfig = pkgs.writeText "ldap-proxy.ini"
-    (generators.toINI {}
-      (flip mapAttrs cfg.ldap-proxy.settings
-        (const (mapAttrs (const renderValue)))));
-
-  privacyidea-token-janitor = pkgs.writeShellScriptBin "privacyidea-token-janitor" ''
-    exec -a privacyidea-token-janitor \
-      /run/wrappers/bin/sudo -u ${cfg.user} \
-      env PRIVACYIDEA_CONFIGFILE=${cfg.stateDir}/privacyidea.cfg \
-      ${penv}/bin/privacyidea-token-janitor $@
-  '';
-in
-
-{
-  options = {
-    services.privacyidea = {
-      enable = mkEnableOption (lib.mdDoc "PrivacyIDEA");
-
-      environmentFile = mkOption {
-        type = types.nullOr types.path;
-        default = null;
-        example = "/root/privacyidea.env";
-        description = lib.mdDoc ''
-          File to load as environment file. Environment variables
-          from this file will be interpolated into the config file
-          using `envsubst` which is helpful for specifying
-          secrets:
-          ```
-          { services.privacyidea.secretKey = "$SECRET"; }
-          ```
-
-          The environment-file can now specify the actual secret key:
-          ```
-          SECRET=veryverytopsecret
-          ```
-        '';
-      };
-
-      stateDir = mkOption {
-        type = types.str;
-        default = "/var/lib/privacyidea";
-        description = lib.mdDoc ''
-          Directory where all PrivacyIDEA files will be placed by default.
-        '';
-      };
-
-      superuserRealm = mkOption {
-        type = types.listOf types.str;
-        default = [ "super" "administrators" ];
-        description = lib.mdDoc ''
-          The realm where users are allowed to login as administrators.
-        '';
-      };
-
-      secretKey = mkOption {
-        type = types.str;
-        example = "t0p s3cr3t";
-        description = lib.mdDoc ''
-          This is used to encrypt the auth_token.
-        '';
-      };
-
-      pepper = mkOption {
-        type = types.str;
-        example = "Never know...";
-        description = lib.mdDoc ''
-          This is used to encrypt the admin passwords.
-        '';
-      };
-
-      encFile = mkOption {
-        type = types.str;
-        default = "${cfg.stateDir}/enckey";
-        defaultText = literalExpression ''"''${config.${opt.stateDir}}/enckey"'';
-        description = lib.mdDoc ''
-          This is used to encrypt the token data and token passwords
-        '';
-      };
-
-      auditKeyPrivate = mkOption {
-        type = types.str;
-        default = "${cfg.stateDir}/private.pem";
-        defaultText = literalExpression ''"''${config.${opt.stateDir}}/private.pem"'';
-        description = lib.mdDoc ''
-          Private Key for signing the audit log.
-        '';
-      };
-
-      auditKeyPublic = mkOption {
-        type = types.str;
-        default = "${cfg.stateDir}/public.pem";
-        defaultText = literalExpression ''"''${config.${opt.stateDir}}/public.pem"'';
-        description = lib.mdDoc ''
-          Public key for checking signatures of the audit log.
-        '';
-      };
-
-      adminPasswordFile = mkOption {
-        type = types.path;
-        description = lib.mdDoc "File containing password for the admin user";
-      };
-
-      adminEmail = mkOption {
-        type = types.str;
-        example = "admin@example.com";
-        description = lib.mdDoc "Mail address for the admin user";
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = lib.mdDoc ''
-          Extra configuration options for pi.cfg.
-        '';
-      };
-
-      user = mkOption {
-        type = types.str;
-        default = "privacyidea";
-        description = lib.mdDoc "User account under which PrivacyIDEA runs.";
-      };
-
-      group = mkOption {
-        type = types.str;
-        default = "privacyidea";
-        description = lib.mdDoc "Group account under which PrivacyIDEA runs.";
-      };
-
-      tokenjanitor = {
-        enable = mkEnableOption (lib.mdDoc "automatic runs of the token janitor");
-        interval = mkOption {
-          default = "quarterly";
-          type = types.str;
-          description = lib.mdDoc ''
-            Interval in which the cleanup program is supposed to run.
-            See {manpage}`systemd.time(7)` for further information.
-          '';
-        };
-        action = mkOption {
-          type = types.enum [ "delete" "mark" "disable" "unassign" ];
-          description = lib.mdDoc ''
-            Which action to take for matching tokens.
-          '';
-        };
-        unassigned = mkOption {
-          default = false;
-          type = types.bool;
-          description = lib.mdDoc ''
-            Whether to search for **unassigned** tokens
-            and apply [](#opt-services.privacyidea.tokenjanitor.action)
-            onto them.
-          '';
-        };
-        orphaned = mkOption {
-          default = true;
-          type = types.bool;
-          description = lib.mdDoc ''
-            Whether to search for **orphaned** tokens
-            and apply [](#opt-services.privacyidea.tokenjanitor.action)
-            onto them.
-          '';
-        };
-      };
-
-      ldap-proxy = {
-        enable = mkEnableOption (lib.mdDoc "PrivacyIDEA LDAP Proxy");
-
-        configFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
-          description = lib.mdDoc ''
-            Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
-          '';
-        };
-
-        user = mkOption {
-          type = types.str;
-          default = "pi-ldap-proxy";
-          description = lib.mdDoc "User account under which PrivacyIDEA LDAP proxy runs.";
-        };
-
-        group = mkOption {
-          type = types.str;
-          default = "pi-ldap-proxy";
-          description = lib.mdDoc "Group account under which PrivacyIDEA LDAP proxy runs.";
-        };
-
-        settings = mkOption {
-          type = with types; attrsOf (attrsOf (oneOf [ str bool int (listOf str) ]));
-          default = {};
-          description = lib.mdDoc ''
-            Attribute-set containing the settings for `privacyidea-ldap-proxy`.
-            It's possible to pass secrets using env-vars as substitutes and
-            use the option [](#opt-services.privacyidea.ldap-proxy.environmentFile)
-            to inject them via `envsubst`.
-          '';
-        };
-
-        environmentFile = mkOption {
-          default = null;
-          type = types.nullOr types.str;
-          description = lib.mdDoc ''
-            Environment file containing secrets to be substituted into
-            [](#opt-services.privacyidea.ldap-proxy.settings).
-          '';
-        };
-      };
-    };
-  };
-
-  config = mkMerge [
-
-    (mkIf cfg.enable {
-
-      assertions = [
-        {
-          assertion = cfg.tokenjanitor.enable -> (cfg.tokenjanitor.orphaned || cfg.tokenjanitor.unassigned);
-          message = ''
-            privacyidea-token-janitor has no effect if neither orphaned nor unassigned tokens
-            are to be searched.
-          '';
-        }
-      ];
-
-      environment.systemPackages = [ pkgs.privacyidea (hiPrio privacyidea-token-janitor) ];
-
-      services.postgresql.enable = mkDefault true;
-
-      systemd.services.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
-        environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
-        path = [ penv ];
-        serviceConfig = {
-          CapabilityBoundingSet = [ "" ];
-          ExecStart = "${pkgs.writeShellScript "pi-token-janitor" ''
-            ${optionalString cfg.tokenjanitor.orphaned ''
-              echo >&2 "Removing orphaned tokens..."
-              privacyidea-token-janitor find \
-                --orphaned true \
-                --action ${cfg.tokenjanitor.action}
-            ''}
-            ${optionalString cfg.tokenjanitor.unassigned ''
-              echo >&2 "Removing unassigned tokens..."
-              privacyidea-token-janitor find \
-                --assigned false \
-                --action ${cfg.tokenjanitor.action}
-            ''}
-          ''}";
-          Group = cfg.group;
-          LockPersonality = true;
-          MemoryDenyWriteExecute = true;
-          ProtectHome = true;
-          ProtectHostname = true;
-          ProtectKernelLogs = true;
-          ProtectKernelModules = true;
-          ProtectKernelTunables = true;
-          ProtectSystem = "strict";
-          ReadWritePaths = cfg.stateDir;
-          Type = "oneshot";
-          User = cfg.user;
-          WorkingDirectory = cfg.stateDir;
-        };
-      };
-      systemd.timers.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
-        wantedBy = [ "timers.target" ];
-        timerConfig.OnCalendar = cfg.tokenjanitor.interval;
-        timerConfig.Persistent = true;
-      };
-
-      systemd.services.privacyidea = let
-        piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
-          uwsgi = {
-            buffer-size = 8192;
-            plugins = [ "python3" ];
-            pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
-            socket = "/run/privacyidea/socket";
-            uid = cfg.user;
-            gid = cfg.group;
-            chmod-socket = 770;
-            chown-socket = "${cfg.user}:nginx";
-            chdir = cfg.stateDir;
-            wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
-            processes = 4;
-            harakiri = 60;
-            reload-mercy = 8;
-            stats = "/run/privacyidea/stats.socket";
-            max-requests = 2000;
-            limit-as = 1024;
-            reload-on-as = 512;
-            reload-on-rss = 256;
-            no-orphans = true;
-            vacuum = true;
-          };
-        });
-      in {
-        wantedBy = [ "multi-user.target" ];
-        after = [ "postgresql.service" ];
-        path = with pkgs; [ openssl ];
-        environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
-        preStart = let
-          pi-manage = "${config.security.sudo.package}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
-          pgsu = config.services.postgresql.superUser;
-          psql = config.services.postgresql.package;
-        in ''
-          mkdir -p ${cfg.stateDir} /run/privacyidea
-          chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
-          umask 077
-          ${lib.getBin pkgs.envsubst}/bin/envsubst -o ${cfg.stateDir}/privacyidea.cfg \
-                                                   -i "${piCfgFile}"
-          chown ${cfg.user}:${cfg.group} ${cfg.stateDir}/privacyidea.cfg
-          if ! test -e "${cfg.stateDir}/db-created"; then
-            ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
-            ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
-            ${pi-manage} create_enckey
-            ${pi-manage} create_audit_keys
-            ${pi-manage} createdb
-            ${pi-manage} admin add admin -e ${cfg.adminEmail} -p "$(cat ${cfg.adminPasswordFile})"
-            ${pi-manage} db stamp head -d ${penv}/lib/privacyidea/migrations
-            touch "${cfg.stateDir}/db-created"
-            chmod g+r "${cfg.stateDir}/enckey" "${cfg.stateDir}/private.pem"
-          fi
-          ${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
-        '';
-        serviceConfig = {
-          Type = "notify";
-          ExecStart = "${uwsgi}/bin/uwsgi --json ${piuwsgi}";
-          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-          EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
-          ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
-          NotifyAccess = "main";
-          KillSignal = "SIGQUIT";
-        };
-      };
-
-      users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
-        group = cfg.group;
-        isSystemUser = true;
-      };
-
-      users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
-    })
-
-    (mkIf cfg.ldap-proxy.enable {
-
-      assertions = [
-        { assertion = let
-            xor = a: b: a && !b || !a && b;
-          in xor (cfg.ldap-proxy.settings == {}) (cfg.ldap-proxy.configFile == null);
-          message = "configFile & settings are mutually exclusive for services.privacyidea.ldap-proxy!";
-        }
-      ];
-
-      warnings = mkIf (cfg.ldap-proxy.configFile != null) [
-        "Using services.privacyidea.ldap-proxy.configFile is deprecated! Use the RFC42-style settings option instead!"
-      ];
-
-      systemd.services.privacyidea-ldap-proxy = let
-        ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
-      in {
-        description = "privacyIDEA LDAP proxy";
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = cfg.ldap-proxy.user;
-          Group = cfg.ldap-proxy.group;
-          StateDirectory = "privacyidea-ldap-proxy";
-          EnvironmentFile = mkIf (cfg.ldap-proxy.environmentFile != null)
-            [ cfg.ldap-proxy.environmentFile ];
-          ExecStartPre =
-            "${pkgs.writeShellScript "substitute-secrets-ldap-proxy" ''
-              umask 0077
-              ${pkgs.envsubst}/bin/envsubst \
-                -i ${ldapProxyConfig} \
-                -o $STATE_DIRECTORY/ldap-proxy.ini
-            ''}";
-          ExecStart = let
-            configPath = if cfg.ldap-proxy.settings != {}
-              then "%S/privacyidea-ldap-proxy/ldap-proxy.ini"
-              else cfg.ldap-proxy.configFile;
-          in ''
-            ${ldap-proxy-env}/bin/twistd \
-              --nodaemon \
-              --pidfile= \
-              -u ${cfg.ldap-proxy.user} \
-              -g ${cfg.ldap-proxy.group} \
-              ldap-proxy \
-              -c ${configPath}
-          '';
-          Restart = "always";
-        };
-      };
-
-      users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
-        group = cfg.ldap-proxy.group;
-        isSystemUser = true;
-      };
-
-      users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};
-    })
-  ];
-
-}
diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix
index 17e170c33de..a2246761141 100644
--- a/nixos/modules/services/web-apps/peertube.nix
+++ b/nixos/modules/services/web-apps/peertube.nix
@@ -352,6 +352,7 @@ in {
         };
         storage = {
           tmp = lib.mkDefault "/var/lib/peertube/storage/tmp/";
+          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/";
@@ -521,6 +522,21 @@ in {
           '';
         };
 
+        locations."~ ^/api/v1/runners/jobs/[^/]+/(update|success)$" = {
+          tryFiles = "/dev/null @api";
+          root = cfg.settings.storage.tmp;
+          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';
+          '';
+        };
+
         locations."~ ^/api/v1/(videos|video-playlists|video-channels|users/me)" = {
           tryFiles = "/dev/null @api";
           priority = 1140;
@@ -607,72 +623,33 @@ in {
           '';
         };
 
-        locations."^~ /lazy-static/avatars/" = {
-          tryFiles = "$uri @api";
-          root = cfg.settings.storage.avatars;
-          priority = 1330;
-          extraConfig = ''
-            if ($request_method = 'OPTIONS') {
-              ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Cache-Control                  'no-cache';
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
-            }
-
-            ${nginxCommonHeaders}
-            add_header Cache-Control                    'public, max-age=7200';
-
-            rewrite ^/lazy-static/avatars/(.*)$         /$1 break;
-          '';
-        };
-
-        locations."^~ /lazy-static/banners/" = {
-          tryFiles = "$uri @api";
-          root = cfg.settings.storage.avatars;
-          priority = 1340;
+        locations."^~ /download/" = {
+          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          priority = 1410;
           extraConfig = ''
-            if ($request_method = 'OPTIONS') {
-              ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Cache-Control                  'no-cache';
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
-            }
-
-            ${nginxCommonHeaders}
-            add_header Cache-Control                    'public, max-age=7200';
+            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
+            proxy_set_header Host                       $host;
+            proxy_set_header X-Real-IP                  $remote_addr;
 
-            rewrite ^/lazy-static/banners/(.*)$         /$1 break;
+            proxy_limit_rate                            5M;
           '';
         };
 
-        locations."^~ /lazy-static/previews/" = {
-          tryFiles = "$uri @api";
-          root = cfg.settings.storage.previews;
-          priority = 1350;
+        locations."^~ /static/streaming-playlists/private/" = {
+          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          priority = 1420;
           extraConfig = ''
-            if ($request_method = 'OPTIONS') {
-              ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Cache-Control                  'no-cache';
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
-            }
-
-            ${nginxCommonHeaders}
-            add_header Cache-Control                    'public, max-age=7200';
+            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
+            proxy_set_header Host                       $host;
+            proxy_set_header X-Real-IP                  $remote_addr;
 
-            rewrite ^/lazy-static/previews/(.*)$        /$1 break;
+            proxy_limit_rate                            5M;
           '';
         };
 
-        locations."^~ /static/streaming-playlists/private/" = {
+        locations."^~ /static/web-videos/private/" = {
           proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
-          priority = 1410;
+          priority = 1430;
           extraConfig = ''
             proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
             proxy_set_header Host                       $host;
@@ -684,7 +661,7 @@ in {
 
         locations."^~ /static/webseed/private/" = {
           proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
-          priority = 1420;
+          priority = 1440;
           extraConfig = ''
             proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
             proxy_set_header Host                       $host;
@@ -694,31 +671,45 @@ in {
           '';
         };
 
-        locations."^~ /static/thumbnails/" = {
+        locations."^~ /static/redundancy/" = {
           tryFiles = "$uri @api";
-          root = cfg.settings.storage.thumbnails;
-          priority = 1430;
+          root = cfg.settings.storage.redundancy;
+          priority = 1450;
           extraConfig = ''
+            set $peertube_limit_rate                    800k;
+
+            if ($request_uri ~ -fragmented.mp4$) {
+              set $peertube_limit_rate                  5M;
+            }
+
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
               add_header Access-Control-Max-Age         1728000;
-              add_header Cache-Control                  'no-cache';
               add_header Content-Type                   'text/plain charset=UTF-8';
               add_header Content-Length                 0;
               return                                    204;
             }
+            if ($request_method = 'GET') {
+              ${nginxCommonHeaders}
+
+              access_log                                off;
+            }
 
-            ${nginxCommonHeaders}
-            add_header Cache-Control                    'public, max-age=7200';
+            aio                                         threads;
+            sendfile                                    on;
+            sendfile_max_chunk                          1M;
+
+            limit_rate                                  $peertube_limit_rate;
+            limit_rate_after                            5M;
 
-            rewrite ^/static/thumbnails/(.*)$           /$1 break;
+            rewrite ^/static/redundancy/(.*)$           /$1 break;
           '';
         };
 
-        locations."^~ /static/redundancy/" = {
+        locations."^~ /static/streaming-playlists/" = {
           tryFiles = "$uri @api";
-          root = cfg.settings.storage.redundancy;
-          priority = 1440;
+          root = cfg.settings.storage.streaming_playlists;
+          priority = 1460;
           extraConfig = ''
             set $peertube_limit_rate                    800k;
 
@@ -746,14 +737,14 @@ in {
             limit_rate                                  $peertube_limit_rate;
             limit_rate_after                            5M;
 
-            rewrite ^/static/redundancy/(.*)$           /$1 break;
+            rewrite ^/static/streaming-playlists/(.*)$  /$1 break;
           '';
         };
 
-        locations."^~ /static/streaming-playlists/" = {
+        locations."^~ /static/web-videos/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.streaming_playlists;
-          priority = 1450;
+          priority = 1470;
           extraConfig = ''
             set $peertube_limit_rate                    800k;
 
@@ -788,7 +779,7 @@ in {
         locations."^~ /static/webseed/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.videos;
-          priority = 1460;
+          priority = 1480;
           extraConfig = ''
             set $peertube_limit_rate                    800k;
 
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index 71f5d7a752c..65e97d53724 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -239,6 +239,26 @@ let
   mkService = name: container: let
     dependsOn = map (x: "${cfg.backend}-${x}.service") container.dependsOn;
     escapedName = escapeShellArg name;
+    preStartScript = pkgs.writeShellApplication {
+      name = "pre-start";
+      runtimeInputs = [ ];
+      text = ''
+        ${cfg.backend} rm -f ${name} || true
+        ${optionalString (isValidLogin container.login) ''
+          cat ${container.login.passwordFile} | \
+          ${cfg.backend} login \
+          ${container.login.registry} \
+          --username ${container.login.username} \
+          --password-stdin
+        ''}
+        ${optionalString (container.imageFile != null) ''
+          ${cfg.backend} load -i ${container.imageFile}
+        ''}
+        ${optionalString (cfg.backend == "podman") ''
+          rm -f /run/podman-${escapedName}.ctr-id
+        ''}
+      '';
+    };
   in {
     wantedBy = [] ++ optional (container.autoStart) "multi-user.target";
     after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ]
@@ -253,23 +273,6 @@ let
       else if cfg.backend == "podman" then [ config.virtualisation.podman.package ]
       else throw "Unhandled backend: ${cfg.backend}";
 
-    preStart = ''
-      ${cfg.backend} rm -f ${name} || true
-      ${optionalString (isValidLogin container.login) ''
-        cat ${container.login.passwordFile} | \
-          ${cfg.backend} login \
-            ${container.login.registry} \
-            --username ${container.login.username} \
-            --password-stdin
-        ''}
-      ${optionalString (container.imageFile != null) ''
-        ${cfg.backend} load -i ${container.imageFile}
-        ''}
-      ${optionalString (cfg.backend == "podman") ''
-        rm -f /run/podman-${escapedName}.ctr-id
-        ''}
-      '';
-
     script = concatStringsSep " \\\n  " ([
       "exec ${cfg.backend} run"
       "--rm"
@@ -318,7 +321,7 @@ let
       ###
       # ExecReload = ...;
       ###
-
+      ExecStartPre = [ "${preStartScript}/bin/pre-start" ];
       TimeoutStartSec = 0;
       TimeoutStopSec = 120;
       Restart = "always";