From 49305054805a50e9d1e815f63cfefcf20aed8fe1 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Tue, 24 Feb 2026 13:39:48 +0100 Subject: [PATCH] nixos/speedtest-tracker: init module Signed-off-by: Paul Meyer --- nixos/modules/module-list.nix | 1 + .../services/web-apps/speedtest-tracker.nix | 429 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/speedtest-tracker.nix | 33 ++ 4 files changed, 464 insertions(+) create mode 100644 nixos/modules/services/web-apps/speedtest-tracker.nix create mode 100644 nixos/tests/speedtest-tracker.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 5903916b62a7..2b3b8fcc1a16 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1755,6 +1755,7 @@ ./services/web-apps/snipe-it.nix ./services/web-apps/snips-sh.nix ./services/web-apps/sogo.nix + ./services/web-apps/speedtest-tracker.nix ./services/web-apps/sshwifty.nix ./services/web-apps/stash.nix ./services/web-apps/stirling-pdf.nix diff --git a/nixos/modules/services/web-apps/speedtest-tracker.nix b/nixos/modules/services/web-apps/speedtest-tracker.nix new file mode 100644 index 000000000000..310fbb52012b --- /dev/null +++ b/nixos/modules/services/web-apps/speedtest-tracker.nix @@ -0,0 +1,429 @@ +{ + pkgs, + config, + lib, + ... +}: + +let + cfg = config.services.speedtest-tracker; + + user = cfg.user; + group = cfg.group; + + defaultUser = "speedtest-tracker"; + defaultGroup = "speedtest-tracker"; + + artisan = "${cfg.package}/artisan"; + + env-file-values = lib.mapAttrs' (n: v: { + name = lib.removeSuffix "_FILE" n; + value = v; + }) (lib.filterAttrs (n: v: v != null && lib.match ".+_FILE" n != null) cfg.settings); + + env-nonfile-values = lib.filterAttrs (n: v: lib.match ".+_FILE" n == null) cfg.settings; + + speedtest-tracker-maintenance = pkgs.writeShellScript "speedtest-tracker-maintenance.sh" '' + set -a + ${lib.toShellVars env-nonfile-values} + ${lib.concatLines (lib.mapAttrsToList (n: v: "${n}=\"$(< ${v})\"") env-file-values)} + set +a + ${lib.optionalString ( + cfg.settings.DB_CONNECTION == "sqlite" + ) "touch ${cfg.dataDir}/storage/database/database.sqlite"} + rm -f ${cfg.dataDir}/cache/*.php + ${artisan} package:discover + ${artisan} migrate --force + ${artisan} optimize:clear + ${artisan} view:cache + ${artisan} config:cache + ''; + + speedtest-tracker-env-script = + command: + pkgs.writeShellScript "speedtest-tracker-${builtins.replaceStrings [ ":" " " ] [ "-" "-" ] command}.sh" '' + set -a + ${lib.toShellVars env-nonfile-values} + ${lib.concatLines (lib.mapAttrsToList (n: v: "${n}=\"$(< ${v})\"") env-file-values)} + set +a + exec ${artisan} ${command} + ''; + + commonServiceConfig = { + Type = "oneshot"; + User = user; + Group = group; + StateDirectory = "speedtest-tracker"; + ReadWritePaths = [ cfg.dataDir ]; + WorkingDirectory = cfg.package; + PrivateTmp = true; + PrivateDevices = true; + CapabilityBoundingSet = ""; + AmbientCapabilities = ""; + ProtectSystem = "strict"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + ProtectClock = true; + ProtectHostname = true; + ProtectHome = "tmpfs"; + ProtectKernelLogs = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + PrivateNetwork = false; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service @resources" + "~@obsolete @privileged" + ]; + RestrictSUIDSGID = true; + RemoveIPC = true; + NoNewPrivileges = true; + RestrictRealtime = true; + RestrictNamespaces = true; + LockPersonality = true; + PrivateUsers = true; + }; + +in +{ + options.services.speedtest-tracker = { + + enable = lib.mkEnableOption "Speedtest Tracker: A self-hosted internet performance tracking application"; + + package = + lib.mkPackageOption pkgs "speedtest-tracker" { } + // lib.mkOption { + apply = + speedtest-tracker: + speedtest-tracker.override (prev: { + dataDir = cfg.dataDir; + }); + }; + + user = lib.mkOption { + type = lib.types.str; + default = defaultUser; + description = "User account under which Speedtest Tracker runs."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = if cfg.enableNginx then "nginx" else defaultGroup; + defaultText = "If `services.speedtest-tracker.enableNginx` is true then `nginx` else ${defaultGroup}"; + description = '' + Group under which Speedtest Tracker runs. It is best to set this to the group + of whatever webserver is being used as the frontend. + ''; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/speedtest-tracker"; + description = '' + The place where Speedtest Tracker stores its state. + ''; + }; + + enableNginx = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to enable nginx or not. If enabled, an nginx virtual host will + be created for access to Speedtest Tracker. If not enabled, then you may use + `''${config.services.speedtest-tracker.package}` as your document root in + whichever webserver you wish to setup. + ''; + }; + + virtualHost = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = '' + The hostname at which you wish Speedtest Tracker to be served. If you have + enabled nginx using `services.speedtest-tracker.enableNginx` then this will + be used. + ''; + }; + + poolConfig = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.str + lib.types.int + lib.types.bool + ] + ); + default = { }; + defaultText = '' + { + "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 Speedtest Tracker PHP pool. See the documentation on `php-fpm.conf` + for details on configuration directives. + ''; + }; + + settings = lib.mkOption { + default = { }; + description = '' + Options for Speedtest Tracker configuration. Refer to + for + details on supported values. All `_FILE` values are supported: + append `_FILE` to the setting name to provide a path to a file + containing the secret value. + ''; + example = lib.literalExpression '' + { + APP_KEY_FILE = "/var/secrets/speedtest-tracker-app-key.txt"; + DB_CONNECTION = "mysql"; + DB_HOST = "db"; + DB_PORT = 3306; + DB_DATABASE = "speedtest-tracker"; + DB_USERNAME = "speedtest-tracker"; + DB_PASSWORD_FILE = "/var/secrets/speedtest-tracker-mysql-password.txt"; + } + ''; + type = lib.types.submodule { + freeformType = lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.str + lib.types.int + lib.types.bool + ] + ); + options = { + APP_KEY_FILE = lib.mkOption { + type = lib.types.path; + description = '' + The path to your appkey. The file should contain a 32 character + random app key. This may be set using `echo "base64:$(head -c 32 + /dev/urandom | base64)" > /path/to/key-file`. + ''; + }; + APP_URL = lib.mkOption { + type = lib.types.str; + default = + if cfg.virtualHost == "localhost" then + "http://${cfg.virtualHost}" + else + "https://${cfg.virtualHost}"; + defaultText = '' + http(s)://''${config.services.speedtest-tracker.virtualHost} + ''; + description = '' + The APP_URL used by Speedtest Tracker internally. Please make sure this + URL matches the external URL of your installation. It is used to + validate specific requests and to generate URLs in notifications. + ''; + }; + DB_CONNECTION = lib.mkOption { + type = lib.types.enum [ + "sqlite" + "mysql" + "mariadb" + "pgsql" + ]; + default = "sqlite"; + example = "pgsql"; + description = '' + The type of database you wish to use. Can be one of "sqlite", + "mysql", "mariadb" or "pgsql". + ''; + }; + DB_HOST = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = '' + The IP or hostname which hosts your database. + ''; + }; + DB_PORT = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = + if cfg.settings.DB_CONNECTION == "pgsql" then + 5432 + else if cfg.settings.DB_CONNECTION == "mysql" || cfg.settings.DB_CONNECTION == "mariadb" then + 3306 + else + null; + defaultText = '' + `null` if DB_CONNECTION is "sqlite", `3306` if "mysql" or "mariadb", `5432` if "pgsql" + ''; + description = '' + The port your database is listening at. sqlite does not require + this value to be filled. + ''; + }; + DB_DATABASE = lib.mkOption { + type = lib.types.str; + default = + if cfg.settings.DB_CONNECTION == "sqlite" then + "${cfg.dataDir}/storage/database/database.sqlite" + else + "speedtest-tracker"; + defaultText = '' + "''${config.services.speedtest-tracker.dataDir}/storage/database/database.sqlite" if DB_CONNECTION is "sqlite", "speedtest-tracker" otherwise + ''; + description = '' + The name of the database, or path to the sqlite file. + ''; + }; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + + services.phpfpm.pools.speedtest-tracker = { + inherit user group; + phpPackage = cfg.package.phpPackage; + phpOptions = '' + log_errors = on + ''; + settings = { + "listen.mode" = lib.mkDefault "0660"; + "listen.owner" = lib.mkDefault user; + "listen.group" = lib.mkDefault group; + "pm" = lib.mkDefault "dynamic"; + "pm.max_children" = lib.mkDefault 32; + "pm.start_servers" = lib.mkDefault 2; + "pm.min_spare_servers" = lib.mkDefault 2; + "pm.max_spare_servers" = lib.mkDefault 4; + "pm.max_requests" = lib.mkDefault 500; + } + // cfg.poolConfig; + }; + + systemd.services.speedtest-tracker-setup = { + after = [ + "postgresql.target" + "mysql.service" + ]; + requiredBy = [ "phpfpm-speedtest-tracker.service" ]; + before = [ "phpfpm-speedtest-tracker.service" ]; + serviceConfig = { + ExecStart = speedtest-tracker-maintenance; + RemainAfterExit = true; + } + // commonServiceConfig; + unitConfig.JoinsNamespaceOf = "phpfpm-speedtest-tracker.service"; + restartTriggers = [ cfg.package ]; + partOf = [ "phpfpm-speedtest-tracker.service" ]; + }; + + systemd.services.speedtest-tracker-scheduler = { + after = [ "speedtest-tracker-setup.service" ]; + wants = [ "speedtest-tracker-setup.service" ]; + description = "Speedtest Tracker scheduler"; + path = [ pkgs.ookla-speedtest ]; + serviceConfig = { + ExecStart = speedtest-tracker-env-script "schedule:run"; + } + // commonServiceConfig; + }; + + systemd.timers.speedtest-tracker-scheduler = { + description = "Speedtest Tracker scheduler timer"; + timerConfig = { + OnCalendar = "minutely"; + Persistent = true; + }; + wantedBy = [ "timers.target" ]; + restartTriggers = [ cfg.package ]; + }; + + systemd.services.speedtest-tracker-queue-worker = { + after = [ "speedtest-tracker-setup.service" ]; + wants = [ "speedtest-tracker-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + description = "Speedtest Tracker queue worker"; + path = [ pkgs.ookla-speedtest ]; + serviceConfig = commonServiceConfig // { + Type = "simple"; + Restart = "always"; + ExecStart = speedtest-tracker-env-script "queue:work --sleep=3 --tries=3"; + }; + }; + + services.nginx = lib.mkIf cfg.enableNginx { + enable = true; + recommendedTlsSettings = lib.mkDefault true; + recommendedOptimisation = lib.mkDefault true; + recommendedGzipSettings = lib.mkDefault true; + virtualHosts.${cfg.virtualHost} = { + root = "${cfg.package}/public"; + locations = { + "/" = { + tryFiles = "$uri $uri/ /index.php?$query_string"; + index = "index.php"; + extraConfig = '' + sendfile off; + ''; + }; + "~ \\.php$" = { + extraConfig = '' + include ${config.services.nginx.package}/conf/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; + fastcgi_param modHeadersAvailable true; + fastcgi_pass unix:${config.services.phpfpm.pools.speedtest-tracker.socket}; + ''; + }; + "~ \\.(js|css|gif|png|ico|jpg|jpeg)$" = { + extraConfig = "expires 365d;"; + }; + }; + }; + }; + + systemd.tmpfiles.settings."10-speedtest-tracker" = + lib.genAttrs + [ + "${cfg.dataDir}/storage" + "${cfg.dataDir}/storage/app" + "${cfg.dataDir}/storage/database" + "${cfg.dataDir}/storage/framework" + "${cfg.dataDir}/storage/framework/cache" + "${cfg.dataDir}/storage/framework/sessions" + "${cfg.dataDir}/storage/framework/views" + "${cfg.dataDir}/storage/logs" + "${cfg.dataDir}/cache" + ] + (n: { + d = { + inherit group; + mode = "0700"; + inherit user; + }; + }) + // { + "${cfg.dataDir}".d = { + inherit group; + mode = "0710"; + inherit user; + }; + }; + + users = { + users = lib.mkIf (user == defaultUser) { + ${defaultUser} = { + inherit group; + isSystemUser = true; + home = cfg.dataDir; + }; + }; + groups = lib.mkIf (group == defaultGroup) { ${defaultGroup} = { }; }; + }; + }; + + meta.maintainers = pkgs.speedtest-tracker.meta.maintainers; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 2296234b1b3c..21638790bfb1 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1484,6 +1484,7 @@ in sonic-server = runTest ./sonic-server.nix; spacecookie = runTest ./spacecookie.nix; spark = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./spark { }; + speedtest-tracker = runTest ./speedtest-tracker.nix; spiped = runTest ./spiped.nix; sqlite3-to-mysql = runTest ./sqlite3-to-mysql.nix; squid = runTest ./squid.nix; diff --git a/nixos/tests/speedtest-tracker.nix b/nixos/tests/speedtest-tracker.nix new file mode 100644 index 000000000000..5d5c42957300 --- /dev/null +++ b/nixos/tests/speedtest-tracker.nix @@ -0,0 +1,33 @@ +{ lib, pkgs, ... }: + +let + app-key = "base64:VGVzdFRlc3RUZXN0VGVzdFRlc3RUZXN0VGVzdFRlc3Q="; +in +{ + name = "speedtest-tracker"; + meta = { + maintainers = pkgs.speedtest-tracker.meta.maintainers; + platforms = lib.platforms.linux; + }; + + nodes.sqlite = { + environment.etc."speedtest-tracker-appkey".text = app-key; + services.speedtest-tracker = { + enable = true; + enableNginx = true; + settings = { + APP_KEY_FILE = "/etc/speedtest-tracker-appkey"; + }; + }; + }; + + testScript = '' + sqlite.wait_for_unit("phpfpm-speedtest-tracker.service") + sqlite.wait_for_unit("nginx.service") + sqlite.wait_for_unit("speedtest-tracker-queue-worker.service") + + sqlite.succeed("curl -Ls -o /dev/null -w '%{http_code}' http://localhost/admin/login | grep '200'") + sqlite.succeed("curl -Ls http://localhost/admin/login | grep -i 'login'") + sqlite.succeed("systemctl start speedtest-tracker-scheduler.service") + ''; +}