Files
serverAppConfigs/serverAppConfigs-docs/Dankclaw/SearXNG.md
Frank 327e78b950 docs(vault): destroy the dead weight — 53 pages and the change diary
The vault was 155 pages / 31,210 lines, most of it measurement ledgers for
payloads that no longer run. A throughput figure for a runtime nobody operates is
not a record: AI moves faster than the vault, so the number is wrong before
anyone reads it, and a wrong number in a runbook gets believed. Per the new
purpose test in `AGENTS.md` → *Documentation Policy*, it is destroyed.

Gone (`.AGENTTRASH/20261019-destruction/`, KILL-LIST.txt is the manifest):
- the `frank-medium` endpoint and its five era pages, `frank-npu`, FastFlowLM,
  the ikllama/kingjones/dealign/frank-redo eras, the ling/swift15/freetoken/ubatch
  campaign pages, qwen3.8 spec-matrix and strix-optimizations, the two
  "Running Qwen3.6 …" pages;
- the whole `Pipeline Parallelism/` folder (no live script passes a pipeline flag)
  and the multi-device/x4-pair/hybrid/offload experiment pages;
- every vLLM page — no vLLM endpoint exists on this fleet;
- `LLM/deployment.md` (`Architecture/Deployment Flow.md` is the test-referenced
  owner) and the external surveys (SGLang, Lemonade, llama.cpp forks/diff,
  self-hosted app census, the TTS survey) that were obsolete by construction;
- `log.md`, 2,380 lines. Git holds the chronology; a causal law belongs to the
  page that owns the thing it governs, never to a diary.

Survivors: 101 pages, 17,542 lines, 0 OKF violations, 0 unresolved links. Eras
tables keep the window, payload, runtime, reason and rollback pointer; the rate
columns are blanked. The live shapes stay verbatim — those numbers are what the
deployer and `models.yml` consume.
2026-10-19 10:56:37 -07:00

18 KiB

type, description, tags, timestamp
type description tags timestamp
reference SearXNG is a privacy-respecting metasearch engine.
dankclaw
searxng
2026-09-03

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 (dankclaw/searxng/quadlet/searxng.container:19; the live container's image reference is exactly that — podman inspect searxng 2026-10-18). Running build: 2026.9.25+12f8b6515 (GET /config)
Port SearXNG listens on 8080 inside Gluetun's network namespace; the host sees 8081, published by the gluetun unit (dankclaw/searxng/quadlet/gluetun.container:31; ss -ltn shows *:8081, curl http://127.0.0.1:8081/ → 200, 2026-10-18)
Config /etc/searxng/settings.yml (bind mount of /opt/searxng/config, searxng.container:22)
Cache Valkey (replaces Redis) — valkey/valkey:9-alpine, container searxng-valkey, publishes nothing (see Valkey)
Formats html, json (agent API) — shared/searxng/settings.yml:18-20; curl "http://127.0.0.1:8081/search?q=…&format=json" → 200 application/json (2026-10-18)

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 dankclaw/searxng/quadlet/ and install to ~/.config/containers/systemd/.

The units live in dankclaw/searxng/quadlet/ and install to ~/.config/containers/systemd/ on Dankclaw (verified live):

Unit file Service Role
searxng.network generated network service DNS-enabled bridge (dns_enabled: true, podman network inspect systemd-searxng 2026-10-18); the created network is named systemd-searxng, subnet pinned to 10.89.2.0/24 (searxng.network:16)
gluetun.container gluetun.service WireGuard tunnel (env-file secrets); publishes 8081:8080 for SearXNG and 8082:8085 for Marreta (gluetun.container:31-32) — SearXNG's is the only port in the search trio, but the tunnel unit carries two, and both are live (ss -ltn 2026-10-18)
searxng-valkey.container searxng-valkey.service Valkey cache on the searxng network, alias valkey (see Valkey)
searxng.container searxng.service Network=gluetun.container — joins the tunnel's netns, so it publishes no port of its own; reached through gluetun's 8081

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 — a wrapper that execs scripts/deploy.sh dankclaw --service searxng (scripts/deploy-searxng-quadlet.sh:11). The dispatcher copies the config, installs the three units plus the network, daemon-reloads, and starts them in dependency order: network → gluetun → valkey → searxng (scripts/deploy-remote.sh:583-593).

settings.yml Keys

use_default_settings: true
server:
  secret_key: "${SEARXNG_SECRET}"   # rendered at deploy by scripts/searxng-render-settings.py
  base_url: "${SEARXNG_BASE_URL}"   # same renderer; SearXNG does NOT expand env vars itself
  limiter: true
  method: "POST"

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

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

outgoing:
  request_timeout: 5.0
  max_request_timeout: 15.0
  retries: 1                     # avoid amplifying upstream 429/CAPTCHA responses

# Anonymous, bounded default aggregation. Disabled engines remain configured
# for explicit/on-demand selection; see SearXNG Providers for the policy.
engines:
  - name: bing
    disabled: false
    weight: 2
    paging: false
    timeout: 5.0
  - name: duckduckgo
    disabled: false
    weight: 2
  - name: kagi
    engine: kagi
    categories: [general, web]
    shortcut: kg
    api_key: "${KAGI_API_KEY}"      # rendered at deploy from /opt/searxng/.env
    kagi_categ: search
    disabled: false
    weight: 2
    paging: false                  # one API credit per aggregated query
    timeout: 8.0
  - name: google cse
    disabled: true
  - name: brave
    disabled: true
  - name: startpage
    disabled: true

The block above is the repo file (shared/searxng/settings.yml, 21 engine stanzas) the deployed render at /opt/searxng/config/settings.yml differs only in the baked secret_key, base_url and KAGI_API_KEY values, and the running engine set is censused below.

Volumes

Container path Host path Proof
/etc/searxng (searxng) /opt/searxng/config searxng.container:22; live dir holds settings.yml + limiter.toml (ls -la /opt/searxng/config 2026-10-18)
/data (valkey container) /opt/searxng/valkey dankclaw/searxng/quadlet/searxng-valkey.container:20 — this is a mount of the valkey container, not of /etc/searxng/valkey

Healthcheck

SearXNG responds on /healthz with 200 when ready — re-checked on 2026-10-18: curl -s -o /dev/null -w '%{http_code} %{content_type}' http://127.0.0.1:8081/healthz → 200 text/plain. The unit itself carries no HealthCmd, so podman ps shows searxng as plain Up (2026-10-18); the homepage card probes 127.0.0.1:8081.

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 (server.limiter: true, shared/searxng/settings.yml:11 — and the running instance confirms it: GET /config reports limiter.enabled: true, 2026-10-18). The file deploys to /opt/searxng/config/limiter.toml (scripts/deploy-remote.sh:211), mounted at /etc/searxng/limiter.toml. The deployed copy is byte-identical to the repo file today (diff clean, 2026-10-18).

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) — shared/searxng/limiter.toml:54-60. An IP in pass_ip skips every bot-detection check, including the rate limiter: ip_lists "outranks every other method" (limiter.toml:41-45). deploy.sh copies the file into place on each deploy (scripts/deploy-remote.sh:208-211).

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.franksdank.cloud, guarded) for browser use: the vhost challenges through anubis:8923 (dankclaw/Caddyfile:102-103) and the post-auth listener :8017 routes it to 192.168.0.143:8081 (dankclaw/Caddyfile:361). A local subagent can hit the published port directly because its slirp/loopback/LAN sources are in pass_ip. The searxng network (systemd-searxng, 10.89.2.0/24) is in trusted_proxies (shared/searxng/limiter.toml:22-29), so Caddy's X-Forwarded-For is honored. The subnet is pinned in searxng.network:16, so it stays stable across reboots — that is what keeps the trusted_proxies entry meaningful.

Unblocking Google & Search Providers

The current request policy allows a modest amount of VPN latency (5.0s request, 15.0s maximum — shared/searxng/settings.yml:26-27) while limiting retries to one (:32). This can recover slow providers, but it cannot fix an upstream CAPTCHA or 429 caused by the shared VPN exit IP. SearXNG queries every engine it has enabled from one Gluetun VPN exit IP: the running build ships 262 engines and 75 are enabled (curl http://127.0.0.1:8081/config → engines[], 2026-10-18). The rate-sensitive engines (Google, DuckDuckGo) then answer with reCAPTCHA / 429. The default provider policy therefore favors anonymous Bing and DuckDuckGo (both enabled: true in /config), disables observed CAPTCHA/429 providers from normal aggregation (google, google cse, google news, google scholar, the whole brave.* family: enabled: false on 2026-10-18), and disables pagination for those engines to reduce upstream request volume. SearXNG suspends offending engines — which is what shows up as "Google went unavailable". The default suspension is shorter than the folklore: see the table in SearXNG Providers. This is upstream, not local.

Fixes (cheapest first): tune the outgoing block (request_timeout retry_on_http_error, suspended_times), duplicate the google engine under a fresh name so a CAPTCHA ban on one leaves the other returning results, and — if Google still re-blocks — route the Google engines at a clean-IP proxy per-engine proxies in settings.yml. See SearXNG Providers for the full root cause, the suspend-model table, the engines: config, and the /json verification recipe.

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.
  • Agent-API contract (verified live 2026-08-31, re-verified 2026-10-18): the guarded path (search.franksdank.cloud) answers non-browser API clients with Anubis' JS-challenge page at HTTP 200 — a status-code-only client silently gets HTML where it expected JSON (curl with a JSON query through the vhost returned 200 text/html, 2026-10-18). Check content-type, not just status. There is deliberately no way for an API client through the guard (JS challenge + UA deny-list); the agent path is the direct published port http://192.168.0.143:8081/search?q=…&format=json from trusted sources — same instance, VPN-bound egress via Gluetun, unreachable from outside the LAN by construction (no public route to the LAN IP). No second agent-only instance is needed; a duplicate would double upstream request volume from the same rate-sensitive VPN exit IP for zero benefit.
  • The config dir is owned by uid 100976 — the searxng container's mapped root inside the rootless user namespace, not a host root account (ls -la /opt/searxng/config 2026-10-18: both files 100976 100976). The official path is deploy.sh --service searxng: it copies the repo settings and renders the ${VAR} placeholders via scripts/searxng-render-settings.py from /opt/searxng/.env (scripts/deploy-remote.sh:208-216). A hotfix can still hand-copy with sudo cp, but remember the renderer: an unrendered copy silently serves the literal placeholders (before 2026-09-26 the live secret_key literally was "${SEARXNG_SECRET}" and nobody noticed). Today's deployed file carries no unrendered placeholder — the only ${ left in it is inside the header comment (grep -n '[$]{' /opt/searxng/config/settings.yml, 2026-10-18).
  • base_url must match the public address or proxy rewrites break — the deployed value does not: SEARXNG_BASE_URL in /opt/searxng/.env is set so that the rendered config reads base_url: "http://192.168.0.143:8081/" (/opt/searxng/config/settings.yml:10, 2026-10-18), i.e. the published port on the LAN IP, not https://search.franksdank.cloud. Browser use through the vhost still works because Caddy rewrites Host, but absolute links SearXNG builds point at the LAN address. The unit also passes SEARXNG_BASE_URL into the container (searxng.container:21), and that is one of the variables SearXNG substitutes at load, so both paths carry the same LAN-IP value. Flagged, not fixed here: it is a config change, out of scope for docs.
  • Keep the valkey cache container on the network SearXNG can actually reach — Valkey Gotchas: SearXNG itself has no network of its own (it borrows Gluetun's netns), so the network that matters is the one Gluetun and searxng-valkey share.

Kagi (paid API engine)

Added 2026-09-26 (user-ordered) and still enabled: the running build ships a native kagi engine module (searx/engines/kagi.py at the pinned commit 12f8b6515) which sets Authorization: Bearer <key> (kagi.py:103) and issues POST https://kagi.com/api/v1/search (base_url + /api/v1/search, kagi.py:76,104,131) and declares require_api_key: True (kagi.py:63).

  • Enabled: weight: 2 alongside bing/duckduckgo, categories [general, web], shortcut kg, kagi_categ: search (shared/searxng/settings.yml:66-77); live /config reports kagi with enabled: true, categories general, web (2026-10-18).
  • Cost control: paging: false — exactly one API credit per aggregated query; timeout: 8.0 so a slow paid query is not wasted (scrapers use 5.0) (shared/searxng/settings.yml:74-77; upstream paging default is True kagi.py:67, so the false here is a real override).
  • Key handling: the repo file keeps api_key: "${KAGI_API_KEY}" deploy-remote.sh bakes the real key into the live settings.yml scripts/searxng-render-settings.py, reading /opt/searxng/.env (owner dank, mode 0600 — ls -l /opt/searxng/.env 2026-10-18; its key names are the WireGuard set plus SEARXNG_SECRET, SEARXNG_BASE_URL, KAGI_API_KEY). The key never enters git and never appears in any process argv. SearXNG does not expand arbitrary ${VAR} placeholders in settings.yml — it substitutes only a fixed set of documented environment variables (SEARXNG_SECRET, SEARXNG_BASE_URL, SEARXNG_VALKEY_URL, …; see the header comments in upstream searx/settings.yml at the pinned commit), and ${KAGI_API_KEY} is not one of them. Without the renderer the engine fails setup() and is dropped at load (kagi.py:91 returns the error when the key is empty). The deployed file has no unrendered placeholder left (2026-10-18).
  • Immune to the VPN-exit-IP problem: Kagi authenticates, so it does not care about the shared Gluetun exit IP reputation (see SearXNG Providers).
  • Verified live 2026-09-26: direct API probe through the gluetun netns (200, data.search[] shape), then a live aggregated query returned 29/29 results tagged kagi, zero unresponsive engines. Re-checked 2026-10-18 only as far as the config goes (engine enabled, key rendered); no paid query was fired for this audit.

Live engine census (2026-10-18)

GET http://127.0.0.1:8081/config answers engines[] for the running instance (200 today; the endpoint would be blocked on a public instance, and public_instance: false here). Counted from that response:

  • 262 engines in the build, 75 enabled, categories of the enabled set led by other (12), it (11), general (9), videos (8), stock images (7).
  • Enabled and matching the policy page: bing, duckduckgo, wikipedia, kagi github, mdn, stackoverflow, askubuntu, superuser, docker hub, pypi arxiv, pubmed, semantic scholar, reuters, bing news, duckduckgo news wikinews, bing images, duckduckgo images/videos/news, wikicommons.* youtube, vimeo, dailymotion, bing videos.
  • Disabled, as configured: google, google cse, google news, google scholar brave, brave.images, brave.news, brave.videos, qwant.
  • The three startpage stanzas in shared/searxng/settings.yml:113-124 target engines that no longer exist in this build — /config lists no startpage startpage images or startpage news, so those overrides are no-ops kept as intent. (labelled 2026-10-18)

Citations

  • dankclaw/searxng/quadlet/searxng.container, gluetun.container, searxng-valkey.container, searxng.network
  • shared/searxng/settings.yml, shared/searxng/limiter.toml
  • scripts/deploy-searxng-quadlet.sh:11, scripts/deploy-remote.sh:208-216,583-593
  • dankclaw/Caddyfile:102-103,361 (guarded vhost → post-auth → 192.168.0.143:8081)
  • Host evidence 2026-10-18: podman ps, podman inspect searxng, podman network inspect systemd-searxng, ss -ltn, curl 127.0.0.1:8081/ → 200, /healthz → 200, /config → 200 (engine census), /search?format=json → 200 application/json, diff shared/searxng/limiter.toml /opt/searxng/config/limiter.toml (identical), systemctl --user is-enabled/is-active gluetun searxng searxng-valkey

See Also