# SearXNG Module (Podman) # Provides: Private meta-search engine running in containers # # Usage: # myModules.searxng = { # enable = true; # port = 8888; # domain = "search.example.com"; # }; { config, lib, pkgs, ... }: let cfg = config.myModules.searxng; anubisPolicy = pkgs.writeText "anubis-policy.yml" '' bots: - name: "Allow OpenSearch" action: ALLOW path_regex: ".*opensearch\\.xml.*" - name: "Catch-All" user_agent_regex: ".*" action: CHALLENGE ''; faviconsConfig = pkgs.writeText "favicons.toml" '' [favicons] cfg_schema = 1 [favicons.cache] db_url = "/var/cache/searxng/faviconcache.db" LIMIT_TOTAL_BYTES = 2147483648 [favicons.proxy.resolver_map] google = "searx.favicons.resolver.google_resolver" duckduckgo = "searx.favicons.resolver.duckduckgo_resolver" ''; in { options.myModules.searxng = { enable = lib.mkEnableOption "SearXNG meta-search engine"; port = lib.mkOption { type = lib.types.port; default = 8888; description = "Port to expose SearXNG on localhost"; }; domain = lib.mkOption { type = lib.types.str; example = "search.example.com"; description = "Public domain name for SearXNG"; }; instanceName = lib.mkOption { type = lib.types.str; default = "SearXNG"; description = "Name displayed in the search interface"; }; donations = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { }; description = "Map of donation platform names to URLs"; }; }; config = lib.mkIf cfg.enable { # Ensure Podman is enabled myModules.podman.enable = true; # Create bridge network systemd.services.create-searxng-network = { description = "Create SearXNG podman network"; after = [ "network-online.target" ]; requires = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; path = [ pkgs.podman ]; script = '' if ! podman network exists searxng-net 2>/dev/null; then podman network create searxng-net --subnet 10.89.2.0/24 fi ''; }; # Valkey Container (Cache) virtualisation.oci-containers.containers."searxng-valkey" = { image = "docker.io/valkey/valkey:alpine"; cmd = [ "valkey-server" "--save" "" "--appendonly" "no" ]; extraOptions = [ "--network=searxng-net" "--network-alias=valkey" ]; }; # SearXNG Container virtualisation.oci-containers.containers."searxng" = { image = "docker.io/searxng/searxng:latest"; environment = { SEARXNG_BASE_URL = "https://${cfg.domain}"; SEARXNG_REDIS_URL = "valkey://valkey:6379"; SEARXNG_URL_BASE = "https://${cfg.domain}"; GRANIAN_HOST = "0.0.0.0"; }; environmentFiles = [ config.sops.templates."searxng.env".path ]; extraOptions = [ "--network=searxng-net" "--network-alias=searxng" "--cap-drop=ALL" "--cap-add=CHOWN" "--cap-add=SETGID" "--cap-add=SETUID" "--cap-add=DAC_OVERRIDE" "--dns=9.9.9.9" "--dns=1.1.1.1" ]; volumes = [ "${config.sops.templates."searxng_settings.yml".path}:/etc/searxng/settings.yml:ro" "${faviconsConfig}:/etc/searxng/favicons.toml:ro" "searxng-cache:/var/cache/searxng" ]; dependsOn = [ "searxng-valkey" ]; }; # Anubis Container (AI Firewall) virtualisation.oci-containers.containers."searxng-anubis" = { image = "ghcr.io/techarohq/anubis:latest"; ports = [ "127.0.0.1:${toString cfg.port}:8080" ]; environment = { TARGET = "http://searxng:8080"; BIND = ":8080"; POLICY_FNAME = "/etc/anubis/policy.yml"; }; extraOptions = [ "--network=searxng-net" "--network-alias=searxng-anubis" ]; volumes = [ "${anubisPolicy}:/etc/anubis/policy.yml:ro" ]; dependsOn = [ "searxng" ]; }; # SOPS templates sops.templates."searxng.env" = { content = '' SEARXNG_SECRET_KEY=${config.sops.placeholder.searxng_secret_key} ''; }; sops.templates."searxng_settings.yml" = { content = '' use_default_settings: true general: debug: false instance_name: "${cfg.instanceName}" contact_url: false issue_url: false donation_url: ${if cfg.donations ? "Monero" then "\"${cfg.donations.Monero}\"" else "false"} donations: ${lib.concatStringsSep "\n " ( lib.mapAttrsToList (name: url: "${name}: \"${url}\"") cfg.donations )} outgoing: request_timeout: 10.0 connect_timeout: 6.0 max_retry_count: 3 enable_ipv6: false search: safe_search: 0 favicon_resolver: "google" autocomplete: "google" default_lang: "en-US" formats: - html - json server: port: 8080 bind_address: "0.0.0.0" secret_key: "${config.sops.placeholder.searxng_secret_key}" limiter: true image_proxy: true public_instance: true default_http_headers: Content-Security-Policy: "upgrade-insecure-requests; default-src 'none'; script-src 'self'; style-src 'self' 'sha256-/ldGxQqxNIMRftg3AGsPF+F281wiBPECUDcL2RJkxdU='; form-action 'self' https://github.com/searxng/searxng/issues/new; font-src 'self'; frame-ancestors 'self'; img-src 'self' data:; connect-src 'self' https://overpass-api.de; manifest-src 'self'" ui: default_theme: simple default_theme_style: dark static_use_hash: true redis: url: valkey://valkey:6379/0 ''; }; # Secret definitions sops.secrets.searxng_secret_key = { }; }; }