nixos/limesurvey: add nginx support

The majority of services in nixpkgs uses nginx, so for better coexistence
we add support for it. The option design follows that of mediawiki.

Co-Authored-By: Julien Malka <julien@malka.sh>
This commit is contained in:
Martin Weinelt 2025-10-05 01:34:16 +02:00
parent 9d9f70b4e9
commit 4d28f658ba
No known key found for this signature in database
GPG key ID: 87C1E9888F856759
3 changed files with 302 additions and 211 deletions

View file

@ -298,6 +298,8 @@
- `services.dnscrypt-proxy` gains a `package` option to specify dnscrypt-proxy package to use.
- `services.limesurvey` now supports nginx as reverse-proxy. Available through [services.limesurvey.webserver](#opt-services.limesurvey.webserver).
- `services.nextcloud.configureRedis` now defaults to `true` in accordance with upstream recommendations to have caching for file locking. See the [upstream doc](https://docs.nextcloud.com/server/31/admin_manual/configuration_files/files_locking_transactional.html) for further details.
- mate-wayland-session 1.28.4 is now using the default wayfire decorator instead of firedecor, thus `services.xserver.desktopManager.mate.enableWaylandSession` is no longer shipping firedecor. If you are experiencing broken window decorations after upgrade, backup and remove `~/.config/mate/wayfire.ini` and re-login.

View file

@ -8,6 +8,8 @@
let
inherit (lib)
literalExpression
mapAttrs
mkDefault
mkEnableOption
mkForce
@ -15,12 +17,10 @@ let
mkMerge
mkOption
mkPackageOption
;
inherit (lib)
literalExpression
mapAttrs
mkRenamedOptionModule
optional
optionalString
recursiveUpdate
types
;
@ -31,7 +31,7 @@ let
php = pkgs.php83;
user = "limesurvey";
group = config.services.httpd.group;
group = config.services.${cfg.webserver}.group;
stateDir = "/var/lib/limesurvey";
configType =
@ -65,6 +65,13 @@ let
in
{
imports = [
(mkRenamedOptionModule
[ "services" "limesurvey" "virtualHost" ]
[ "services" "limesurvey" "httpd" "virtualHost" ]
)
];
# interface
options.services.limesurvey = {
@ -193,7 +200,19 @@ in
};
};
virtualHost = mkOption {
webserver = mkOption {
type = types.enum [
"httpd"
"nginx"
];
default = "httpd";
example = "nginx";
description = ''
Webserver to configure for reverse-proxying limesurvey.
'';
};
httpd.virtualHost = mkOption {
type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
example = literalExpression ''
{
@ -209,6 +228,23 @@ in
'';
};
nginx.virtualHost = mkOption {
type = types.submodule (
recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { }
);
example = literalExpression ''
{
serverName = "survey.example.org";
forceSSL = true;
enableACME = true;
}
'';
description = ''
Nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
See [](#opt-services.nginx.virtualHosts) for further information.
'';
};
poolConfig = mkOption {
type =
with types;
@ -244,192 +280,123 @@ in
# implementation
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
message = "services.limesurvey.createLocally is currently only supported for database type 'mysql'";
}
{
assertion = cfg.database.createLocally -> cfg.database.user == user;
message = "services.limesurvey.database.user must be set to ${user} if services.limesurvey.database.createLocally is set true";
}
{
assertion = cfg.database.createLocally -> cfg.database.socket != null;
message = "services.limesurvey.database.socket must be set if services.limesurvey.database.createLocally is set to true";
}
{
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
message = "a password cannot be specified if services.limesurvey.database.createLocally is set to true";
}
{
assertion = cfg.encryptionKey != null || cfg.encryptionKeyFile != null;
message = ''
You must set `services.limesurvey.encryptionKeyFile` to a file containing a 32-character uppercase hex string.
If this message appears when updating your system, please turn off encryption
in the LimeSurvey interface and create backups before filling the key.
'';
}
{
assertion = cfg.encryptionNonce != null || cfg.encryptionNonceFile != null;
message = ''
You must set `services.limesurvey.encryptionNonceFile` to a file containing a 24-character uppercase hex string.
If this message appears when updating your system, please turn off encryption
in the LimeSurvey interface and create backups before filling the nonce.
'';
}
];
services.limesurvey.config = mapAttrs (name: mkDefault) {
runtimePath = "${stateDir}/tmp/runtime";
components = {
db = {
connectionString =
"${cfg.database.type}:dbname=${cfg.database.name};host=${
if pgsqlLocal then cfg.database.socket else cfg.database.host
};port=${toString cfg.database.port}"
+ optionalString mysqlLocal ";socket=${cfg.database.socket}";
username = cfg.database.user;
password = mkIf (
cfg.database.passwordFile != null
) "file_get_contents(\"${toString cfg.database.passwordFile}\");";
tablePrefix = "limesurvey_";
};
assetManager.basePath = "${stateDir}/tmp/assets";
urlManager = {
urlFormat = "path";
showScriptName = false;
};
};
config = {
tempdir = "${stateDir}/tmp";
uploaddir = "${stateDir}/upload";
userquestionthemerootdir = "${stateDir}/upload/themes/question";
force_ssl = mkIf (
cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL
) "on";
config.defaultlang = "en";
};
};
services.mysql = mkIf mysqlLocal {
enable = true;
package = mkDefault pkgs.mariadb;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
config = mkIf (cfg.enable) (mkMerge [
{
assertions = [
{
name = cfg.database.user;
ensurePermissions = {
"${cfg.database.name}.*" = "SELECT, CREATE, INSERT, UPDATE, DELETE, ALTER, DROP, INDEX";
};
assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
message = "services.limesurvey.createLocally is currently only supported for database type 'mysql'";
}
];
};
services.phpfpm.pools.limesurvey = {
inherit user group;
phpPackage = php;
phpEnv.DBENGINE = "${cfg.database.dbEngine}";
phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}";
# App code cannot access credentials directly since the service starts
# with the root user so we copy the credentials to a place accessible to Limesurvey
phpEnv.CREDENTIALS_DIRECTORY = "${stateDir}/credentials";
settings = {
"listen.owner" = config.services.httpd.user;
"listen.group" = config.services.httpd.group;
}
// cfg.poolConfig;
};
systemd.services.phpfpm-limesurvey.serviceConfig = {
ExecStartPre = pkgs.writeShellScript "limesurvey-phpfpm-exec-pre" ''
cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_key "${stateDir}/credentials/encryption_key"
chown ${user}:${group} "${stateDir}/credentials/encryption_key"
cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_nonce "${stateDir}/credentials/encryption_nonce"
chown ${user}:${group} "${stateDir}/credentials/encryption_nonce"
'';
LoadCredential = [
"encryption_key:${
if cfg.encryptionKeyFile != null then
cfg.encryptionKeyFile
else
pkgs.writeText "key" cfg.encryptionKey
}"
"encryption_nonce:${
if cfg.encryptionNonceFile != null then
cfg.encryptionNonceFile
else
pkgs.writeText "nonce" cfg.encryptionKey
}"
];
};
services.httpd = {
enable = true;
adminAddr = mkDefault cfg.virtualHost.adminAddr;
extraModules = [ "proxy_fcgi" ];
virtualHosts.${cfg.virtualHost.hostName} = mkMerge [
cfg.virtualHost
{
documentRoot = mkForce "${cfg.package}/share/limesurvey";
extraConfig = ''
Alias "/tmp" "${stateDir}/tmp"
<Directory "${stateDir}">
AllowOverride all
Require all granted
Options -Indexes +FollowSymlinks
</Directory>
assertion = cfg.database.createLocally -> cfg.database.user == user;
message = "services.limesurvey.database.user must be set to ${user} if services.limesurvey.database.createLocally is set true";
}
{
assertion = cfg.database.createLocally -> cfg.database.socket != null;
message = "services.limesurvey.database.socket must be set if services.limesurvey.database.createLocally is set to true";
}
{
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
message = "a password cannot be specified if services.limesurvey.database.createLocally is set to true";
}
{
assertion = cfg.encryptionKey != null || cfg.encryptionKeyFile != null;
message = ''
You must set `services.limesurvey.encryptionKeyFile` to a file containing a 32-character uppercase hex string.
Alias "/upload" "${stateDir}/upload"
<Directory "${stateDir}/upload">
AllowOverride all
Require all granted
Options -Indexes
</Directory>
If this message appears when updating your system, please turn off encryption
in the LimeSurvey interface and create backups before filling the key.
'';
}
{
assertion = cfg.encryptionNonce != null || cfg.encryptionNonceFile != null;
message = ''
You must set `services.limesurvey.encryptionNonceFile` to a file containing a 24-character uppercase hex string.
<Directory "${cfg.package}/share/limesurvey">
<FilesMatch "\.php$">
<If "-f %{REQUEST_FILENAME}">
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
</If>
</FilesMatch>
AllowOverride all
Options -Indexes
DirectoryIndex index.php
</Directory>
If this message appears when updating your system, please turn off encryption
in the LimeSurvey interface and create backups before filling the nonce.
'';
}
];
};
systemd.tmpfiles.rules = [
"d ${stateDir} 0750 ${user} ${group} - -"
"d ${stateDir}/tmp 0750 ${user} ${group} - -"
"d ${stateDir}/tmp/assets 0750 ${user} ${group} - -"
"d ${stateDir}/tmp/runtime 0750 ${user} ${group} - -"
"d ${stateDir}/tmp/upload 0750 ${user} ${group} - -"
"d ${stateDir}/credentials 0700 ${user} ${group} - -"
"C ${stateDir}/upload 0750 ${user} ${group} - ${cfg.package}/share/limesurvey/upload"
];
users.users.${user} = {
group = group;
isSystemUser = true;
};
systemd.services.limesurvey-init = {
wantedBy = [ "multi-user.target" ];
before = [ "phpfpm-limesurvey.service" ];
after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target";
environment.DBENGINE = "${cfg.database.dbEngine}";
environment.LIMESURVEY_CONFIG = limesurveyConfig;
script = ''
# update or install the database as required
${lib.getExe php} ${cfg.package}/share/limesurvey/application/commands/console.php updatedb || \
${lib.getExe php} ${cfg.package}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
'';
serviceConfig = {
User = user;
Group = group;
Type = "oneshot";
systemd.services.limesurvey-init = {
wantedBy = [ "multi-user.target" ];
before = [ "phpfpm-limesurvey.service" ];
after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target";
environment.DBENGINE = "${cfg.database.dbEngine}";
environment.LIMESURVEY_CONFIG = limesurveyConfig;
script = ''
# update or install the database as required
${lib.getExe php} ${cfg.package}/share/limesurvey/application/commands/console.php updatedb || \
${lib.getExe php} ${cfg.package}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
'';
serviceConfig = {
User = user;
Group = group;
Type = "oneshot";
LoadCredential = [
"encryption_key:${
if cfg.encryptionKeyFile != null then
cfg.encryptionKeyFile
else
pkgs.writeText "key" cfg.encryptionKey
}"
"encryption_nonce:${
if cfg.encryptionNonceFile != null then
cfg.encryptionNonceFile
else
pkgs.writeText "nonce" cfg.encryptionKey
}"
];
};
};
services.limesurvey.config = mapAttrs (name: mkDefault) {
runtimePath = "${stateDir}/tmp/runtime";
components = {
db = {
connectionString =
"${cfg.database.type}:dbname=${cfg.database.name};host=${
if pgsqlLocal then cfg.database.socket else cfg.database.host
};port=${toString cfg.database.port}"
+ optionalString mysqlLocal ";socket=${cfg.database.socket}";
username = cfg.database.user;
password = mkIf (
cfg.database.passwordFile != null
) "file_get_contents(\"${toString cfg.database.passwordFile}\");";
tablePrefix = "limesurvey_";
};
assetManager.basePath = "${stateDir}/tmp/assets";
urlManager = {
urlFormat = "path";
showScriptName = false;
};
};
config = {
tempdir = "${stateDir}/tmp";
uploaddir = "${stateDir}/upload";
userquestionthemerootdir = "${stateDir}/upload/themes/question";
force_ssl = mkIf (
cfg.${cfg.webserver}.virtualHost.addSSL
|| cfg.${cfg.webserver}.virtualHost.forceSSL
|| cfg.${cfg.webserver}.virtualHost.onlySSL
) "on";
config.defaultlang = "en";
};
};
systemd.services.phpfpm-limesurvey.serviceConfig = {
ExecStartPre = pkgs.writeShellScript "limesurvey-phpfpm-exec-pre" ''
cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_key "${stateDir}/credentials/encryption_key"
chown ${user}:${group} "${stateDir}/credentials/encryption_key"
cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_nonce "${stateDir}/credentials/encryption_nonce"
chown ${user}:${group} "${stateDir}/credentials/encryption_nonce"
'';
LoadCredential = [
"encryption_key:${
if cfg.encryptionKeyFile != null then
@ -445,15 +412,118 @@ in
}"
];
};
};
systemd.services.httpd.after =
optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target";
services.phpfpm.pools.limesurvey = {
inherit user group;
phpPackage = php;
phpEnv.DBENGINE = "${cfg.database.dbEngine}";
phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}";
# App code cannot access credentials directly since the service starts
# with the root user so we copy the credentials to a place accessible to Limesurvey
phpEnv.CREDENTIALS_DIRECTORY = "${stateDir}/credentials";
settings = {
"listen.owner" = config.services.${cfg.webserver}.user;
"listen.group" = config.services.${cfg.webserver}.group;
}
// cfg.poolConfig;
};
users.users.${user} = {
group = group;
isSystemUser = true;
};
systemd.tmpfiles.rules = [
"d ${stateDir} 0750 ${user} ${group} - -"
"d ${stateDir}/tmp 0750 ${user} ${group} - -"
"d ${stateDir}/tmp/assets 0750 ${user} ${group} - -"
"d ${stateDir}/tmp/runtime 0750 ${user} ${group} - -"
"d ${stateDir}/tmp/upload 0750 ${user} ${group} - -"
"d ${stateDir}/credentials 0700 ${user} ${group} - -"
"C ${stateDir}/upload 0750 ${user} ${group} - ${cfg.package}/share/limesurvey/upload"
];
}
};
(mkIf mysqlLocal {
services.mysql = {
enable = true;
package = mkDefault pkgs.mariadb;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.database.user;
ensurePermissions = {
"${cfg.database.name}.*" = "SELECT, CREATE, INSERT, UPDATE, DELETE, ALTER, DROP, INDEX";
};
}
];
};
})
(mkIf (cfg.webserver == "httpd") {
services.httpd = {
enable = true;
adminAddr = mkDefault cfg.httpd.virtualHost.adminAddr;
extraModules = [ "proxy_fcgi" ];
virtualHosts.${cfg.httpd.virtualHost.hostName} = mkMerge [
cfg.httpd.virtualHost
{
documentRoot = mkForce "${cfg.package}/share/limesurvey";
extraConfig = ''
Alias "/tmp" "${stateDir}/tmp"
<Directory "${stateDir}">
AllowOverride all
Require all granted
Options -Indexes +FollowSymlinks
</Directory>
Alias "/upload" "${stateDir}/upload"
<Directory "${stateDir}/upload">
AllowOverride all
Require all granted
Options -Indexes
</Directory>
<Directory "${cfg.package}/share/limesurvey">
<FilesMatch "\.php$">
<If "-f %{REQUEST_FILENAME}">
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
</If>
</FilesMatch>
AllowOverride all
Options -Indexes
DirectoryIndex index.php
</Directory>
'';
}
];
};
systemd.services.httpd.after =
optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target";
})
(mkIf (cfg.webserver == "nginx") {
services.nginx = {
enable = true;
virtualHosts.${cfg.nginx.virtualHost.serverName} = lib.mkMerge [
cfg.nginx.virtualHost
{
root = lib.mkForce "${cfg.package}/share/limesurvey";
locations = {
"/" = {
index = "index.php";
tryFiles = "$uri /index.php?$args";
};
"~ \.php$".extraConfig = ''
fastcgi_pass unix:${config.services.phpfpm.pools."limesurvey".socket};
'';
"/tmp".root = "/var/lib/limesurvey";
"/upload/".root = "/var/lib/limesurvey";
};
}
];
};
systemd.services.nginx.after =
optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
})
]);
}

View file

@ -1,31 +1,50 @@
{ lib, pkgs, ... }:
{
name = "limesurvey";
meta.maintainers = [ lib.maintainers.aanderse ];
nodes.machine =
{ ... }:
{
services.limesurvey = {
enable = true;
virtualHost = {
hostName = "example.local";
adminAddr = "root@example.local";
};
encryptionKeyFile = pkgs.writeText "key" (lib.strings.replicate 32 "0");
encryptionNonceFile = pkgs.writeText "nonce" (lib.strings.replicate 24 "0");
nodes.machine = {
services.limesurvey = {
enable = true;
httpd.virtualHost = {
hostName = "example.local";
adminAddr = "root@example.local";
};
# limesurvey won't work without a dot in the hostname
networking.hosts."127.0.0.1" = [ "example.local" ];
encryptionKeyFile = pkgs.writeText "key" (lib.strings.replicate 32 "0");
encryptionNonceFile = pkgs.writeText "nonce" (lib.strings.replicate 24 "0");
};
testScript = ''
start_all()
# limesurvey won't work without a dot in the hostname
networking.hosts."127.0.0.1" = [ "example.local" ];
machine.wait_for_unit("phpfpm-limesurvey.service")
assert "The following surveys are available" in machine.succeed(
"curl -f http://example.local/"
)
'';
specialisation.nginx = {
inheritParentConfig = true;
configuration.services.limesurvey = {
webserver = "nginx";
nginx.virtualHost.serverName = "example.local";
};
};
};
testScript =
{ nodes, ... }:
''
def test():
machine.wait_until_succeeds("curl --fail --silent http://example.local/ | grep -q 'The following surveys are available'")
start_all()
machine.wait_for_unit("phpfpm-limesurvey.service")
machine.wait_for_unit("httpd.service")
machine.wait_for_open_port(80)
test()
machine.execute("${nodes.machine.system.build.toplevel}/specialisation/nginx/bin/switch-to-configuration test")
machine.wait_for_unit("nginx.service")
test()
'';
}