Give Your Local AI Private Web Search With SearXNG

Wire Open WebUI to a self-hosted SearXNG metasearch engine so your local LLM answers current-events questions with citations — no cloud, no API keys.

On this page
  1. Why a metasearch engine, and why self-host it
  2. Deploy SearXNG
  3. Wire Open WebUI to it — and the silent traps
  4. Trap 1: in recent versions, the database beats the environment
  5. Trap 2: the model needs the right calling mode, or search silently no-ops
  6. Trap 3: leave “search query generation” off for small models
  7. The speed reality (so you’re not surprised)
  8. Don’t let it fail silently
  9. What’s next

I love running a local model. I can ask it anything, it never phones home, and it costs me nothing per question. But it has one embarrassing blind spot: it cannot see today. Ask my Ollama model who won a match last night or what version of some tool just shipped, and it either shrugs or — worse — invents a confident, wrong answer. A model that can’t see the present is only half a tool.

The fix is to give it eyes: a search engine it can query, fetch pages from, and cite. And because this is a homelab and I care about not leaking every question I ask to a search company, I want that engine to be mine. This post wires Open WebUI — the chat front-end most of us put on top of Ollama — to a self-hosted SearXNG metasearch engine, so answers come back with citations and nothing sensitive ever leaves my network but the search itself. This is the first stop in the Self-Host the Apps You Actually Use series, and it’s my favorite because the payoff is so immediate.

Here’s the whole pipeline we’re about to build:

Your question, answered with live sources — all on your hardwareYouOpen WebUI+ OllamaSearXNGmetasearchEngine AEngine BEngine Cpublic search enginescited answer [1][2]
First: make these values your own

Every address, container ID, and model name below is an example. Before you copy a command, swap in your own values: 10.0.0.20 is a stand-in for your SearXNG host’s IP, 10.0.0.10 for your Open WebUI host, 8888 is the port I happened to use, and model names like qwen2.5:3b should be whatever you have pulled in Ollama. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.


Why a metasearch engine, and why self-host it

A metasearch engine doesn’t crawl the web itself. It takes your query, forwards it to a pile of existing search engines at once, merges what they return, and hands you a clean, ad-free, tracker-free list. SearXNG is the best-known open-source one, and it’s a joy to run at home for two reasons.

First, no API keys. Every “give your LLM web search” tutorial that reaches for a commercial search API asks you to paste a billing-attached key and start metering your curiosity. SearXNG needs none — it speaks to public engines the same way your browser does.

Second, no query leakage. When your model searches through SearXNG, the questions fan out to public engines but never accumulate into a profile at a single company tied to your account. For a machine whose whole selling point is privacy, sending every question to a cloud search API would rather defeat the purpose.

The catch — and it’s the whole reason this post exists — is that the wiring between Open WebUI and SearXNG has a few silent failure modes that make everything look connected while quietly doing nothing. We’ll deploy SearXNG first, then walk the wiring carefully.


Deploy SearXNG

I run SearXNG natively (no Docker) in a small Proxmox LXC container — it’s a lightweight Python app and doesn’t need a container-in-a-container. The official step-by-step install is the source of truth; here’s the shape of it with the two homelab-specific settings that matter.

1Create a container with internet access5 min

A Debian LXC with 2 CPU cores, 1 GB of RAM, and 10 GB of disk is plenty. SearXNG must be able to reach the internet — it’s the thing doing the searching — so make sure the container gets a normal network setup and can resolve DNS. Everything after this runs inside that container.

2Install SearXNG into a virtualenv10 min

Following the official install, you create a dedicated searxng user, clone the source, and install it into a Python virtual environment. A virtualenv is just an isolated folder of Python packages so SearXNG’s dependencies never collide with the system’s.

Install SearXNG from source (abridged — see official docs)

# as root, create the service user and directories
sudo useradd --system --home-dir /usr/local/searxng --shell /bin/bash searxng

# as the searxng user, clone and install into a venv
git clone https://github.com/searxng/searxng /usr/local/searxng/searxng-src
python3 -m venv /usr/local/searxng/searx-pyenv
. /usr/local/searxng/searx-pyenv/bin/activate
pip install --use-pep517 --no-build-isolation -e /usr/local/searxng/searxng-src
3Enable the JSON API — this is the one that bites you2 min

By default SearXNG only returns HTML — a web page for humans. Open WebUI needs JSON — structured data for machines. If you forget this single line, Open WebUI’s searches come back with a 403 Forbidden and you’ll chase phantom network problems for an hour. Both the SearXNG search API docs and Open WebUI’s own SearXNG guide call this out.

Edit /etc/searxng/settings.yml and set the formats and a couple of LAN-friendly options:

/etc/searxng/settings.yml — the parts that matter

use_default_settings: true

search:
formats:
  - html
  - json          # REQUIRED, or Open WebUI gets empty/403 results

server:
limiter: false    # LAN-only instance: skip the Redis/Valkey rate-limiter
public_instance: false
secret_key: "CHANGE_ME"   # generate a random value inside the container
Two settings that are safe only because this is private

Turning the limiter off skips SearXNG’s abuse protection, which is fine for an instance only you can reach on a trusted subnet — but never do it on anything internet-facing. Generate the secret_key with something like openssl rand -hex 32 inside the container and keep it there.

4Serve it with granian and log the queries5 min

SearXNG’s current recommended app server is granian (the replacement for the older uWSGI). Run it under a systemd service so it starts on boot. One flag is worth adding deliberately: --access-log. Granian doesn’t log requests by default, and being able to see your searches land is how you prove the whole thing works.

Run SearXNG under granian

granian --interface wsgi --host 0.0.0.0 --port 8888 --access-log searx.webapp:app
Leave the worker count alone

It’s tempting to crank up --workers, but SearXNG’s docs specifically advise against it — more workers means more resource use and can trip the public engines’ bot detection. The defaults are tuned for this. Stick with them.

Confirm the JSON API answers before you touch Open WebUI. From inside the container:

Prove the JSON API works

curl -s -o /dev/null -w '%{http_code}
' "http://127.0.0.1:8888/search?q=proxmox&format=json"
# expect: 200

That 200 is your green light. (When I re-checked my live instance while writing this, that’s exactly what it returned.)

Known-normal log noise, don't chase it

On a fresh server IP, engines like Startpage and DuckDuckGo may temporarily CAPTCHA-suspend themselves — SearXNG just serves results from the rest of the pool and self-heals. You may also see a cosmetic botdetection: X-Forwarded-For nor X-Real-IP header is set warning on direct connections. Neither is a failure.


Wire Open WebUI to it — and the silent traps

This is where the hour goes, so let’s go slowly. On paper it’s three settings. In practice, three separate things can make the globe icon in your chat do absolutely nothing while reporting no error at all.

The official Open WebUI SearXNG docs give you the three settings:

Open WebUI web-search settings

ENABLE_WEB_SEARCH=True
WEB_SEARCH_ENGINE=searxng
SEARXNG_QUERY_URL=http://10.0.0.20:8888/search?q=<query>

The literal string <query> in that URL is a placeholder Open WebUI fills in — leave it exactly as written. Now the three traps, in the order they bit me.

Trap 1: in recent versions, the database beats the environment

Newer Open WebUI (I was on the 0.10 line) stores settings in a database table, and on an existing install those rows win over environment variables. You can set ENABLE_WEB_SEARCH in your service file all day; if a config row already exists, your variable is politely ignored. The reliable fix is to set it in the running app’s Admin Panel → Settings → Web Search, which writes the database directly. Set the engine to searxng and paste the query URL there.

Verify against the right surface

Don’t confirm your settings by hitting the unauthenticated /api/config endpoint — it only exposes auth-related flags, not the web-search config. I wasted real time “verifying” a setting that endpoint never reports. Check it in the admin panel instead.

Trap 2: the model needs the right calling mode, or search silently no-ops

This is the subtle one. In the version I ran, Open WebUI only runs its forced web-search step for a model whose calling mode is set to legacy function calling. The default (native) path instead expects the model to decide on its own to call a search tool — which small local models essentially never do. And a freshly pulled Ollama model has no per-model config entry at all, so the setting is simply absent, and search is skipped for everyone with no error.

I only found this by reading the backend source for the exact version I was running, which is the honest lesson here: when a feature silently does nothing, read what the running code actually checks. The fix is to give each chat model a config entry setting its function-calling mode to legacy, via the model editor.

Same globe toggle, two very different outcomesglobe ON,native calling (default)model waits to call a tool…but never doesno searchno error, no clueglobe ON,legacy calling setOWUI forces the searchfetch + embed pagescited answer[1][2][3]

Trap 3: leave “search query generation” off for small models

Open WebUI has an option to let a model rewrite your prompt into a search query before searching. It sounds smart. With small local models it’s a disaster: they emit malformed or empty queries, and Open WebUI then silently skips the search and answers from stale memory — sometimes with a confident, wrong result. I re-enabled it deliberately once just to confirm, and got zero sources and a hallucinated answer. Leave search-query generation off; the raw prompt is a perfectly good search query.


The speed reality (so you’re not surprised)

When it works, the first thing you’ll notice is that web-search answers are slow on CPU-only hardware. That’s expected, and it’s mostly not the model’s fault. Turning your prompt into an answer breaks into: search (fast), fetch several web pages and embed their text (slow), then generate the reply (medium).

On my GPU-less nodes, a small qwen2.5:3b model gave plain chat replies in roughly 15–30 seconds but took around 80 seconds with web search — because fetching and embedding the pages dominates, not the thinking. A 7B model on the same CPU was simply too slow to enjoy. Two honest takeaways: use a small model for interactive web search on CPU, and know that a GPU is the real fix if you want it snappy. See Run an Ollama Cluster in LXC for how the inference side is laid out.


Don’t let it fail silently

There’s a nasty property to this setup: if SearXNG goes down, Open WebUI’s citations just quietly stop — the chat still answers, just without sources, and you might not notice for weeks. That’s exactly the kind of silent failure worth a monitoring check. I point a simple black-box HTTP probe at the SearXNG URL and alert if it stops returning 200, so a dead search engine pages me instead of degrading in silence. The companion playbook includes the deploy steps end to end.


What’s next

You’ve now got a local model that can see today, with citations, and without leaking a single question to a search vendor. Next in the series, we point that same local-AI muscle at your bookmarks: Karakeep auto-tags every link you save using a local model, so your bookmark graveyard becomes something you can actually find things in.


Related posts:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.