diff --git a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
index e1317621418d..a65e29614c3f 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
@@ -99,6 +99,14 @@
services.ulogd.
+
+
+ photoprism,
+ a AI-Powered Photos App for the Decentralized Web. Available
+ as
+ services.photoprism.
+
+
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index 1620e98f3aa3..e520a08960e3 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -34,6 +34,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [ulogd](https://www.netfilter.org/projects/ulogd/index.html), a userspace logging daemon for netfilter/iptables related logging. Available as [services.ulogd](options.html#opt-services.ulogd.enable).
+- [photoprism](https://photoprism.app/), a AI-Powered Photos App for the Decentralized Web. Available as [services.photoprism](options.html#opt-services.photoprism.enable).
+
## Backward Incompatibilities {#sec-release-23.05-incompatibilities}
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index f0ee3fc93972..a28c5d0309c1 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1165,6 +1165,7 @@
./services/web-apps/peertube.nix
./services/web-apps/pgpkeyserver-lite.nix
./services/web-apps/phylactery.nix
+ ./services/web-apps/photoprism.nix
./services/web-apps/pict-rs.nix
./services/web-apps/plantuml-server.nix
./services/web-apps/plausible.nix
diff --git a/nixos/modules/services/web-apps/photoprism.nix b/nixos/modules/services/web-apps/photoprism.nix
new file mode 100644
index 000000000000..d5ca6014780a
--- /dev/null
+++ b/nixos/modules/services/web-apps/photoprism.nix
@@ -0,0 +1,155 @@
+{ config, pkgs, lib, ... }:
+let
+ cfg = config.services.photoprism;
+
+ env = {
+ PHOTOPRISM_ORIGINALS_PATH = cfg.originalsPath;
+ PHOTOPRISM_STORAGE_PATH = cfg.storagePath;
+ PHOTOPRISM_IMPORT_PATH = cfg.importPath;
+ PHOTOPRISM_HTTP_HOST = cfg.address;
+ PHOTOPRISM_HTTP_PORT = toString cfg.port;
+ } // (
+ lib.mapAttrs (_: toString) cfg.settings
+ );
+
+ manage =
+ let
+ setupEnv = lib.concatStringsSep "\n" (lib.mapAttrsToList (name: val: "export ${name}=${lib.escapeShellArg val}") env);
+ in
+ pkgs.writeShellScript "manage" ''
+ ${setupEnv}
+ exec ${cfg.package}/bin/photoprism "$@"
+ '';
+in
+{
+ meta.maintainers = with lib.maintainers; [ stunkymonkey ];
+
+ options.services.photoprism = {
+
+ enable = lib.mkEnableOption (lib.mdDoc "Photoprism web server");
+
+ passwordFile = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ description = lib.mdDoc ''
+ Admin password file.
+ '';
+ };
+
+ address = lib.mkOption {
+ type = lib.types.str;
+ default = "localhost";
+ description = lib.mdDoc ''
+ Web interface address.
+ '';
+ };
+
+ port = lib.mkOption {
+ type = lib.types.port;
+ default = 2342;
+ description = lib.mdDoc ''
+ Web interface port.
+ '';
+ };
+
+ originalsPath = lib.mkOption {
+ type = lib.types.path;
+ default = null;
+ example = "/data/photos";
+ description = lib.mdDoc ''
+ Storage path of your original media files (photos and videos).
+ '';
+ };
+
+ importPath = lib.mkOption {
+ type = lib.types.str;
+ default = "import";
+ description = lib.mdDoc ''
+ Relative or absolute to the `originalsPath` from where the files should be imported.
+ '';
+ };
+
+ storagePath = lib.mkOption {
+ type = lib.types.path;
+ default = "/var/lib/photoprism";
+ description = lib.mdDoc ''
+ Location for sidecar, cache, and database files.
+ '';
+ };
+
+ package = lib.mkPackageOptionMD pkgs "photoprism" { };
+
+ settings = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ description = lib.mdDoc ''
+ See [the getting-started guide](https://docs.photoprism.app/getting-started/config-options/) for available options.
+ '';
+ example = {
+ PHOTOPRISM_DEFAULT_LOCALE = "de";
+ PHOTOPRISM_ADMIN_USER = "root";
+ };
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ systemd.services.photoprism = {
+ description = "Photoprism server";
+
+ serviceConfig = {
+ Restart = "on-failure";
+ User = "photoprism";
+ Group = "photoprism";
+ DynamicUser = true;
+ StateDirectory = "photoprism";
+ WorkingDirectory = "/var/lib/photoprism";
+ RuntimeDirectory = "photoprism";
+
+ LoadCredential = lib.optionalString (cfg.passwordFile != null)
+ "PHOTOPRISM_ADMIN_PASSWORD:${cfg.passwordFile}";
+
+ CapabilityBoundingSet = "";
+ LockPersonality = true;
+ PrivateDevices = true;
+ PrivateUsers = true;
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
+ UMask = "0066";
+ } // lib.optionalAttrs (cfg.port < 1024) {
+ AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+ CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+ };
+
+ wantedBy = [ "multi-user.target" ];
+ environment = env;
+
+ # reminder: easier password configuration will come in https://github.com/photoprism/photoprism/pull/2302
+ preStart = ''
+ ln -sf ${manage} photoprism-manage
+
+ ${lib.optionalString (cfg.passwordFile != null) ''
+ export PHOTOPRISM_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PHOTOPRISM_ADMIN_PASSWORD")
+ ''}
+ exec ${cfg.package}/bin/photoprism migrations run -f
+ '';
+
+ script = ''
+ ${lib.optionalString (cfg.passwordFile != null) ''
+ export PHOTOPRISM_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PHOTOPRISM_ADMIN_PASSWORD")
+ ''}
+ exec ${cfg.package}/bin/photoprism start
+ '';
+ };
+ };
+}
+
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index acc42acf37a6..e6c786744d9d 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -513,6 +513,7 @@ in {
pgjwt = handleTest ./pgjwt.nix {};
pgmanage = handleTest ./pgmanage.nix {};
phosh = handleTest ./phosh.nix {};
+ photoprism = handleTest ./photoprism.nix {};
php = handleTest ./php {};
php80 = handleTest ./php { php = pkgs.php80; };
php81 = handleTest ./php { php = pkgs.php81; };
diff --git a/nixos/tests/photoprism.nix b/nixos/tests/photoprism.nix
new file mode 100644
index 000000000000..a77ab59f5c9a
--- /dev/null
+++ b/nixos/tests/photoprism.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+ name = "photoprism";
+ meta.maintainers = with lib.maintainers; [ stunkymonkey ];
+
+ nodes.machine = { pkgs, ... }: {
+ services.photoprism = {
+ enable = true;
+ port = 8080;
+ originalsPath = "/media/photos/";
+ passwordFile = pkgs.writeText "password" "secret";
+ };
+ environment.extraInit = ''
+ mkdir -p /media/photos
+ '';
+ };
+
+ testScript = ''
+ machine.wait_for_unit("multi-user.target")
+ machine.wait_for_open_port(8080)
+ response = machine.succeed("curl -vvv -s -H 'Host: photoprism' http://127.0.0.1:8080/library/login")
+ assert 'PhotoPrism' in response, "Login page didn't load successfully"
+ '';
+})