summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix371
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/wordpress.nix285
-rw-r--r--nixos/tests/wordpress.nix65
4 files changed, 399 insertions, 323 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index cbd57fad609..c4ee28a9593 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -779,6 +779,7 @@
   ./services/web-apps/tt-rss.nix
   ./services/web-apps/selfoss.nix
   ./services/web-apps/virtlyst.nix
+  ./services/web-apps/wordpress.nix
   ./services/web-apps/youtrack.nix
   ./services/web-servers/apache-httpd/default.nix
   ./services/web-servers/caddy.nix
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
new file mode 100644
index 00000000000..624b0089a03
--- /dev/null
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -0,0 +1,371 @@
+{ config, pkgs, lib, ... }:
+
+let
+  inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
+  inherit (lib) any attrValues concatMapStringsSep flatten literalExample;
+  inherit (lib) mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
+
+  eachSite = config.services.wordpress;
+  user = "wordpress";
+  group = config.services.httpd.group;
+  stateDir = hostName: "/var/lib/wordpress/${hostName}";
+
+  pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
+    pname = "wordpress-${hostName}";
+    version = src.version;
+    src = cfg.package;
+
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      # symlink the wordpress config
+      ln -s ${wpConfig hostName cfg} $out/share/wordpress/wp-config.php
+      # symlink uploads directory
+      ln -s ${cfg.uploadsDir} $out/share/wordpress/wp-content/uploads
+
+      # https://github.com/NixOS/nixpkgs/pull/53399
+      #
+      # Symlinking works for most plugins and themes, but Avada, for instance, fails to
+      # understand the symlink, causing its file path stripping to fail. This results in
+      # requests that look like: https://example.com/wp-content//nix/store/...plugin/path/some-file.js
+      # Since hard linking directories is not allowed, copying is the next best thing.
+
+      # copy additional plugin(s) and theme(s)
+      ${concatMapStringsSep "\n" (theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${theme.name}") cfg.themes}
+      ${concatMapStringsSep "\n" (plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${plugin.name}") cfg.plugins}
+    '';
+  };
+
+  wpConfig = hostName: cfg: pkgs.writeText "wp-config-${hostName}.php" ''
+    <?php
+      define('DB_NAME', '${cfg.database.name}');
+      define('DB_HOST', '${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}');
+      define('DB_USER', '${cfg.database.user}');
+      ${optionalString (cfg.database.passwordFile != null) "define('DB_PASSWORD', file_get_contents('${cfg.database.passwordFile}'));"}
+      define('DB_CHARSET', 'utf8');
+      $table_prefix  = '${cfg.database.tablePrefix}';
+
+      require_once('${stateDir hostName}/secret-keys.php');
+
+      # wordpress is installed onto a read-only file system
+      define('DISALLOW_FILE_EDIT', true);
+      define('AUTOMATIC_UPDATER_DISABLED', true);
+
+      ${cfg.extraConfig}
+
+      if ( !defined('ABSPATH') )
+        define('ABSPATH', dirname(__FILE__) . '/');
+
+      require_once(ABSPATH . 'wp-settings.php');
+    ?>
+  '';
+
+  siteOpts = { lib, name, ... }:
+    {
+      options = {
+        package = mkOption {
+          type = types.package;
+          default = pkgs.wordpress;
+          description = "Which WordPress package to use.";
+        };
+
+        uploadsDir = mkOption {
+          type = types.path;
+          default = "/var/lib/wordpress/${name}/uploads";
+          description = ''
+            This directory is used for uploads of pictures. The directory passed here is automatically
+            created and permissions adjusted as required.
+          '';
+        };
+
+        plugins = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+            List of path(s) to respective plugin(s) which are copied from the 'plugins' directory.
+            <note><para>These plugins need to be packaged before use, see example.</para></note>
+          '';
+          example = ''
+            # Wordpress plugin 'embed-pdf-viewer' installation example
+            embedPdfViewerPlugin = pkgs.stdenv.mkDerivation {
+              name = "embed-pdf-viewer-plugin";
+              # Download the theme from the wordpress site
+              src = pkgs.fetchurl {
+                url = https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip;
+                sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd";
+              };
+              # We need unzip to build this package
+              buildInputs = [ pkgs.unzip ];
+              # Installing simply means copying all files to the output directory
+              installPhase = "mkdir -p $out; cp -R * $out/";
+            };
+
+            And then pass this theme to the themes list like this:
+              plugins = [ embedPdfViewerPlugin ];
+          '';
+        };
+
+        themes = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+            List of path(s) to respective theme(s) which are copied from the 'theme' directory.
+            <note><para>These themes need to be packaged before use, see example.</para></note>
+          '';
+          example = ''
+            # For shits and giggles, let's package the responsive theme
+            responsiveTheme = pkgs.stdenv.mkDerivation {
+              name = "responsive-theme";
+              # Download the theme from the wordpress site
+              src = pkgs.fetchurl {
+                url = https://downloads.wordpress.org/theme/responsive.3.14.zip;
+                sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3";
+              };
+              # We need unzip to build this package
+              buildInputs = [ pkgs.unzip ];
+              # Installing simply means copying all files to the output directory
+              installPhase = "mkdir -p $out; cp -R * $out/";
+            };
+
+            And then pass this theme to the themes list like this:
+              themes = [ responsiveTheme ];
+          '';
+        };
+
+        database = rec {
+          host = mkOption {
+            type = types.str;
+            default = "localhost";
+            description = "Database host address.";
+          };
+
+          port = mkOption {
+            type = types.port;
+            default = 3306;
+            description = "Database host port.";
+          };
+
+          name = mkOption {
+            type = types.str;
+            default = "wordpress";
+            description = "Database name.";
+          };
+
+          user = mkOption {
+            type = types.str;
+            default = "wordpress";
+            description = "Database user.";
+          };
+
+          passwordFile = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            example = "/run/keys/wordpress-dbpassword";
+            description = ''
+              A file containing the password corresponding to
+              <option>database.user</option>.
+            '';
+          };
+
+          tablePrefix = mkOption {
+            type = types.str;
+            default = "wp_";
+            description = ''
+              The $table_prefix is the value placed in the front of your database tables.
+              Change the value if you want to use something other than wp_ for your database
+              prefix. Typically this is changed if you are installing multiple WordPress blogs
+              in the same database.
+
+              See <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php#table_prefix'/>.
+            '';
+          };
+
+          socket = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            defaultText = "/run/mysqld/mysqld.sock";
+            description = "Path to the unix socket file to use for authentication.";
+          };
+
+          createLocally = mkOption {
+            type = types.bool;
+            default = true;
+            description = "Create the database and database user locally.";
+          };
+        };
+
+        virtualHost = mkOption {
+          type = types.submodule ({
+            options = import ../web-servers/apache-httpd/per-server-options.nix {
+              inherit lib;
+              forMainServer = false;
+            };
+          });
+          example = literalExample ''
+            {
+              enableSSL = true;
+              adminAddr = "webmaster@example.org";
+              sslServerCert = "/var/lib/acme/wordpress.example.org/full.pem";
+              sslServerKey = "/var/lib/acme/wordpress.example.org/key.pem";
+            }
+          '';
+          description = ''
+            Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
+          '';
+        };
+
+        poolConfig = mkOption {
+          type = types.lines;
+          default = ''
+            pm = dynamic
+            pm.max_children = 32
+            pm.start_servers = 2
+            pm.min_spare_servers = 2
+            pm.max_spare_servers = 4
+            pm.max_requests = 500
+          '';
+          description = ''
+            Options for the WordPress PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+            for details on configuration directives.
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = ''
+            Any additional text to be appended to the wp-config.php
+            configuration file. This is a PHP script. For configuration
+            settings, see <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php'/>.
+          '';
+          example = ''
+            define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds
+          '';
+        };
+      };
+
+      config.virtualHost.hostName = mkDefault name;
+    };
+in
+{
+  # interface
+  options = {
+    services.wordpress = mkOption {
+      type = types.attrsOf (types.submodule siteOpts);
+      default = {};
+      description = "Specification of one or more WordPress sites to serve via Apache.";
+    };
+  };
+
+  # implementation
+  config = mkIf (eachSite != {}) {
+
+    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";
+      }
+    ) eachSite;
+
+    services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
+      ensureUsers = mapAttrsToList (hostName: cfg:
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ) eachSite;
+    };
+
+    services.phpfpm.pools = mapAttrs' (hostName: cfg: (
+      nameValuePair "wordpress-${hostName}" {
+        listen = "/run/phpfpm/wordpress-${hostName}.sock";
+        extraConfig = ''
+          listen.owner = ${config.services.httpd.user}
+          listen.group = ${config.services.httpd.group}
+          user = ${user}
+          group = ${group}
+
+          ${cfg.poolConfig}
+        '';
+      }
+    )) eachSite;
+
+    services.httpd = {
+      enable = true;
+      extraModules = [ "proxy_fcgi" ];
+      virtualHosts = mapAttrsToList (hostName: cfg:
+        (mkMerge [
+          cfg.virtualHost {
+            documentRoot = mkForce "${pkg hostName cfg}/share/wordpress";
+            extraConfig = ''
+              <Directory "${pkg hostName cfg}/share/wordpress">
+                <FilesMatch "\.php$">
+                  <If "-f %{REQUEST_FILENAME}">
+                    SetHandler "proxy:unix:/run/phpfpm/wordpress-${hostName}.sock|fcgi://localhost/"
+                  </If>
+                </FilesMatch>
+
+                # standard wordpress .htaccess contents
+                <IfModule mod_rewrite.c>
+                  RewriteEngine On
+                  RewriteBase /
+                  RewriteRule ^index\.php$ - [L]
+                  RewriteCond %{REQUEST_FILENAME} !-f
+                  RewriteCond %{REQUEST_FILENAME} !-d
+                  RewriteRule . /index.php [L]
+                </IfModule>
+
+                DirectoryIndex index.php
+                Require all granted
+                Options +FollowSymLinks
+              </Directory>
+
+              # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php
+              <Files wp-config.php>
+                Require all denied
+              </Files>
+            '';
+          }
+        ])
+      ) 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} - -"
+    ]) eachSite);
+
+    systemd.services = mkMerge [
+      (mapAttrs' (hostName: cfg: (
+        nameValuePair "wordpress-init-${hostName}" {
+          wantedBy = [ "multi-user.target" ];
+          before = [ "phpfpm-wordpress-${hostName}.service" ];
+          after = optional cfg.database.createLocally "mysql.service";
+          script = ''
+            if ! test -e "${stateDir hostName}/secret-keys.php"; then
+              echo "<?php" >> "${stateDir hostName}/secret-keys.php"
+              ${pkgs.curl}/bin/curl -s https://api.wordpress.org/secret-key/1.1/salt/ >> "${stateDir hostName}/secret-keys.php"
+              echo "?>" >> "${stateDir hostName}/secret-keys.php"
+              chmod 440 "${stateDir hostName}/secret-keys.php"
+            fi
+          '';
+
+          serviceConfig = {
+            Type = "oneshot";
+            User = user;
+            Group = group;
+          };
+      })) eachSite)
+
+      (optionalAttrs (any (v: v.database.createLocally) (attrValues eachSite)) {
+        httpd.after = [ "mysql.service" ];
+      })
+    ];
+
+    users.users.${user}.group = group;
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/apache-httpd/wordpress.nix b/nixos/modules/services/web-servers/apache-httpd/wordpress.nix
deleted file mode 100644
index 3dddda138fe..00000000000
--- a/nixos/modules/services/web-servers/apache-httpd/wordpress.nix
+++ /dev/null
@@ -1,285 +0,0 @@
-{ config, lib, pkgs, serverInfo, ... }:
-# http://codex.wordpress.org/Hardening_WordPress
-
-with lib;
-
-let
-  # Our bare-bones wp-config.php file using the above settings
-  wordpressConfig = pkgs.writeText "wp-config.php" ''
-    <?php
-    define('DB_NAME',     '${config.dbName}');
-    define('DB_USER',     '${config.dbUser}');
-    define('DB_PASSWORD', file_get_contents('${config.dbPasswordFile}'));
-    define('DB_HOST',     '${config.dbHost}');
-    define('DB_CHARSET',  'utf8');
-    $table_prefix  = '${config.tablePrefix}';
-    define('AUTOMATIC_UPDATER_DISABLED', true);
-    ${config.extraConfig}
-    if ( !defined('ABSPATH') )
-    	define('ABSPATH', dirname(__FILE__) . '/');
-    require_once(ABSPATH . 'wp-settings.php');
-  '';
-
-  # .htaccess to support pretty URLs
-  htaccess = pkgs.writeText "htaccess" ''
-    <IfModule mod_rewrite.c>
-    RewriteEngine On
-    RewriteBase /
-    RewriteRule ^index\.php$ - [L]
-
-    # add a trailing slash to /wp-admin
-    RewriteRule ^wp-admin$ wp-admin/ [R=301,L]
-
-    RewriteCond %{REQUEST_FILENAME} -f [OR]
-    RewriteCond %{REQUEST_FILENAME} -d
-    RewriteRule ^ - [L]
-    RewriteRule ^(wp-(content|admin|includes).*) $1 [L]
-    RewriteRule ^(.*\.php)$ $1 [L]
-    RewriteRule . index.php [L]
-    </IfModule>
-
-    ${config.extraHtaccess}
-  '';
-
-  # WP translation can be found here:
-  #   https://github.com/nixcloud/wordpress-translations
-  supportedLanguages = {
-    en_GB = { revision="d6c005372a5318fd758b710b77a800c86518be13"; sha256="0qbbsi87k47q4rgczxx541xz4z4f4fr49hw4lnaxkdsf5maz8p9p"; };
-    de_DE = { revision="3c62955c27baaae98fd99feb35593d46562f4736"; sha256="1shndgd11dk836dakrjlg2arwv08vqx6j4xjh4jshvwmjab6ng6p"; };
-    zh_ZN = { revision="12b9f811e8cae4b6ee41de343d35deb0a8fdda6d"; sha256="1339ggsxh0g6lab37jmfxicsax4h702rc3fsvv5azs7mcznvwh47"; };
-    fr_FR = { revision="688c8b1543e3d38d9e8f57e0a6f2a2c3c8b588bd"; sha256="1j41iak0i6k7a4wzyav0yrllkdjjskvs45w53db8vfm8phq1n014"; };
-  };
-
-  downloadLanguagePack = language: revision: sha256s:
-    pkgs.stdenv.mkDerivation rec {
-      name = "wp_${language}";
-      src = pkgs.fetchFromGitHub {
-        owner = "nixcloud";
-        repo = "wordpress-translations";
-        rev = revision;
-        sha256 = sha256s;
-      };
-      installPhase = "mkdir -p $out; cp -R * $out/";
-    };
-
-  selectedLanguages = map (lang: downloadLanguagePack lang supportedLanguages.${lang}.revision supportedLanguages.${lang}.sha256) (config.languages);
-
-  # The wordpress package itself
-  wordpressRoot = pkgs.stdenv.mkDerivation rec {
-    name = "wordpress";
-    src = config.package;
-    installPhase = ''
-      mkdir -p $out
-      # copy all the wordpress files we downloaded
-      cp -R * $out/
-
-      # symlink the wordpress config
-      ln -s ${wordpressConfig} $out/wp-config.php
-      # symlink custom .htaccess
-      ln -s ${htaccess} $out/.htaccess
-      # symlink uploads directory
-      ln -s ${config.wordpressUploads} $out/wp-content/uploads
-
-      # remove bundled plugins(s) coming with wordpress
-      rm -Rf $out/wp-content/plugins/*
-      # remove bundled themes(s) coming with wordpress
-      rm -Rf $out/wp-content/themes/*
-
-      # copy additional theme(s)
-      ${concatMapStrings (theme: "cp -r ${theme} $out/wp-content/themes/${theme.name}\n") config.themes}
-      # copy additional plugin(s)
-      ${concatMapStrings (plugin: "cp -r ${plugin} $out/wp-content/plugins/${plugin.name}\n") (config.plugins) }
-
-      # symlink additional translation(s)
-      mkdir -p $out/wp-content/languages
-      ${concatMapStrings (language: "ln -s ${language}/*.mo ${language}/*.po $out/wp-content/languages/\n") (selectedLanguages) }
-    '';
-  };
-
-in
-
-{
-
-  # And some httpd extraConfig to make things work nicely
-  extraConfig = ''
-    <Directory ${wordpressRoot}>
-      DirectoryIndex index.php
-      Allow from *
-      Options FollowSymLinks
-      AllowOverride All
-    </Directory>
-  '';
-
-  enablePHP = true;
-
-  options = {
-    package = mkOption {
-      type = types.path;
-      default = pkgs.wordpress;
-      description = ''
-        Path to the wordpress sources.
-        Upgrading? We have a test! nix-build ./nixos/tests/wordpress.nix
-      '';
-    };
-    dbHost = mkOption {
-      default = "localhost";
-      description = "The location of the database server.";
-      example = "localhost";
-    };
-    dbName = mkOption {
-      default = "wordpress";
-      description = "Name of the database that holds the Wordpress data.";
-      example = "localhost";
-    };
-    dbUser = mkOption {
-      default = "wordpress";
-      description = "The dbUser, read: the username, for the database.";
-      example = "wordpress";
-    };
-    dbPassword = mkOption {
-      default = "wordpress";
-      description = ''
-        The mysql password to the respective dbUser.
-
-        Warning: this password is stored in the world-readable Nix store. It's
-        recommended to use the $dbPasswordFile option since that gives you control over
-        the security of the password. $dbPasswordFile also takes precedence over $dbPassword.
-      '';
-      example = "wordpress";
-    };
-    dbPasswordFile = mkOption {
-      type = types.str;
-      default = toString (pkgs.writeTextFile {
-        name = "wordpress-dbpassword";
-        text = config.dbPassword;
-      });
-      example = "/run/keys/wordpress-dbpassword";
-      description = ''
-        Path to a file that contains the mysql password to the respective dbUser.
-        The file should be readable by the user: config.services.httpd.user.
-
-        $dbPasswordFile takes precedence over the $dbPassword option.
-
-        This defaults to a file in the world-readable Nix store that contains the value
-        of the $dbPassword option. It's recommended to override this with a path not in
-        the Nix store. Tip: use nixops key management:
-        <link xlink:href='https://nixos.org/nixops/manual/#idm140737318306400'/>
-      '';
-    };
-    tablePrefix = mkOption {
-      default = "wp_";
-      description = ''
-        The $table_prefix is the value placed in the front of your database tables. Change the value if you want to use something other than wp_ for your database prefix. Typically this is changed if you are installing multiple WordPress blogs in the same database. See <link xlink:href='http://codex.wordpress.org/Editing_wp-config.php#table_prefix'/>.
-      '';
-    };
-    wordpressUploads = mkOption {
-    default = "/data/uploads";
-      description = ''
-        This directory is used for uploads of pictures and must be accessible (read: owned) by the httpd running user. The directory passed here is automatically created and permissions are given to the httpd running user.
-      '';
-    };
-    plugins = mkOption {
-      default = [];
-      type = types.listOf types.path;
-      description =
-        ''
-          List of path(s) to respective plugin(s) which are symlinked from the 'plugins' directory. Note: These plugins need to be packaged before use, see example.
-        '';
-      example = ''
-        # Wordpress plugin 'akismet' installation example
-        akismetPlugin = pkgs.stdenv.mkDerivation {
-          name = "akismet-plugin";
-          # Download the theme from the wordpress site
-          src = pkgs.fetchurl {
-            url = https://downloads.wordpress.org/plugin/akismet.3.1.zip;
-            sha256 = "1i4k7qyzna08822ncaz5l00wwxkwcdg4j9h3z2g0ay23q640pclg";
-          };
-          # We need unzip to build this package
-          buildInputs = [ pkgs.unzip ];
-          # Installing simply means copying all files to the output directory
-          installPhase = "mkdir -p $out; cp -R * $out/";
-        };
-
-        And then pass this theme to the themes list like this:
-          plugins = [ akismetPlugin ];
-      '';
-    };
-    themes = mkOption {
-      default = [];
-      type = types.listOf types.path;
-      description =
-        ''
-          List of path(s) to respective theme(s) which are symlinked from the 'theme' directory. Note: These themes need to be packaged before use, see example.
-        '';
-      example = ''
-        # For shits and giggles, let's package the responsive theme
-        responsiveTheme = pkgs.stdenv.mkDerivation {
-          name = "responsive-theme";
-          # Download the theme from the wordpress site
-          src = pkgs.fetchurl {
-            url = http://wordpress.org/themes/download/responsive.1.9.7.6.zip;
-            sha256 = "06i26xlc5kdnx903b1gfvnysx49fb4kh4pixn89qii3a30fgd8r8";
-          };
-          # We need unzip to build this package
-          buildInputs = [ pkgs.unzip ];
-          # Installing simply means copying all files to the output directory
-          installPhase = "mkdir -p $out; cp -R * $out/";
-        };
-
-        And then pass this theme to the themes list like this:
-          themes = [ responsiveTheme ];
-      '';
-    };
-    languages = mkOption {
-          default = [];
-          description = "Installs wordpress language packs based on the list, see wordpress.nix for possible translations.";
-          example = "[ \"en_GB\" \"de_DE\" ];";
-    };
-    extraConfig = mkOption {
-      type = types.lines;
-      default = "";
-      example =
-        ''
-          define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds
-        '';
-      description = ''
-        Any additional text to be appended to Wordpress's wp-config.php
-        configuration file.  This is a PHP script.  For configuration
-        settings, see <link xlink:href='http://codex.wordpress.org/Editing_wp-config.php'/>.
-      '';
-    };
-    extraHtaccess = mkOption {
-      default = "";
-      example =
-        ''
-          php_value upload_max_filesize 20M
-          php_value post_max_size 20M
-        '';
-      description = ''
-        Any additional text to be appended to Wordpress's .htaccess file.
-      '';
-    };
-  };
-
-  documentRoot = wordpressRoot;
-
-  # FIXME adding the user has to be done manually for the time being
-  startupScript = pkgs.writeScript "init-wordpress.sh" ''
-    #!/bin/sh
-    mkdir -p ${config.wordpressUploads}
-    chown ${serverInfo.serverConfig.user} ${config.wordpressUploads}
-
-    # we should use systemd dependencies here
-    if [ ! -d ${serverInfo.fullConfig.services.mysql.dataDir}/${config.dbName} ]; then
-      echo "Need to create the database '${config.dbName}' and grant permissions to user named '${config.dbUser}'."
-      # Wait until MySQL is up
-      while [ ! -S /run/mysqld/mysqld.sock ]; do
-        sleep 1
-      done
-      ${pkgs.mysql}/bin/mysql -e 'CREATE DATABASE ${config.dbName};'
-      ${pkgs.mysql}/bin/mysql -e "GRANT ALL ON ${config.dbName}.* TO ${config.dbUser}@localhost IDENTIFIED BY \"$(cat ${config.dbPasswordFile})\";"
-    else
-      echo "Good, no need to do anything database related."
-    fi
-  '';
-}
diff --git a/nixos/tests/wordpress.nix b/nixos/tests/wordpress.nix
index 5003e25a7d5..774ef6293b5 100644
--- a/nixos/tests/wordpress.nix
+++ b/nixos/tests/wordpress.nix
@@ -6,48 +6,37 @@ import ./make-test.nix ({ pkgs, ... }:
     maintainers = [ grahamc ]; # under duress!
   };
 
-  nodes =
-    { web =
-        { pkgs, ... }:
-        {
-          services.mysql = {
-            enable = true;
-            package = pkgs.mysql;
-          };
-          services.httpd = {
-            enable = true;
-            logPerVirtualHost = true;
-            adminAddr="js@lastlog.de";
-
-            virtualHosts = [
-              {
-                hostName = "wordpress";
-                extraSubservices =
-                  [
-                    {
-                      serviceType = "wordpress";
-                      dbPassword = "wordpress";
-                      dbHost = "127.0.0.1";
-                      languages = [ "de_DE" "en_GB" ];
-                    }
-                  ];
-              }
-            ];
-          };
-        };
-    };
-
-  testScript =
+  machine =
     { ... }:
-    ''
-      startAll;
+    { services.httpd.adminAddr = "webmaster@site.local";
+      services.httpd.logPerVirtualHost = true;
+
+      services.wordpress."site1.local" = {
+        database.tablePrefix = "site1_";
+      };
 
-      $web->waitForUnit("mysql");
-      $web->waitForUnit("httpd");
+      services.wordpress."site2.local" = {
+        database.tablePrefix = "site2_";
+      };
+
+      networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
+
+      # required for wordpress-init.service to succeed
+      systemd.tmpfiles.rules = [
+        "F /var/lib/wordpress/site1.local/secret-keys.php 0440 wordpress wwwrun - -"
+        "F /var/lib/wordpress/site2.local/secret-keys.php 0440 wordpress wwwrun - -"
+      ];
+    };
 
-      $web->succeed("curl -L 127.0.0.1:80 | grep 'Welcome to the famous'");
+  testScript = ''
+    startAll;
 
+    $machine->waitForUnit("httpd");
+    $machine->waitForUnit("phpfpm-wordpress-site1.local");
+    $machine->waitForUnit("phpfpm-wordpress-site2.local");
 
-    '';
+    $machine->succeed("curl -L site1.local | grep 'Welcome to the famous'");
+    $machine->succeed("curl -L site2.local | grep 'Welcome to the famous'");
+  '';
 
 })