nixos/limesurvey: nginx support (#448680)

This commit is contained in:
Martin Weinelt
2025-10-15 13:38:59 +00:00
committed by GitHub
4 changed files with 309 additions and 213 deletions

View File

@@ -325,6 +325,8 @@
- `services.dnscrypt-proxy` gains a `package` option to specify dnscrypt-proxy package to use. - `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. - `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. - 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 let
inherit (lib) inherit (lib)
literalExpression
mapAttrs
mkDefault mkDefault
mkEnableOption mkEnableOption
mkForce mkForce
@@ -15,20 +17,21 @@ let
mkMerge mkMerge
mkOption mkOption
mkPackageOption mkPackageOption
; mkRenamedOptionModule
inherit (lib)
literalExpression
mapAttrs
optional optional
optionalString optionalString
recursiveUpdate
types types
; ;
cfg = config.services.limesurvey; cfg = config.services.limesurvey;
fpm = config.services.phpfpm.pools.limesurvey; fpm = config.services.phpfpm.pools.limesurvey;
# https://github.com/LimeSurvey/LimeSurvey/blob/master/.github/workflows/main.yml
php = pkgs.php83;
user = "limesurvey"; user = "limesurvey";
group = config.services.httpd.group; group = config.services.${cfg.webserver}.group;
stateDir = "/var/lib/limesurvey"; stateDir = "/var/lib/limesurvey";
configType = configType =
@@ -62,6 +65,13 @@ let
in in
{ {
imports = [
(mkRenamedOptionModule
[ "services" "limesurvey" "virtualHost" ]
[ "services" "limesurvey" "httpd" "virtualHost" ]
)
];
# interface # interface
options.services.limesurvey = { options.services.limesurvey = {
@@ -190,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); type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
example = literalExpression '' example = literalExpression ''
{ {
@@ -206,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 { poolConfig = mkOption {
type = type =
with types; with types;
@@ -241,192 +280,123 @@ in
# implementation # implementation
config = mkIf cfg.enable { config = mkIf (cfg.enable) (mkMerge [
{
assertions = [ 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 = [
{ {
name = cfg.database.user; assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
ensurePermissions = { message = "services.limesurvey.createLocally is currently only supported for database type 'mysql'";
"${cfg.database.name}.*" = "SELECT, CREATE, INSERT, UPDATE, DELETE, ALTER, DROP, INDEX";
};
} }
];
};
services.phpfpm.pools.limesurvey = {
inherit user group;
phpPackage = pkgs.php83;
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"; assertion = cfg.database.createLocally -> cfg.database.user == user;
extraConfig = '' message = "services.limesurvey.database.user must be set to ${user} if services.limesurvey.database.createLocally is set true";
Alias "/tmp" "${stateDir}/tmp" }
<Directory "${stateDir}"> {
AllowOverride all assertion = cfg.database.createLocally -> cfg.database.socket != null;
Require all granted message = "services.limesurvey.database.socket must be set if services.limesurvey.database.createLocally is set to true";
Options -Indexes +FollowSymlinks }
</Directory> {
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" If this message appears when updating your system, please turn off encryption
<Directory "${stateDir}/upload"> in the LimeSurvey interface and create backups before filling the key.
AllowOverride all '';
Require all granted }
Options -Indexes {
</Directory> 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"> If this message appears when updating your system, please turn off encryption
<FilesMatch "\.php$"> in the LimeSurvey interface and create backups before filling the nonce.
<If "-f %{REQUEST_FILENAME}">
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
</If>
</FilesMatch>
AllowOverride all
Options -Indexes
DirectoryIndex index.php
</Directory>
''; '';
} }
]; ];
};
systemd.tmpfiles.rules = [ users.users.${user} = {
"d ${stateDir} 0750 ${user} ${group} - -" group = group;
"d ${stateDir}/tmp 0750 ${user} ${group} - -" isSystemUser = true;
"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"
];
systemd.services.limesurvey-init = { systemd.services.limesurvey-init = {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
before = [ "phpfpm-limesurvey.service" ]; before = [ "phpfpm-limesurvey.service" ];
after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target"; after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target";
environment.DBENGINE = "${cfg.database.dbEngine}"; environment.DBENGINE = "${cfg.database.dbEngine}";
environment.LIMESURVEY_CONFIG = limesurveyConfig; environment.LIMESURVEY_CONFIG = limesurveyConfig;
script = '' script = ''
# update or install the database as required # update or install the database as required
${pkgs.php83}/bin/php ${cfg.package}/share/limesurvey/application/commands/console.php updatedb || \ ${lib.getExe php} ${cfg.package}/share/limesurvey/application/commands/console.php updatedb || \
${pkgs.php83}/bin/php ${cfg.package}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose ${lib.getExe php} ${cfg.package}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
''; '';
serviceConfig = { serviceConfig = {
User = user; User = user;
Group = group; Group = group;
Type = "oneshot"; 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 = [ LoadCredential = [
"encryption_key:${ "encryption_key:${
if cfg.encryptionKeyFile != null then if cfg.encryptionKeyFile != null then
@@ -442,15 +412,118 @@ in
}" }"
]; ];
}; };
};
systemd.services.httpd.after = services.phpfpm.pools.limesurvey = {
optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target"; 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} = { systemd.tmpfiles.rules = [
group = group; "d ${stateDir} 0750 ${user} ${group} - -"
isSystemUser = true; "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, ... }: { lib, pkgs, ... }:
{ {
name = "limesurvey"; name = "limesurvey";
meta.maintainers = [ lib.maintainers.aanderse ]; meta.maintainers = [ lib.maintainers.aanderse ];
nodes.machine = nodes.machine = {
{ ... }: services.limesurvey = {
{ enable = true;
services.limesurvey = { httpd.virtualHost = {
enable = true; hostName = "example.local";
virtualHost = { adminAddr = "root@example.local";
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");
}; };
encryptionKeyFile = pkgs.writeText "key" (lib.strings.replicate 32 "0");
# limesurvey won't work without a dot in the hostname encryptionNonceFile = pkgs.writeText "nonce" (lib.strings.replicate 24 "0");
networking.hosts."127.0.0.1" = [ "example.local" ];
}; };
testScript = '' # limesurvey won't work without a dot in the hostname
start_all() networking.hosts."127.0.0.1" = [ "example.local" ];
machine.wait_for_unit("phpfpm-limesurvey.service") specialisation.nginx = {
assert "The following surveys are available" in machine.succeed( inheritParentConfig = true;
"curl -f http://example.local/" 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()
'';
} }

View File

@@ -4,6 +4,7 @@
fetchFromGitHub, fetchFromGitHub,
writeText, writeText,
nixosTests, nixosTests,
nix-update-script,
}: }:
stdenv.mkDerivation rec { stdenv.mkDerivation rec {
@@ -33,8 +34,9 @@ stdenv.mkDerivation rec {
runHook postInstall runHook postInstall
''; '';
passthru.tests = { passthru = {
smoke-test = nixosTests.limesurvey; tests = { inherit (nixosTests) limesurvey; };
updateScript = nix-update-script { };
}; };
meta = with lib; { meta = with lib; {