diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 4a392b6f5c9..8ae3c7dca00 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -432,6 +432,7 @@
./services/misc/uhub.nix
./services/misc/weechat.nix
./services/misc/xmr-stak.nix
+ ./services/misc/zoneminder.nix
./services/misc/zookeeper.nix
./services/monitoring/alerta.nix
./services/monitoring/apcupsd.nix
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
new file mode 100644
index 00000000000..a40e9e84613
--- /dev/null
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -0,0 +1,362 @@
+{ config, lib, pkgs, ... }:
+
+let
+ cfg = config.services.zoneminder;
+ pkg = pkgs.zoneminder;
+
+ dirName = pkg.dirName;
+
+ user = "zoneminder";
+ group = {
+ nginx = config.services.nginx.group;
+ none = user;
+ }."${cfg.webserver}";
+
+ useNginx = cfg.webserver == "nginx";
+
+ defaultDir = "/var/lib/${user}";
+ home = if useCustomDir then cfg.storageDir else defaultDir;
+
+ useCustomDir = !(builtins.isNull cfg.storageDir);
+
+ socket = "/run/phpfpm/${dirName}.sock";
+
+ zms = "/cgi-bin/zms";
+
+ dirs = dirList: [ dirName ] ++ map (e: "${dirName}/${e}") dirList;
+
+ cacheDirs = [ "swap" ];
+ libDirs = [ "events" "exports" "images" "sounds" ];
+
+ dirStanzas = baseDir:
+ lib.concatStringsSep "\n" (map (e:
+ "ZM_DIR_${lib.toUpper e}=${baseDir}/${e}"
+ ) libDirs);
+
+ defaultsFile = pkgs.writeText "60-defaults.conf" ''
+ # 01-system-paths.conf
+ ${dirStanzas home}
+ ZM_PATH_ARP=${lib.getBin pkgs.nettools}/bin/arp
+ ZM_PATH_LOGS=/var/log/${dirName}
+ ZM_PATH_MAP=/dev/shm
+ ZM_PATH_SOCKS=/run/${dirName}
+ ZM_PATH_SWAP=/var/cache/${dirName}/swap
+ ZM_PATH_ZMS=${zms}
+
+ # 02-multiserver.conf
+ ZM_SERVER_HOST=
+
+ # Database
+ ZM_DB_TYPE=mysql
+ ZM_DB_HOST=${cfg.database.host}
+ ZM_DB_NAME=${cfg.database.name}
+ ZM_DB_USER=${cfg.database.username}
+ ZM_DB_PASS=${cfg.database.password}
+
+ # Web
+ ZM_WEB_USER=${user}
+ ZM_WEB_GROUP=${group}
+ '';
+
+ configFile = pkgs.writeText "80-nixos.conf" ''
+ # You can override defaults here
+
+ ${cfg.extraConfig}
+ '';
+
+ phpExtensions = with pkgs.phpPackages; [
+ { pkg = apcu; name = "apcu"; }
+ ];
+
+in {
+ options = {
+ services.zoneminder = with lib; {
+ enable = lib.mkEnableOption ''
+ ZoneMinder
+
+ If you intend to run the database locally, you should set
+ `config.services.zoneminder.database.createLocally` to true. Otherwise,
+ when set to `false` (the default), you will have to create the database
+ and database user as well as populate the database yourself.
+ '';
+
+ webserver = mkOption {
+ type = types.enum [ "nginx" "none" ];
+ default = "nginx";
+ description = ''
+ The webserver to configure for the PHP frontend.
+
+
+
+ Set it to `none` if you want to configure it yourself. PRs are welcome
+ for support for other web servers.
+ '';
+ };
+
+ hostname = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = ''
+ The hostname on which to listen.
+ '';
+ };
+
+ port = mkOption {
+ type = types.int;
+ default = 8095;
+ description = ''
+ The port on which to listen.
+ '';
+ };
+
+ openFirewall = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Open the firewall port(s).
+ '';
+ };
+
+ database = {
+ createLocally = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Create the database and database user locally.
+ '';
+ };
+
+ host = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = ''
+ Hostname hosting the database.
+ '';
+ };
+
+ name = mkOption {
+ type = types.str;
+ default = "zm";
+ description = ''
+ Name of database.
+ '';
+ };
+
+ username = mkOption {
+ type = types.str;
+ default = "zmuser";
+ description = ''
+ Username for accessing the database.
+ '';
+ };
+
+ password = mkOption {
+ type = types.str;
+ default = "zmpass";
+ description = ''
+ Username for accessing the database.
+ '';
+ };
+ };
+
+ cameras = mkOption {
+ type = types.int;
+ default = 1;
+ description = ''
+ Set this to the number of cameras you expect to support.
+ '';
+ };
+
+ storageDir = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "/storage/tank";
+ description = ''
+ ZoneMinder can generate quite a lot of data, so in case you don't want
+ to use the default ${home}, you can override the path here.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Additional configuration added verbatim to the configuration file.
+ '';
+ };
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+
+ environment.etc = {
+ "zoneminder/60-defaults.conf".source = defaultsFile;
+ "zoneminder/80-nixos.conf".source = configFile;
+ };
+
+ networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
+
+ services = {
+ fcgiwrap = lib.mkIf useNginx {
+ enable = true;
+ preforkProcesses = cfg.cameras;
+ inherit user group;
+ };
+
+ mysql = lib.mkIf cfg.database.createLocally {
+ ensureDatabases = [ cfg.database.name ];
+ ensureUsers = {
+ name = cfg.database.username;
+ ensurePermissions = [
+ { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }
+ ];
+ initialDatabases = [
+ { inherit (cfg.database) name; schema = "${pkg}/share/zoneminder/db/zm_create.sql"; }
+ ];
+ };
+ };
+
+ nginx = lib.mkIf useNginx {
+ enable = true;
+ virtualHosts = {
+ "${cfg.hostname}" = {
+ default = true;
+ root = "${pkg}/share/zoneminder/www";
+ listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ];
+ extraConfig = let
+ fcgi = config.services.fcgiwrap;
+ in ''
+ index index.php;
+
+ location / {
+ try_files $uri $uri/ /index.php?$args =404;
+
+ location ~ /api/(css|img|ico) {
+ rewrite ^/api(.+)$ /api/app/webroot/$1 break;
+ try_files $uri $uri/ =404;
+ }
+
+ location ~ \.(gif|ico|jpg|jpeg|png)$ {
+ access_log off;
+ expires 30d;
+ }
+
+ location /api {
+ rewrite ^/api(.+)$ /api/app/webroot/index.php?p=$1 last;
+ }
+
+ location /cgi-bin {
+ gzip off;
+
+ include ${pkgs.nginx}/conf/fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME ${pkg}/libexec/zoneminder/${zms};
+ fastcgi_param HTTP_PROXY "";
+ fastcgi_intercept_errors on;
+
+ fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress};
+ }
+
+ location /cache {
+ alias /var/cache/${dirName};
+ }
+
+ location ~ \.php$ {
+ try_files $uri =404;
+ fastcgi_index index.php;
+
+ include ${pkgs.nginx}/conf/fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME $request_filename;
+ fastcgi_param HTTP_PROXY "";
+
+ fastcgi_pass unix:${socket};
+ }
+ }
+ '';
+ };
+ };
+ };
+
+ phpfpm = lib.mkIf useNginx {
+ phpOptions = ''
+ date.timezone = "${config.time.timeZone}"
+
+ ${lib.concatStringsSep "\n" (map (e:
+ "extension=${e.pkg}/lib/php/extensions/${e.name}.so") phpExtensions)}
+ '';
+ pools.zoneminder = {
+ listen = socket;
+ extraConfig = ''
+ user = ${user}
+ group = ${group}
+
+ listen.owner = ${user}
+ listen.group = ${group}
+ listen.mode = 0660
+
+ pm = dynamic
+ pm.start_servers = 1
+ pm.min_spare_servers = 1
+ pm.max_spare_servers = 2
+ pm.max_requests = 500
+ pm.max_children = 5
+ pm.status_path = /$pool-status
+ ping.path = /$pool-ping
+ '';
+ };
+ };
+ };
+
+ systemd.services = {
+ zoneminder = with pkgs; rec {
+ inherit (zoneminder.meta) description;
+ documentation = [ "https://zoneminder.readthedocs.org/en/latest/" ];
+ path = [
+ coreutils
+ procps
+ psmisc
+ ];
+ after = [ "mysql.service" "nginx.service" ];
+ wantedBy = [ "multi-user.target" ];
+ restartTriggers = [ defaultsFile configFile ];
+ preStart = lib.mkIf useCustomDir ''
+ install -dm775 -o ${user} -g ${group} ${cfg.storageDir}/{${lib.concatStringsSep "," libDirs}}
+ '';
+ serviceConfig = {
+ User = user;
+ Group = group;
+ SupplementaryGroups = [ "video" ];
+ ExecStart = "${zoneminder}/bin/zmpkg.pl start";
+ ExecStop = "${zoneminder}/bin/zmpkg.pl stop";
+ ExecReload = "${zoneminder}/bin/zmpkg.pl restart";
+ PIDFile = "/run/${dirName}/zm.pid";
+ Type = "forking";
+ Restart = "on-failure";
+ RestartSec = "10s";
+ CacheDirectory = dirs cacheDirs;
+ RuntimeDirectory = dirName;
+ ReadWriteDirectories = lib.mkIf useCustomDir [ cfg.storageDir ];
+ StateDirectory = dirs (if useCustomDir then [] else libDirs);
+ LogsDirectory = dirName;
+ PrivateTmp = true;
+ ProtectSystem = "strict";
+ ProtectKernelTunables = true;
+ SystemCallArchitectures = "native";
+ NoNewPrivileges = true;
+ };
+ };
+ };
+
+ users.groups."${user}" = {
+ gid = config.ids.gids.zoneminder;
+ };
+
+ users.users."${user}" = {
+ uid = config.ids.uids.zoneminder;
+ group = user;
+ inherit home;
+ inherit (pkgs.zoneminder.meta) description;
+ };
+ };
+
+ meta.maintainers = with lib.maintainers; [ peterhoeg ];
+}