Files
serverAppConfigs/docs/Dankclaw/SearXNG.md
T
Frank 90c4ac69a7 Doc audit: fix all broken links + correct stale facts across the KB
Fanned out 5 read-only doc-auditor subagents (one per service group) that
fact-checked the knowledge base against the live system + repo config,
researched official setup docs online, and verified progress. Applied:
- All 34 broken links fixed (real pages / external URLs / correct relative
  paths); 0 broken links remain. No .AGENTTRASH links (the 4 mentions are
  accurate descriptive text, not links).
- Fact-check fixes: Readarr retired (May 2026), Radarr v4->v6, Calibre
  9.2.1 live, Calibre-Web 0.6.25 live, Copyparty v1.20.21, LLM index
  ncmoe 38 + 786432 ctx, frank-default-small active (not frank-pp-worker),
  Hermes/SearXNG/Grafana hostnames, Loki/Alloy local images, Observability
  diagram (Promtail->Alloy), VLLM retired note, DFlash status, benchmark
  live-drift notes, Homepage test count 116.
2026-08-24 01:10:44 -07:00

5.3 KiB

SearXNG

SearXNG is a privacy-respecting metasearch engine. It aggregates results from many upstream search engines into one result set and serves both an HTML UI and a JSON API for agents.

Host: Dankclaw — Hostname: search.franksdank.cloud (Caddy) — Docs: https://docs.searxng.org

Facts (from research, primary source)

Item Value
Image ghcr.io/searxng/searxng:latest (or DockerHub mirror)
Port 8081 (host, on Gluetun) → 8080 (container)
Config /etc/searxng/settings.yml (bind mount)
Cache Valkey (replaces Redis) — valkey/valkey:9-alpine
Formats html, json (agent API)

VPN Routing (critical)

All SearXNG outbound traffic exits through the Gluetun VPN container. The searxng unit sets Network=gluetun.container, which makes it reuse gluetun's network namespace:

[Container]
ContainerName=searxng
Network=gluetun.container

Port publishing happens on the gluetun unit, not on searxng. See Gluetun (KB Pattern 2 is the one used here).

Boot Persistence (Quadlet)

The trio runs as quadlet units, not podman-compose, so it starts at boot. The unit files live in searxng/quadlet/ and install to ~/.config/containers/systemd/.

Unit file Service Role
searxng.network searxng-network.service DNS-enabled bridge, subnet pinned to 10.89.2.0/24
gluetun.container gluetun.service WireGuard tunnel; publishes 8081:8080
searxng-valkey.container searxng-valkey.service Valkey cache, alias valkey
searxng.container searxng.service Reuses gluetun's netns; publishes no port

gluetun and valkey join the searxng network. searxng reuses gluetun's netns and reaches valkey by name on that network. Each unit sets WantedBy=default.target. User linger is on, so the units start at boot.

Deploy with scripts/deploy-searxng-quadlet.sh dankclaw. It copies the units, daemon-reloads, enables them, and starts them in dependency order.

settings.yml Keys

use_default_settings: true
server:
  secret_key: "${SEARXNG_SECRET}"   # from .env
  base_url: "${SEARXNG_BASE_URL}"  # resolves to http://192.168.0.143:8081/
  limiter: true
  method: "POST"

search:
  default_lang: "en"
  formats:
    - html
    - json

valkey:
  url: "valkey://searxng-valkey:6379/0"

outgoing:
  request_timeout: 3.0
  max_request_timeout: 10.0
  retries: 3

Volumes

Container path Host path (convention)
/etc/searxng /opt/searxng/config
/etc/searxng/valkey (valkey container data)

Healthcheck

SearXNG responds on /healthz with 200 when ready.

Trusted Group (limiter.toml)

The trusted group lives in shared/searxng/limiter.toml, not in settings.yml. The settings.yml file only turns the limiter on. The file deploys to /opt/searxng/config/limiter.toml, mounted at /etc/searxng/limiter.toml.

Two lists control access:

  • pass_ip — an IP in this list gets unrestricted access and bypasses all bot detection.
  • trusted_proxies — if the request IP is in this list, SearXNG reads the real client IP from X-Forwarded-For (set by Caddy).

The pass_ip list holds loopback (127.0.0.1/32, ::1), the whole homelab LAN (192.168.0.0/24), and the host's private podman/slirp ranges (10.88.0.0/16, 10.89.0.0/16). An IP in pass_ip skips every bot-detection check, including the rate limiter. deploy.sh copies the file into place on each deploy.

Why so many ranges: rootless podman strips the client IP on published-port hits. A request to the published port reaches the container with a slirp/bridge source (10.88.x/10.89.x), never the real client IP. Only Caddy's X-Forwarded-For restores it. So a local subagent that hits the port directly must present a pass_ip source, or it is treated as an untrusted client. All these ranges are reachable only from the host or the LAN, never the internet.

The rate limiter's JSON cap is API_MAX = 4 requests per IP per hour. The constant is hardcoded in Python (searx/botdetection/ip_limit.py). It cannot be raised from limiter.toml. pass_ip is the only bypass. This is what lets 4 parallel subagents poll the JSON API at full rate without a 429.

Reach SearXNG through Caddy (search.dankclaw.local) for browser use. A local subagent can hit the published port directly because its slirp source is now trusted. The searxng subnet 10.89.2.0/24 is in trusted_proxies, so Caddy's X-Forwarded-For is honored. The subnet is pinned in searxng.network, so it stays stable across reboots.

Gotchas

  • The JSON API rate cap (4 per 3600s per IP) is hardcoded in Python, not settable in limiter.toml. The only bypass is pass_ip. Local subagents are covered because their slirp/loopback/LAN sources are in pass_ip.
  • The limiter stays armed for untrusted sources (a spoofed public IP still 429s). Do not disable it. Widen only pass_ip for sources you trust on this host.
  • The config file is root-owned (uid of the rootless podman namespace). Update it with a one-off sudo cp, then run systemctl --user restart searxng.service to reload.
  • base_url must match the public address or proxy rewrites break.
  • Keep the valkey cache container on the same podman network as searxng.

See Also