1{
2 options,
3 config,
4 lib,
5 pkgs,
6 ...
7}:
8
9with lib;
10
11let
12 runDir = "/run/searx";
13
14 cfg = config.services.searx;
15
16 settingsFile = pkgs.writeText "settings.yml" (builtins.toJSON cfg.settings);
17
18 faviconsSettingsFile = (pkgs.formats.toml { }).generate "favicons.toml" cfg.faviconsSettings;
19 limiterSettingsFile = (pkgs.formats.toml { }).generate "limiter.toml" cfg.limiterSettings;
20
21 generateConfig = ''
22 cd ${runDir}
23
24 # write NixOS settings as JSON
25 (
26 umask 077
27 ${pkgs.envsubst}/bin/envsubst < ${settingsFile} > settings.yml
28 )
29 '';
30
31 settingType =
32 with types;
33 (oneOf [
34 bool
35 int
36 float
37 str
38 (listOf settingType)
39 (attrsOf settingType)
40 ])
41 // {
42 description = "JSON value";
43 };
44in
45{
46 options = {
47 services.searx = {
48 enable = mkOption {
49 type = types.bool;
50 default = false;
51 relatedPackages = [ "searx" ];
52 description = "Whether to enable Searx, the meta search engine.";
53 };
54
55 domain = mkOption {
56 type = types.str;
57 description = ''
58 The domain under which searxng will be served.
59 Right now this is only used with the configureNginx option.
60 '';
61 };
62
63 environmentFile = mkOption {
64 type = types.nullOr types.path;
65 default = null;
66 description = ''
67 Environment file (see {manpage}`systemd.exec(5)` "EnvironmentFile=" section for the syntax) to define variables for Searx.
68 This option can be used to safely include secret keys into the Searx configuration.
69 '';
70 };
71
72 redisCreateLocally = mkOption {
73 type = types.bool;
74 default = false;
75 description = ''
76 Configure a local Redis server for SearXNG.
77 This is required if you want to enable the rate limiter and bot protection of SearXNG.
78 '';
79 };
80
81 settings = mkOption {
82 type = types.submodule {
83 freeformType = settingType;
84 imports = [
85 (mkRenamedOptionModule [ "redis" ] [ "valkey" ])
86 ];
87 };
88 default = { };
89 example = literalExpression ''
90 {
91 server.port = 8080;
92 server.bind_address = "0.0.0.0";
93 server.secret_key = "$SEARX_SECRET_KEY";
94
95 engines = [ {
96 name = "wolframalpha";
97 shortcut = "wa";
98 api_key = "$WOLFRAM_API_KEY";
99 engine = "wolframalpha_api";
100 } ];
101 }
102 '';
103 description = ''
104 Searx settings.
105 These will be merged with (taking precedence over) the default configuration.
106 It's also possible to refer to environment variables (defined in [](#opt-services.searx.environmentFile)) using the syntax `$VARIABLE_NAME`.
107
108 ::: {.note}
109 For available settings, see the Searx [docs](https://docs.searxng.org/admin/settings/index.html).
110 :::
111 '';
112 };
113
114 settingsFile = mkOption {
115 type = types.path;
116 default = "${runDir}/settings.yml";
117 description = ''
118 The path of the Searx server settings.yml file.
119 If no file is specified, a default file is used (default config file has debug mode enabled).
120
121 ::: {.note}
122 Setting this options overrides [](#opt-services.searx.settings).
123 :::
124
125 ::: {.warning}
126 This file, along with any secret key it contains, will be copied into the world-readable Nix store.
127 :::
128 '';
129 };
130
131 faviconsSettings = mkOption {
132 type = types.attrsOf settingType;
133 default = { };
134 example = literalExpression ''
135 {
136 favicons = {
137 cfg_schema = 1;
138 cache = {
139 db_url = "/var/cache/searx/faviconcache.db";
140 HOLD_TIME = 5184000;
141 LIMIT_TOTAL_BYTES = 2147483648;
142 BLOB_MAX_BYTES = 40960;
143 MAINTENANCE_MODE = "auto";
144 MAINTENANCE_PERIOD = 600;
145 };
146 };
147 }
148 '';
149 description = ''
150 Favicons settings for SearXNG.
151
152 ::: {.note}
153 For available settings, see the SearXNG
154 [schema file](https://github.com/searxng/searxng/blob/master/searx/favicons/favicons.toml).
155 :::
156 '';
157 };
158
159 limiterSettings = mkOption {
160 type = types.attrsOf settingType;
161 default = { };
162 example = literalExpression ''
163 {
164 real_ip = {
165 x_for = 1;
166 ipv4_prefix = 32;
167 ipv6_prefix = 56;
168 }
169 botdetection.ip_lists.block_ip = [
170 # "93.184.216.34" # example.org
171 ];
172 }
173 '';
174 description = ''
175 Limiter settings for SearXNG.
176
177 ::: {.note}
178 For available settings, see the SearXNG [schema file](https://github.com/searxng/searxng/blob/master/searx/limiter.toml).
179 :::
180 '';
181 };
182
183 package = mkPackageOption pkgs "searxng" { };
184
185 configureUwsgi = mkOption {
186 type = types.bool;
187 default = false;
188 description = ''
189 Whether to run searx in uWSGI as a "vassal", instead of using its
190 built-in HTTP server. This is the recommended mode for public or
191 large instances, but is unnecessary for LAN or local-only use.
192
193 ::: {.warning}
194 The built-in HTTP server logs all queries by default.
195 :::
196 '';
197 };
198
199 configureNginx = mkOption {
200 type = types.bool;
201 default = false;
202 description = ''
203 Whether to configure nginx as an frontend to uwsgi.
204 '';
205 };
206
207 uwsgiConfig = mkOption {
208 inherit (options.services.uwsgi.instance) type;
209 default = {
210 http = ":8080";
211 };
212 example = literalExpression ''
213 {
214 disable-logging = true;
215 http = ":8080"; # serve via HTTP...
216 socket = "/run/searx/searx.sock"; # ...or UNIX socket
217 chmod-socket = "660"; # allow the searx group to read/write to the socket
218 }
219 '';
220 description = ''
221 Additional configuration of the uWSGI vassal running searx. It
222 should notably specify on which interfaces and ports the vassal
223 should listen.
224 '';
225 };
226 };
227 };
228
229 imports = [
230 (mkRenamedOptionModule [ "services" "searx" "configFile" ] [ "services" "searx" "settingsFile" ])
231 (mkRenamedOptionModule [ "services" "searx" "runInUwsgi" ] [ "services" "searx" "configureUwsgi" ])
232 ];
233
234 config = mkIf cfg.enable {
235 environment = {
236 etc = {
237 "searxng/favicons.toml" = lib.mkIf (cfg.faviconsSettings != { }) {
238 source = faviconsSettingsFile;
239 };
240 "searxng/limiter.toml" = lib.mkIf (cfg.limiterSettings != { }) {
241 source = limiterSettingsFile;
242 };
243 };
244 systemPackages = [ cfg.package ];
245 };
246
247 services = {
248 nginx = lib.mkIf cfg.configureNginx {
249 enable = true;
250 virtualHosts."${cfg.domain}".locations = {
251 "/" = {
252 recommendedProxySettings = true;
253 recommendedUwsgiSettings = true;
254 uwsgiPass = "unix:${config.services.uwsgi.instance.vassals.searx.socket}";
255 extraConfig = # nginx
256 ''
257 uwsgi_param HTTP_HOST $host;
258 uwsgi_param HTTP_CONNECTION $http_connection;
259 uwsgi_param HTTP_X_SCHEME $scheme;
260 uwsgi_param HTTP_X_SCRIPT_NAME ""; # NOTE: When we ever make the path configurable, this must be set to anything not "/"!
261 uwsgi_param HTTP_X_REAL_IP $remote_addr;
262 uwsgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
263 '';
264 };
265 "/static/".alias = lib.mkDefault "${cfg.package}/share/static/";
266 };
267 };
268
269 redis.servers.searx = lib.mkIf cfg.redisCreateLocally {
270 enable = true;
271 user = "searx";
272 port = 0;
273 };
274
275 searx = {
276 configureUwsgi = lib.mkIf cfg.configureNginx true;
277 settings = {
278 # merge NixOS settings with defaults settings.yml
279 use_default_settings = mkDefault true;
280 server.base_url = lib.mkIf cfg.configureNginx "http${
281 lib.optionalString (lib.any lib.id (
282 with config.services.nginx.virtualHosts."${cfg.domain}";
283 [
284 onlySSL
285 addSSL
286 forceSSL
287 ]
288 )) "s"
289 }://${cfg.domain}/";
290 valkey.url = lib.mkIf cfg.redisCreateLocally "unix://${config.services.redis.servers.searx.unixSocket}";
291 };
292 };
293
294 uwsgi = mkIf cfg.configureUwsgi {
295 enable = true;
296 plugins = [ "python3" ];
297 instance.type = "emperor";
298 instance.vassals.searx = {
299 type = "normal";
300 strict = true;
301 immediate-uid = "searx";
302 immediate-gid = "searx";
303 lazy-apps = true;
304 enable-threads = true;
305 module = "searx.webapp";
306 env = [
307 "SEARXNG_SETTINGS_PATH=${cfg.settingsFile}"
308 ];
309 buffer-size = 32768;
310 pythonPackages = _: [ cfg.package ];
311 }
312 // lib.optionalAttrs cfg.configureNginx {
313 socket = "/run/searx/uwsgi.sock";
314 chmod-socket = "660";
315 }
316 // cfg.uwsgiConfig;
317 };
318 };
319
320 systemd.services = {
321 nginx = lib.mkIf cfg.configureNginx {
322 serviceConfig.SupplementaryGroups = [ "searx" ];
323 };
324
325 searx-init = {
326 description = "Initialise Searx settings";
327 serviceConfig = {
328 Type = "oneshot";
329 RemainAfterExit = true;
330 User = "searx";
331 RuntimeDirectory = "searx";
332 RuntimeDirectoryMode = "750";
333 RuntimeDirectoryPreserve = "yes";
334 }
335 // optionalAttrs (cfg.environmentFile != null) {
336 EnvironmentFile = cfg.environmentFile;
337 };
338 script = generateConfig;
339 };
340
341 searx = mkIf (!cfg.configureUwsgi) {
342 description = "Searx server, the meta search engine.";
343 wantedBy = [ "multi-user.target" ];
344 requires = [ "searx-init.service" ];
345 after = [
346 "searx-init.service"
347 "network.target"
348 ];
349 serviceConfig = {
350 User = "searx";
351 Group = "searx";
352 ExecStart = lib.getExe cfg.package;
353 }
354 // optionalAttrs (cfg.environmentFile != null) {
355 EnvironmentFile = cfg.environmentFile;
356 };
357 environment = {
358 SEARXNG_SETTINGS_PATH = cfg.settingsFile;
359 };
360 };
361
362 uwsgi = mkIf cfg.configureUwsgi {
363 requires = [ "searx-init.service" ];
364 after = [ "searx-init.service" ];
365 restartTriggers = [
366 cfg.package
367 cfg.settingsFile
368 ]
369 ++ lib.optional (cfg.environmentFile != null) cfg.environmentFile;
370 };
371 };
372
373 users = {
374 groups.searx = { };
375 users.searx = {
376 description = "Searx daemon user";
377 group = "searx";
378 isSystemUser = true;
379 };
380 };
381 };
382
383 meta.maintainers = with maintainers; [
384 SuperSandro2000
385 _999eagle
386 ];
387}