From 8c6fb9a752528f933499c0617fc8203951c42a75 Mon Sep 17 00:00:00 2001 From: Adam Thompson-Sharpe Date: Mon, 2 Mar 2026 18:40:58 -0500 Subject: [PATCH] nixos/kiwix-serve: init module Adds a NixOS service module for kiwix-serve, which was requested quite a while ago. kiwix-serve allows one to host ZIM files (such as for archives of Wikipedia) over HTTP. A NixOS VM test that generates and serves a basic ZIM file has also been added. The ZIM file is generated as part of the test, since the output file is relatively large (~60 KB) relative to the source content (~100 bytes). See: --- .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/kiwix-serve.nix | 187 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/kiwix-serve/default.nix | 75 +++++++ nixos/tests/kiwix-serve/html/icon.png | Bin 0 -> 84 bytes nixos/tests/kiwix-serve/html/index.html | 11 ++ pkgs/by-name/ki/kiwix-tools/package.nix | 3 + 8 files changed, 280 insertions(+) create mode 100644 nixos/modules/services/misc/kiwix-serve.nix create mode 100644 nixos/tests/kiwix-serve/default.nix create mode 100644 nixos/tests/kiwix-serve/html/icon.png create mode 100644 nixos/tests/kiwix-serve/html/index.html diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index a708c285091b..8a7d7d6b9f23 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -30,6 +30,8 @@ - [qui](https://github.com/autobrr/qui), a modern alternative webUI for qBittorrent, with multi-instance support. Written in Go/React. Available as [services.qui](#opt-services.qui.enable). +- [kiwix-serve](https://wiki.kiwix.org/wiki/Kiwix-serve), a service that serves ZIM files (such as Wikipedia archives) over HTTP. Available as [services.kiwix-serve](#opt-services.kiwix-serve.enable). + - [Remark42](https://remark42.com/), a self-hosted comment engine. Available as [services.remark42](#opt-services.remark42.enable). - [LibreChat](https://www.librechat.ai/), open-source self-hostable ChatGPT clone with Agents and RAG APIs. Available as [services.librechat](#opt-services.librechat.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 2db1636f824e..a47b556eca70 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -882,6 +882,7 @@ ./services/misc/jackett.nix ./services/misc/jellyfin.nix ./services/misc/jellyseerr.nix + ./services/misc/kiwix-serve.nix ./services/misc/klipper.nix ./services/misc/languagetool.nix ./services/misc/leaps.nix diff --git a/nixos/modules/services/misc/kiwix-serve.nix b/nixos/modules/services/misc/kiwix-serve.nix new file mode 100644 index 000000000000..7f818d7e9121 --- /dev/null +++ b/nixos/modules/services/misc/kiwix-serve.nix @@ -0,0 +1,187 @@ +{ + config, + lib, + pkgs, + utils, + ... +}: +let + inherit (lib) types; + cfg = config.services.kiwix-serve; + # Create a directory containing symlinks to ZIM files + mkLibrary = + library: + let + libraryEntries = lib.mapAttrsToList (name: path: { + name = "${name}.zim"; + inherit path; + }) library; + + zimsDrv = pkgs.linkFarm "zims" libraryEntries; + + files = map (entry: "${zimsDrv}/${entry.name}") libraryEntries; + in + { + derivation = zimsDrv; + inherit files; + }; +in +{ + options = { + services.kiwix-serve = { + enable = lib.mkEnableOption "the kiwix-serve server"; + + package = lib.mkPackageOption pkgs "kiwix-tools" { }; + + address = lib.mkOption { + type = types.str; + default = "all"; + example = "ipv4"; + description = '' + Listen only on the specified IP address. + Specify "ipv4", "ipv6" or "all" to listen on all IPv4, IPv6, or both types of addresses, respectively. + ''; + }; + + port = lib.mkOption { + type = types.port; + default = 8080; + description = "The port on which to run kiwix-serve."; + }; + + openFirewall = lib.mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for the configured port."; + }; + + library = lib.mkOption { + type = types.attrsOf types.path; + default = { }; + example = lib.literalExpression ( + lib.removeSuffix "\n" '' + { + wikipedia = "/data/wikipedia_en_all_maxi_2026-02.zim"; + nix = pkgs.fetchurl { + url = "https://download.kiwix.org/zim/devdocs/devdocs_en_nix_2026-01.zim"; + hash = "sha256-QxB9qDKSzzEU8t4droI08BXdYn+HMVkgiJMO3SoGTqM="; + }; + } + '' + ); + description = '' + A set of ZIM files to serve. The key is used as the name for the ZIM files + (e.g. in the example, the files will be served as `wikipedia.zim` and `nix.zim`). + + Exclusive with [services.kiwix-serve.libraryPath](#opt-services.kiwix-serve.libraryPath). + ''; + }; + + libraryPath = lib.mkOption { + type = types.nullOr types.path; + default = null; + example = "/data/library.xml"; + description = '' + An XML library file listing ZIM files to serve. + For more information, see . + + Exclusive with [services.kiwix-serve.library](#opt-services.kiwix-serve.library). + ''; + }; + + extraArgs = lib.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "--verbose" + "--skipInvalid" + ]; + description = "Extra arguments to pass to kiwix-serve."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = (cfg.library == { }) != (cfg.libraryPath == null); + message = "Exactly one of services.kiwix-serve.library or services.kiwix-serve.libraryPath must be provided."; + } + ]; + + systemd.services.kiwix-serve = + let + library = mkLibrary cfg.library; + in + { + description = "ZIM file HTTP server"; + documentation = [ "https://kiwix-tools.readthedocs.io/en/latest/kiwix-serve.html" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "exec"; + DynamicUser = true; + Restart = "on-failure"; + ExecStart = utils.escapeSystemdExecArgs ( + [ + (lib.getExe' cfg.package "kiwix-serve") + "--address" + cfg.address + "--port" + cfg.port + ] + ++ lib.optionals (cfg.libraryPath != null) [ + "--library" + cfg.libraryPath + ] + ++ lib.optionals (cfg.library != { }) library.files + ++ cfg.extraArgs + ); + + CapabilityBoundingSet = ""; + DeviceAllow = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateUsers = true; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + ]; + UMask = "0077"; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; + }; + + meta = { + maintainers = with lib.maintainers; [ MysteryBlokHed ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 343500f195e2..cfd5427f3979 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -840,6 +840,7 @@ in keymap = handleTest ./keymap.nix { }; kimai = runTest ./kimai.nix; kismet = runTest ./kismet.nix; + kiwix-serve = runTest ./kiwix-serve; kmonad = runTest ./kmonad.nix; kmscon = runTest ./kmscon.nix; knot = runTest ./knot.nix; diff --git a/nixos/tests/kiwix-serve/default.nix b/nixos/tests/kiwix-serve/default.nix new file mode 100644 index 000000000000..b30f50c115b4 --- /dev/null +++ b/nixos/tests/kiwix-serve/default.nix @@ -0,0 +1,75 @@ +{ lib, pkgs, ... }: +let + mkTestZim = + name: + pkgs.runCommandLocal "${name}.zim" + { + nativeBuildInputs = [ pkgs.zim-tools ]; + } + '' + ${lib.getExe' pkgs.zim-tools "zimwriterfs"} \ + --name "${name}" \ + --title 'NixOS kiwix-serve Test' \ + --description 'NixOS test of kiwix-serve' \ + --creator Nixpkgs \ + --publisher Nixpkgs \ + --language eng \ + --welcome index.html \ + --illustration icon.png \ + ${./html} \ + $out + ''; + + # Test files must have different names or kiwix-serve will only serve one of them + testZimStore = mkTestZim "test-store"; + testZimOutside = mkTestZim "test-outside"; +in +{ + name = "kiwix-serve"; + meta.maintainers = with lib.maintainers; [ MysteryBlokHed ]; + + nodes = { + machine = { + systemd.services.copy-zim-file = { + description = "Copy test ZIM file to host system to test paths outside of store"; + wantedBy = [ "multi-user.target" ]; + before = [ "kiwix-serve.service" ]; + requiredBy = [ "kiwix-serve.service" ]; + + serviceConfig = { + Type = "oneshot"; + }; + + script = '' + mkdir -p /var/lib/kiwix-serve + cp ${testZimOutside} /var/lib/kiwix-serve/test-outside.zim + ''; + }; + + services.kiwix-serve = { + enable = true; + port = 8080; + library = { + test-store = testZimStore; + test-outside = "/var/lib/kiwix-serve/test-outside.zim"; + }; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("kiwix-serve.service") + machine.wait_for_open_port(8080) + machine.wait_until_succeeds("curl --fail --silent --head http://localhost:8080") + + # ZIM file in store + test_content = machine.succeed("curl --fail --silent --location http://localhost:8080/content/test-store") + print(test_content) + assert "NixOS test of kiwix-serve" in test_content, "kiwix-serve did not provide the expected page for the store ZIM file" + + # ZIM file outside of store + test_content = machine.succeed("curl --fail --silent --location http://localhost:8080/content/test-outside") + print(test_content) + assert "NixOS test of kiwix-serve" in test_content, "kiwix-serve did not provide the expected page for the out-of-store ZIM file" + ''; +} diff --git a/nixos/tests/kiwix-serve/html/icon.png b/nixos/tests/kiwix-serve/html/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e69b9eaa1eb139299859045d4877df563ac40f5a GIT binary patch literal 84 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-Mg|53hWg4QS_}*fOeH~n!3+##lh0a!mAN>E gNL)@%kYK&S!oV27z_|R;EisUCPgg&ebxsLQ0FA*CD*ylh literal 0 HcmV?d00001 diff --git a/nixos/tests/kiwix-serve/html/index.html b/nixos/tests/kiwix-serve/html/index.html new file mode 100644 index 000000000000..c0c37b508812 --- /dev/null +++ b/nixos/tests/kiwix-serve/html/index.html @@ -0,0 +1,11 @@ + + + + + + NixOS kiwix-serve Test + + +

NixOS test of kiwix-serve

+ + diff --git a/pkgs/by-name/ki/kiwix-tools/package.nix b/pkgs/by-name/ki/kiwix-tools/package.nix index fa7cad399f3e..ed389c75bc98 100644 --- a/pkgs/by-name/ki/kiwix-tools/package.nix +++ b/pkgs/by-name/ki/kiwix-tools/package.nix @@ -3,6 +3,7 @@ docopt_cpp, fetchFromGitHub, gitUpdater, + nixosTests, icu, libkiwix, meson, @@ -34,6 +35,8 @@ stdenv.mkDerivation (finalAttrs: { libkiwix ]; + passthru.tests.kiwix-serve = nixosTests.kiwix-serve; + passthru.updateScript = gitUpdater { }; meta = {