From eaffe290dd6711a1e35006d2fe09ad27614a2aac Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Tue, 23 Jun 2026 22:24:49 +0300 Subject: [PATCH] nixos/nginx: add lua option for Lua scripting support Add `services.nginx.lua.{enable,extraPackages}` to enable OpenResty's lua-nginx-module on a stock nginx. When enabled it adds the module, includes lua-resty-core, and wires up lua_package_path / lua_package_cpath (and lua_ssl_trusted_certificate) from a luajit_openresty package set built from extraPackages. When the configured package already bundles Lua (openresty), the module and bundled libraries are not re-added; only the search path is set up so its own lualib stays in use. Migrate the openresty-lua test to the new option and add an nginx-lua test covering the stock-nginx path. Assisted-by: Claude:claude-opus-4-8 --- .../manual/release-notes/rl-2611.section.md | 2 + .../services/web-servers/nginx/default.nix | 60 ++++++++++++++++++- nixos/tests/all-tests.nix | 1 + nixos/tests/nginx-lua.nix | 37 ++++++++++++ nixos/tests/openresty-lua.nix | 19 ++---- 5 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 nixos/tests/nginx-lua.nix diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 314044b1b2c6..44f2fa6aa420 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -62,6 +62,8 @@ - `boot.loader.systemd-boot` gained support for [Automatic Boot Assessment](https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/) via the new [`boot.loader.systemd-boot.bootCounting`](#opt-boot.loader.systemd-boot.bootCounting.enable) options, allowing automatic detection of and recovery from bad NixOS generations. As part of this change, boot loader entries on the ESP/XBOOTLDR partition are now named `nixos-.conf` instead of `nixos-generation-.conf`; existing entries are migrated automatically on the next `nixos-rebuild boot`/`switch`. +- `services.nginx` gained a [`lua`](#opt-services.nginx.lua.enable) option to enable Lua scripting via OpenResty's lua-nginx-module on a stock nginx, configuring `lua_package_path`/`lua_package_cpath` from the packages listed in [`services.nginx.lua.extraPackages`](#opt-services.nginx.lua.extraPackages). Use this to add Lua to a regular nginx; for the full OpenResty platform (libraries that rely on its bundled lualib, such as `lua-resty-openidc`), set `services.nginx.package` to `pkgs.openresty` instead — the option configures the Lua search path for it too. + - `security.polkit.settings` added for RFC42 style configuration of the polkitd daemon. - `services.plausible` can now again seed an initial admin user declaratively via [`services.plausible.adminUser.email`](#opt-services.plausible.adminUser.email). diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index 73efd441d23d..c213aaf652ea 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -126,6 +126,28 @@ let '') (filterAttrs (name: conf: conf.enable) cfg.proxyCachePath) ); + # openresty bundles the lua module and resty.core; stock nginx needs them added. + packageBundlesLua = p: lib.getName p == "openresty"; + luaEnv = pkgs.luajit_openresty.withPackages ( + ps: + lib.optional (cfg.lua.enable && !packageBundlesLua cfg.package) ps.lua-resty-core + ++ cfg.lua.extraPackages ps + ); + luaVersion = pkgs.luajit_openresty.luaversion; + # Lua modules install under lib/lua or share/lua depending on the package; ;; keeps nginx's defaults. + luaConfig = '' + lua_package_path '${ + lib.concatMapStringsSep ";" (s: "${luaEnv}/${s}") [ + "lib/lua/${luaVersion}/?.lua" + "lib/lua/${luaVersion}/?/init.lua" + "share/lua/${luaVersion}/?.lua" + "share/lua/${luaVersion}/?/init.lua" + ] + };;'; + lua_package_cpath '${luaEnv}/lib/lua/${luaVersion}/?.so;;'; + lua_ssl_trusted_certificate ${config.security.pki.caBundle}; + ''; + toUpstreamParameter = key: value: if builtins.isBool value then lib.optionalString value key else "${key}=${toString value}"; @@ -295,6 +317,8 @@ let server_tokens ${if cfg.serverTokens then "on" else "off"}; + ${optionalString cfg.lua.enable luaConfig} + ${cfg.commonHttpConfig} ${proxyCachePathConfig} @@ -771,7 +795,11 @@ in apply = p: p.override { - modules = lib.unique (p.modules ++ cfg.additionalModules); + modules = lib.unique ( + p.modules + ++ cfg.additionalModules + ++ lib.optional (cfg.lua.enable && !packageBundlesLua p) pkgs.nginxModules.lua + ); }; description = '' Nginx package to use. This defaults to the stable version. Note @@ -791,6 +819,36 @@ in ''; }; + lua = { + enable = mkEnableOption '' + Lua scripting in nginx via OpenResty's lua-nginx-module, + wiring up `lua_package_path`/`lua_package_cpath` for + {option}`services.nginx.lua.extraPackages`. + + Use this to add Lua to a stock nginx. For the full OpenResty platform — + required by libraries that depend on its bundled lualib (for example + `lua-resty-openidc`, which needs `resty.string` and friends) — set + {option}`services.nginx.package` to `pkgs.openresty` instead; this option + then only sets up the search path and leaves OpenResty's built-in Lua + module in place + ''; + + extraPackages = mkOption { + type = types.functionTo (types.listOf types.package); + default = ps: [ ]; + defaultText = literalExpression "ps: [ ]"; + example = literalExpression '' + ps: with ps; [ lua-resty-openidc ] + ''; + description = '' + Extra Lua packages to put on `lua_package_path` / `lua_package_cpath`, + for both stock nginx and `pkgs.openresty`. Packages are selected from + `pkgs.luajit_openresty.pkgs`. `lua-resty-core`, which the Lua module + requires to start, is added automatically. + ''; + }; + }; + logError = mkOption { default = "stderr"; type = types.str; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 09b0a4632d28..f1d216695044 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1125,6 +1125,7 @@ in nginx-etag-compression = runTest ./nginx-etag-compression.nix; nginx-globalredirect = runTest ./nginx-globalredirect.nix; nginx-http3 = import ./nginx-http3.nix { inherit pkgs runTest; }; + nginx-lua = runTest ./nginx-lua.nix; nginx-mime = runTest ./nginx-mime.nix; nginx-modsecurity = runTest ./nginx-modsecurity.nix; nginx-moreheaders = runTest ./nginx-moreheaders.nix; diff --git a/nixos/tests/nginx-lua.nix b/nixos/tests/nginx-lua.nix new file mode 100644 index 000000000000..8159882a7d5c --- /dev/null +++ b/nixos/tests/nginx-lua.nix @@ -0,0 +1,37 @@ +{ lib, ... }: +{ + name = "nginx-lua"; + + meta.maintainers = [ lib.maintainers.kranzes ]; + + nodes.machine = { + services.nginx = { + enable = true; + lua = { + enable = true; + extraPackages = p: [ + p.lua-resty-lrucache + p.lua-cjson + ]; + }; + virtualHosts."localhost".locations."/" = { + extraConfig = '' + default_type text/plain; + content_by_lua_block { + local cache = require("resty.lrucache").new(8) + cache:set("greeting", require("cjson").decode('"Hello world!"')) + ngx.say((cache:get("greeting"))) + } + ''; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("nginx") + machine.wait_for_open_port(80) + + response = machine.wait_until_succeeds("curl -fsS http://127.0.0.1/").strip() + assert response == "Hello world!", f"Expected 'Hello world!', got '{response}'" + ''; +} diff --git a/nixos/tests/openresty-lua.nix b/nixos/tests/openresty-lua.nix index 367154947371..999bd6963470 100644 --- a/nixos/tests/openresty-lua.nix +++ b/nixos/tests/openresty-lua.nix @@ -1,12 +1,4 @@ -{ pkgs, lib, ... }: -let - luaLibs = [ - pkgs.lua.pkgs.markdown - ]; - - getLuaPath = lib: "${lib}/share/lua/${pkgs.lua.luaversion}/?.lua"; - luaPath = lib.concatStringsSep ";" (map getLuaPath luaLibs); -in +{ pkgs, ... }: { name = "openresty-lua"; meta = with pkgs.lib.maintainers; { @@ -15,7 +7,7 @@ in nodes = { webserver = - { pkgs, lib, ... }: + { pkgs, ... }: { networking = { extraHosts = '' @@ -27,9 +19,10 @@ in enable = true; package = pkgs.openresty; - commonHttpConfig = '' - lua_package_path '${luaPath};;'; - ''; + lua = { + enable = true; + extraPackages = p: [ p.markdown ]; + }; virtualHosts."default.test" = { default = true;