about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/web-apps
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2021-07-23 09:26:00 +0000
committerAlyssa Ross <hi@alyssa.is>2021-07-23 09:26:00 +0000
commitab63e0bb8dcf2b1bf8d4a26ed360af777b8f241d (patch)
tree504b28a058661f6c1cbb7d3f580020e50367ca7f /nixpkgs/nixos/modules/services/web-apps
parent55cc63c079f49e81d695a25bc2f5b3902f2bd290 (diff)
parentb09661d41fb93562fd53f31574dbf781b130ac44 (diff)
downloadnixlib-ab63e0bb8dcf2b1bf8d4a26ed360af777b8f241d.tar
nixlib-ab63e0bb8dcf2b1bf8d4a26ed360af777b8f241d.tar.gz
nixlib-ab63e0bb8dcf2b1bf8d4a26ed360af777b8f241d.tar.bz2
nixlib-ab63e0bb8dcf2b1bf8d4a26ed360af777b8f241d.tar.lz
nixlib-ab63e0bb8dcf2b1bf8d4a26ed360af777b8f241d.tar.xz
nixlib-ab63e0bb8dcf2b1bf8d4a26ed360af777b8f241d.tar.zst
nixlib-ab63e0bb8dcf2b1bf8d4a26ed360af777b8f241d.zip
Merge commit 'b09661d41fb93562fd53f31574dbf781b130ac44'
Diffstat (limited to 'nixpkgs/nixos/modules/services/web-apps')
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/discourse.nix19
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/discourse.xml43
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix18
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/nextcloud.nix16
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/nextcloud.xml2
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/plausible.nix18
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/vikunja.nix145
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/wordpress.nix126
8 files changed, 335 insertions, 52 deletions
diff --git a/nixpkgs/nixos/modules/services/web-apps/discourse.nix b/nixpkgs/nixos/modules/services/web-apps/discourse.nix
index d3ae072f86a8..8d5302ba267b 100644
--- a/nixpkgs/nixos/modules/services/web-apps/discourse.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/discourse.nix
@@ -475,21 +475,16 @@ in
       plugins = lib.mkOption {
         type = lib.types.listOf lib.types.package;
         default = [];
-        example = ''
-          [
-            (pkgs.fetchFromGitHub {
-              owner = "discourse";
-              repo = "discourse-spoiler-alert";
-              rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
-              sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
-            })
+        example = lib.literalExample ''
+          with config.services.discourse.package.plugins; [
+            discourse-canned-replies
+            discourse-github
           ];
         '';
         description = ''
-          <productname>Discourse</productname> plugins to install as a
-          list of derivations. As long as a plugin supports the
-          standard install method, packaging it should only require
-          fetching its source with an appropriate fetcher.
+          Plugins to install as part of
+          <productname>Discourse</productname>, expressed as a list of
+          derivations.
         '';
       };
 
diff --git a/nixpkgs/nixos/modules/services/web-apps/discourse.xml b/nixpkgs/nixos/modules/services/web-apps/discourse.xml
index bae562423213..1d6866e7b352 100644
--- a/nixpkgs/nixos/modules/services/web-apps/discourse.xml
+++ b/nixpkgs/nixos/modules/services/web-apps/discourse.xml
@@ -262,9 +262,31 @@ services.discourse = {
     <para>
       You can install <productname>Discourse</productname> plugins
       using the <xref linkend="opt-services.discourse.plugins" />
-      option. As long as a plugin supports the standard install
-      method, packaging it should only require fetching its source
-      with an appropriate fetcher.
+      option. Pre-packaged plugins are provided in
+      <literal>&lt;your_discourse_package_here&gt;.plugins</literal>. If
+      you want the full suite of plugins provided through
+      <literal>nixpkgs</literal>, you can also set the <xref
+      linkend="opt-services.discourse.package" /> option to
+      <literal>pkgs.discourseAllPlugins</literal>.
+    </para>
+
+    <para>
+      Plugins can be built with the
+      <literal>&lt;your_discourse_package_here&gt;.mkDiscoursePlugin</literal>
+      function. Normally, it should suffice to provide a
+      <literal>name</literal> and <literal>src</literal> attribute. If
+      the plugin has Ruby dependencies, however, they need to be
+      packaged in accordance with the <link
+      xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
+      with Ruby</link> section of the Nixpkgs manual and the
+      appropriate gem options set in <literal>bundlerEnvArgs</literal>
+      (normally <literal>gemdir</literal> is sufficient). A plugin's
+      Ruby dependencies are listed in its
+      <filename>plugin.rb</filename> file as function calls to
+      <literal>gem</literal>. To construct the corresponding
+      <filename>Gemfile</filename>, run <command>bundle
+      init</command>, then add the <literal>gem</literal> lines to it
+      verbatim.
     </para>
 
     <para>
@@ -280,7 +302,10 @@ services.discourse = {
     <para>
       For example, to add the <link
       xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
-      plugin and disable it by default:
+      and <link
+      xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
+      plugins, and disable <literal>discourse-spoiler-alert</literal>
+      by default:
 
 <programlisting>
 services.discourse = {
@@ -301,13 +326,9 @@ services.discourse = {
     <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
   };
   <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = [
-    (pkgs.fetchFromGitHub {
-      owner = "discourse";
-      repo = "discourse-spoiler-alert";
-      rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
-      sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
-    })
+  <link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = with config.services.discourse.package.plugins; [
+    discourse-spoiler-alert
+    discourse-solved
   ];
   <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
     plugins = {
diff --git a/nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix b/nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
index eea49bda283b..f8f0854f1bcb 100644
--- a/nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
@@ -23,6 +23,16 @@ in {
       '';
     };
 
+    libraryPaths = mkOption {
+      type = attrsOf package;
+      default = { };
+      description = ''
+        Libraries to add to the Icingaweb2 library path.
+        The name of the attribute is the name of the library, the value
+        is the package to add.
+      '';
+    };
+
     virtualHost = mkOption {
       type = nullOr str;
       default = "icingaweb2";
@@ -167,6 +177,9 @@ in {
     services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
       ${poolName} = {
         user = "icingaweb2";
+        phpEnv = {
+          ICINGAWEB_LIBDIR = toString (pkgs.linkFarm "icingaweb2-libdir" (mapAttrsToList (name: path: { inherit name path; }) cfg.libraryPaths));
+        };
         phpPackage = pkgs.php.withExtensions ({ enabled, all }: [ all.imagick ] ++ enabled);
         phpOptions = ''
           date.timezone = "${cfg.timezone}"
@@ -184,6 +197,11 @@ in {
       };
     };
 
+    services.icingaweb2.libraryPaths = {
+      ipl = pkgs.icingaweb2-ipl;
+      thirdparty = pkgs.icingaweb2-thirdparty;
+    };
+
     systemd.services."phpfpm-${poolName}".serviceConfig.ReadWritePaths = [ "/etc/icingaweb2" ];
 
     services.nginx = {
diff --git a/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix b/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix
index 545deaa905f2..111b31734696 100644
--- a/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix
@@ -92,7 +92,7 @@ in {
     package = mkOption {
       type = types.package;
       description = "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud19" "nextcloud20" "nextcloud21" ];
+      relatedPackages = [ "nextcloud20" "nextcloud21" "nextcloud22" ];
     };
 
     maxUploadSize = mkOption {
@@ -385,7 +385,7 @@ in {
       ];
 
       warnings = let
-        latest = 21;
+        latest = 22;
         upgradeWarning = major: nixos:
           ''
             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
@@ -403,9 +403,9 @@ in {
           Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release.
           Please migrate your configuration to config.services.nextcloud.poolSettings.
         '')
-        ++ (optional (versionOlder cfg.package.version "19") (upgradeWarning 18 "20.09"))
         ++ (optional (versionOlder cfg.package.version "20") (upgradeWarning 19 "21.05"))
-        ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05"));
+        ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05"))
+        ++ (optional (versionOlder cfg.package.version "22") (upgradeWarning 21 "21.11"));
 
       services.nextcloud.package = with pkgs;
         mkDefault (
@@ -415,13 +415,13 @@ in {
               nextcloud defined in an overlay, please set `services.nextcloud.package` to
               `pkgs.nextcloud`.
             ''
-          else if versionOlder stateVersion "20.09" then nextcloud18
           # 21.03 will not be an official release - it was instead 21.05.
           # This versionOlder statement remains set to 21.03 for backwards compatibility.
           # See https://github.com/NixOS/nixpkgs/pull/108899 and
           # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
           else if versionOlder stateVersion "21.03" then nextcloud19
-          else nextcloud21
+          else if versionOlder stateVersion "21.11" then nextcloud21
+          else nextcloud22
         );
     }
 
@@ -616,9 +616,7 @@ in {
 
       services.nginx.enable = mkDefault true;
 
-      services.nginx.virtualHosts.${cfg.hostName} = let
-        major = toInt (versions.major cfg.package.version);
-      in {
+      services.nginx.virtualHosts.${cfg.hostName} = {
         root = cfg.package;
         locations = {
           "= /robots.txt" = {
diff --git a/nixpkgs/nixos/modules/services/web-apps/nextcloud.xml b/nixpkgs/nixos/modules/services/web-apps/nextcloud.xml
index 83a6f68edcbf..3af37b15dd56 100644
--- a/nixpkgs/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixpkgs/nixos/modules/services/web-apps/nextcloud.xml
@@ -11,7 +11,7 @@
   desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
  </para>
  <para>
-  The current default by NixOS is <package>nextcloud21</package> which is also the latest
+  The current default by NixOS is <package>nextcloud22</package> which is also the latest
   major version available.
  </para>
  <section xml:id="module-services-nextcloud-basic-usage">
diff --git a/nixpkgs/nixos/modules/services/web-apps/plausible.nix b/nixpkgs/nixos/modules/services/web-apps/plausible.nix
index caf5ba466dfe..b56848b79d21 100644
--- a/nixpkgs/nixos/modules/services/web-apps/plausible.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/plausible.nix
@@ -7,10 +7,15 @@ let
 
   # FIXME consider using LoadCredential as soon as it actually works.
   envSecrets = ''
-    export ADMIN_USER_PWD="$(<${cfg.adminUser.passwordFile})"
-    export SECRET_KEY_BASE="$(<${cfg.server.secretKeybaseFile})"
+    ADMIN_USER_PWD="$(<${cfg.adminUser.passwordFile})"
+    export ADMIN_USER_PWD # separate export to make `set -e` work
+
+    SECRET_KEY_BASE="$(<${cfg.server.secretKeybaseFile})"
+    export SECRET_KEY_BASE # separate export to make `set -e` work
+
     ${optionalString (cfg.mail.smtp.passwordFile != null) ''
-      export SMTP_USER_PWD="$(<${cfg.mail.smtp.passwordFile})"
+      SMTP_USER_PWD="$(<${cfg.mail.smtp.passwordFile})"
+      export SMTP_USER_PWD # separate export to make `set -e` work
     ''}
   '';
 in {
@@ -102,6 +107,11 @@ in {
         type = types.str;
         description = ''
           Public URL where plausible is available.
+
+          Note that <literal>/path</literal> components are currently ignored:
+          <link xlink:href="https://github.com/plausible/analytics/issues/1182">
+            https://github.com/plausible/analytics/issues/1182
+          </link>.
         '';
       };
     };
@@ -228,6 +238,7 @@ in {
             WorkingDirectory = "/var/lib/plausible";
             StateDirectory = "plausible";
             ExecStartPre = "@${pkgs.writeShellScript "plausible-setup" ''
+              set -eu -o pipefail
               ${envSecrets}
               ${pkgs.plausible}/createdb.sh
               ${pkgs.plausible}/migrate.sh
@@ -238,6 +249,7 @@ in {
               ''}
             ''} plausible-setup";
             ExecStart = "@${pkgs.writeShellScript "plausible" ''
+              set -eu -o pipefail
               ${envSecrets}
               plausible start
             ''} plausible";
diff --git a/nixpkgs/nixos/modules/services/web-apps/vikunja.nix b/nixpkgs/nixos/modules/services/web-apps/vikunja.nix
new file mode 100644
index 000000000000..b0b6eb6df17e
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/web-apps/vikunja.nix
@@ -0,0 +1,145 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.vikunja;
+  format = pkgs.formats.yaml {};
+  configFile = format.generate "config.yaml" cfg.settings;
+  useMysql = cfg.database.type == "mysql";
+  usePostgresql = cfg.database.type == "postgres";
+in {
+  options.services.vikunja = with lib; {
+    enable = mkEnableOption "vikunja service";
+    package-api = mkOption {
+      default = pkgs.vikunja-api;
+      type = types.package;
+      defaultText = "pkgs.vikunja-api";
+      description = "vikunja-api derivation to use.";
+    };
+    package-frontend = mkOption {
+      default = pkgs.vikunja-frontend;
+      type = types.package;
+      defaultText = "pkgs.vikunja-frontend";
+      description = "vikunja-frontend derivation to use.";
+    };
+    environmentFiles = mkOption {
+      type = types.listOf types.path;
+      default = [ ];
+      description = ''
+        List of environment files set in the vikunja systemd service.
+        For example passwords should be set in one of these files.
+      '';
+    };
+    setupNginx = mkOption {
+      type = types.bool;
+      default = config.services.nginx.enable;
+      defaultText = "config.services.nginx.enable";
+      description = ''
+        Whether to setup NGINX.
+        Further nginx configuration can be done by changing
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;</option>.
+        This does not enable TLS or ACME by default. To enable this, set the
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;.enableACME</option> to
+        <literal>true</literal> and if appropriate do the same for
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;.forceSSL</option>.
+      '';
+    };
+    frontendScheme = mkOption {
+      type = types.enum [ "http" "https" ];
+      description = ''
+        Whether the site is available via http or https.
+        This does not configure https or ACME in nginx!
+      '';
+    };
+    frontendHostname = mkOption {
+      type = types.str;
+      description = "The Hostname under which the frontend is running.";
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = {};
+      description = ''
+        Vikunja configuration. Refer to
+        <link xlink:href="https://vikunja.io/docs/config-options/"/>
+        for details on supported values.
+        '';
+    };
+    database = {
+      type = mkOption {
+        type = types.enum [ "sqlite" "mysql" "postgres" ];
+        example = "postgres";
+        default = "sqlite";
+        description = "Database engine to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Database host address. Can also be a socket.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = "vikunja";
+        description = "Database user.";
+      };
+      database = mkOption {
+        type = types.str;
+        default = "vikunja";
+        description = "Database name.";
+      };
+      path = mkOption {
+        type = types.str;
+        default = "/var/lib/vikunja/vikunja.db";
+        description = "Path to the sqlite3 database file.";
+      };
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    services.vikunja.settings = {
+      database = {
+        inherit (cfg.database) type host user database path;
+      };
+      service = {
+        frontendurl = "${cfg.frontendScheme}://${cfg.frontendHostname}/";
+      };
+      files = {
+        basepath = "/var/lib/vikunja/files";
+      };
+    };
+
+    systemd.services.vikunja-api = {
+      description = "vikunja-api";
+      after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.package-api ];
+      restartTriggers = [ configFile ];
+
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "vikunja";
+        ExecStart = "${cfg.package-api}/bin/vikunja";
+        Restart = "always";
+        EnvironmentFile = cfg.environmentFiles;
+      };
+    };
+
+    services.nginx.virtualHosts."${cfg.frontendHostname}" = mkIf cfg.setupNginx {
+      locations = {
+        "/" = {
+          root = cfg.package-frontend;
+          tryFiles = "try_files $uri $uri/ /";
+        };
+        "~* ^/(api|dav|\\.well-known)/" = {
+          proxyPass = "http://localhost:3456";
+          extraConfig = ''
+            client_max_body_size 20M;
+          '';
+        };
+      };
+    };
+
+    environment.etc."vikunja/config.yaml".source = configFile;
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/web-apps/wordpress.nix b/nixpkgs/nixos/modules/services/web-apps/wordpress.nix
index 775ecb3acaf0..6f1ef815bc46 100644
--- a/nixpkgs/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/wordpress.nix
@@ -3,13 +3,18 @@
 let
   inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
   inherit (lib) any attrValues concatMapStringsSep flatten literalExample;
-  inherit (lib) mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
+  inherit (lib) filterAttrs mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
 
-  eachSite = config.services.wordpress;
+  cfg = migrateOldAttrs config.services.wordpress;
+  eachSite = cfg.sites;
   user = "wordpress";
-  group = config.services.httpd.group;
+  webserver = config.services.${cfg.webserver};
   stateDir = hostName: "/var/lib/wordpress/${hostName}";
 
+  # Migrate config.services.wordpress.<hostName> to config.services.wordpress.sites.<hostName>
+  oldSites = filterAttrs (o: _: o != "sites" && o != "webserver");
+  migrateOldAttrs = cfg: cfg // { sites = cfg.sites // oldSites cfg; };
+
   pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
     pname = "wordpress-${hostName}";
     version = src.version;
@@ -261,21 +266,48 @@ in
   # interface
   options = {
     services.wordpress = mkOption {
-      type = types.attrsOf (types.submodule siteOpts);
+      type = types.submodule {
+        # Used to support old interface
+        freeformType = types.attrsOf (types.submodule siteOpts);
+
+        # New interface
+        options.sites = mkOption {
+          type = types.attrsOf (types.submodule siteOpts);
+          default = {};
+          description = "Specification of one or more WordPress sites to serve";
+        };
+
+        options.webserver = mkOption {
+          type = types.enum [ "httpd" "nginx" ];
+          default = "httpd";
+          description = ''
+            Whether to use apache2 or nginx for virtual host management.
+
+            Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
+            See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+
+            Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
+            See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+          '';
+        };
+      };
       default = {};
-      description = "Specification of one or more WordPress sites to serve via Apache.";
+      description = "Wordpress configuration";
     };
+
   };
 
   # implementation
-  config = mkIf (eachSite != {}) {
+  config = mkIf (eachSite != {}) (mkMerge [{
 
     assertions = mapAttrsToList (hostName: cfg:
       { assertion = cfg.database.createLocally -> cfg.database.user == user;
-        message = "services.wordpress.${hostName}.database.user must be ${user} if the database is to be automatically provisioned";
+        message = ''services.wordpress.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
       }
     ) eachSite;
 
+    warnings = mapAttrsToList (hostName: _: ''services.wordpress."${hostName}" is deprecated use services.wordpress.sites."${hostName}"'') (oldSites cfg);
+
     services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
       enable = true;
       package = mkDefault pkgs.mariadb;
@@ -289,14 +321,18 @@ in
 
     services.phpfpm.pools = mapAttrs' (hostName: cfg: (
       nameValuePair "wordpress-${hostName}" {
-        inherit user group;
+        inherit user;
+        group = webserver.group;
         settings = {
-          "listen.owner" = config.services.httpd.user;
-          "listen.group" = config.services.httpd.group;
+          "listen.owner" = webserver.user;
+          "listen.group" = webserver.group;
         } // cfg.poolConfig;
       }
     )) eachSite;
 
+  }
+
+  (mkIf (cfg.webserver == "httpd") {
     services.httpd = {
       enable = true;
       extraModules = [ "proxy_fcgi" ];
@@ -332,11 +368,13 @@ in
         '';
       } ]) eachSite;
     };
+  })
 
+  {
     systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
-      "d '${stateDir hostName}' 0750 ${user} ${group} - -"
-      "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
-      "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
+      "d '${stateDir hostName}' 0750 ${user} ${webserver.group} - -"
+      "d '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
+      "Z '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
     ]) eachSite);
 
     systemd.services = mkMerge [
@@ -350,7 +388,7 @@ in
           serviceConfig = {
             Type = "oneshot";
             User = user;
-            Group = group;
+            Group = webserver.group;
           };
       })) eachSite)
 
@@ -360,9 +398,65 @@ in
     ];
 
     users.users.${user} = {
-      group = group;
+      group = webserver.group;
       isSystemUser = true;
     };
+  }
 
-  };
+  (mkIf (cfg.webserver == "nginx") {
+    services.nginx = {
+      enable = true;
+      virtualHosts = mapAttrs (hostName: cfg: {
+        serverName = mkDefault hostName;
+        root = "${pkg hostName cfg}/share/wordpress";
+        extraConfig = ''
+          index index.php;
+        '';
+        locations = {
+          "/" = {
+            priority = 200;
+            extraConfig = ''
+              try_files $uri $uri/ /index.php$is_args$args;
+            '';
+          };
+          "~ \\.php$" = {
+            priority = 500;
+            extraConfig = ''
+              fastcgi_split_path_info ^(.+\.php)(/.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket};
+              fastcgi_index index.php;
+              include "${config.services.nginx.package}/conf/fastcgi.conf";
+              fastcgi_param PATH_INFO $fastcgi_path_info;
+              fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
+              # Mitigate https://httpoxy.org/ vulnerabilities
+              fastcgi_param HTTP_PROXY "";
+              fastcgi_intercept_errors off;
+              fastcgi_buffer_size 16k;
+              fastcgi_buffers 4 16k;
+              fastcgi_connect_timeout 300;
+              fastcgi_send_timeout 300;
+              fastcgi_read_timeout 300;
+            '';
+          };
+          "~ /\\." = {
+            priority = 800;
+            extraConfig = "deny all;";
+          };
+          "~* /(?:uploads|files)/.*\\.php$" = {
+            priority = 900;
+            extraConfig = "deny all;";
+          };
+          "~* \\.(js|css|png|jpg|jpeg|gif|ico)$" = {
+            priority = 1000;
+            extraConfig = ''
+              expires max;
+              log_not_found off;
+            '';
+          };
+        };
+      }) eachSite;
+    };
+  })
+
+  ]);
 }