Files
esh-pfi-infrastructure/stacks/searxng
vh 0f748ea54e feat(searxng): move to nh3-docker, update, and expose as an MCP tool
The ana-docker instance was returning zero results for every query while
reporting healthy — 4.5 months stale (2026.4.17 against a current 2026.9.3),
its engine scrapers rotted against sites that had changed. /healthz proves
the web app answers and says nothing about whether search works, so seven
days of green sat on top of a search box that found nothing.

Moved to nh3-docker rather than updated in place, because the colo egress is
the other half of the problem: 38.120.12.42 is a datacenter address that
DuckDuckGo and Startpage CAPTCHA, while nh3-docker egresses residentially at
70.230.226.88. Same reasoning as the fleet's residential proxy for yt-dlp,
applied at the source instead of around it.

Config corrected along the way: base_url said searxng.pfi.local, a name
retired on 2026-08-19, while the environment said something else — the env
won so nothing broke and the file quietly lied. The karmasearch.videos
removal key never matched, because the engine's real name has a space.

scripts/searxng-health.sh asserts results > 0 across three unrelated
queries. That is the check that would have caught this, and the only kind
that can: the mechanism was healthy throughout.

services/searxng-mcp exposes it as `web_search` at user scope, so every
Claude Code session has it. Zero results raise rather than returning an
empty list — an empty list is indistinguishable from a broken aggregator,
which is precisely how this hid.

Old instance stopped and removed; DNS alias repointed to searxng.nh3.internal.
2026-09-03 14:07:06 -07:00
..

searxng

Privacy-respecting metasearch. Runs on nh3-docker (10.100.50.40:9996, searxng.nh3.internal), and is exposed to every Claude Code session on nh3-dev as the web_search MCP tool.

⚠ Why NH3 and not the colo

Measured 2026-09-03:

ana-docker egress   38.120.12.42    datacenter   -> DuckDuckGo/Startpage CAPTCHA
nh3-docker egress   70.230.226.88   residential  -> no gate on that account

Search engines gate datacenter ranges. Running the aggregator from the residential-egress site removes the problem at the source rather than proxying around it — the same reason the fleet keeps a residential SOCKS5 proxy on nh3-dev for yt-dlp. If NH3's egress ever changes, outgoing.proxies in conf/searxng-settings.yml has the fallback commented in place.

⚠ It is not a complete fix: brave, duckduckgo and startpage still CAPTCHA from here. google cse carries general search at ~20 results/query, and yandex, wiby, github, stackoverflow and marginalia all work. General search is therefore effectively single-engine — if google cse breaks, the instance goes quiet, which is exactly the failure below.

⚠ /healthz cannot tell you whether search works

On 2026-09-03 the old ana-docker instance was found returning zero results for every query, for an unknown number of weeks, while:

  • the container reported healthy for 7 straight days, 0 restarts;
  • the Homepage card was green;
  • /healthz returned 200 every 30 seconds.

It was running 2026.4.17 while current was 2026.9.3 — 4.5 months of engine scrapers rotting against sites that had changed their markup. :latest means "latest at pull time", and nothing re-pulls on its own.

Proven by experiment before touching anything: the new image, same settings file, same host, same query returned 20 results where the running one returned 0.

scripts/searxng-health.sh asserts the property, not the mechanism — three unrelated queries must each return results > 0. Run it after any change, and periodically; it is the only thing that catches rot.

Update

ssh infra-ops@nh3-docker 'cd /opt/docker/compose/searxng && \
  sudo docker compose pull && sudo docker compose up -d'
scripts/searxng-health.sh

The MCP tool

services/searxng-mcp/ — installed with uv tool install and registered at user scope (claude mcp add --scope user searxng searxng-mcp), so every Claude Code session gets web_search with no per-project setup.

Zero results raise an error rather than returning an empty list. An empty list is indistinguishable from a broken aggregator, and that ambiguity is what let the old instance fail silently for weeks. Same reasoning as althing's "an unreachable post office is an OUTAGE, never an empty inbox".

⚠ Written against mcp 2.x, where FastMCP became MCPServer and the v1 @app.list_tools() decorator API is gone. v1 examples fail at import with 'Server' object has no attribute 'list_tools'.

uv tool install --force alone served a cached build and silently reinstalled the old code — the installed file still had the v1 API after the source no longer did. --reinstall --no-cache was required, and the check that caught it was md5sum of source vs installed.

Secret

SEARXNG_SECRET lives in /opt/docker/compose/searxng/.env (0600, root) on the host and is vaulted at nh3-docker/searxng-secret. Never in git.