Self-hosting SearXNG gives your AI agent a web search tool that costs nothing per query and sends no search history to a third-party API vendor. It is a metasearch engine: your agent asks it a question, it asks Google, Bing, DuckDuckGo and others, and it returns merged results. On a VPS the setup is a Docker Compose file, one settings change to turn on JSON, and a few lines to keep it private.
Disclosure: the Hostinger links in this post are affiliate links, and I may earn a commission if you buy through them. I did not have a running Docker daemon in this session, so the SearXNG stack itself is unrun. What I did run: the Python tool below against a stand-in server that mimics the JSON shape. Configuration is checked against the SearXNG docs on 2026-10-23.
What do you need before you start?
A Linux VPS with Docker and Docker Compose, and SSH access. SearXNG is light compared with running a model: the official compose file starts the app plus a small Valkey (a Redis-compatible store). A 2 vCPU, 8 GB box is plenty, and it can share a server with your agent or n8n. If you need a server, Hostinger KVM 2 (2 vCPU, 8 GB, 100 GB at $8.79 on a term, renewing at $14.99, per Hostinger's VPS page on 2026-10-23) is what I would look at first; Hetzner or DigitalOcean work the same. I have not tested any Hostinger server for this post. To size it properly see which Hostinger VPS plan for AI apps.
Harden the box first. Search endpoints are a magnet for scanners, so run through the VPS hardening checklist before you add anything.
How do you run SearXNG in Docker?
The SearXNG Docker docs recommend Compose. These are their commands, taken from the docs on 2026-10-23:
mkdir -p ./searxng/core-config/
cd ./searxng/
curl -fsSL \
-O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
-O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
cp -i .env.example .env
Before starting it, read what the compose file does. It defines two services: core (image docker.io/searxng/searxng, tag from SEARXNG_VERSION, default latest) and valkey. The core service publishes its port like this:
ports:
- ${SEARXNG_HOST:+${SEARXNG_HOST}:}${SEARXNG_PORT:-8080}:${SEARXNG_PORT:-8080}
If SEARXNG_HOST is empty, that expands to 8080:8080, which Docker publishes on all interfaces. Do not assume your firewall covers a Docker-published port, and verify from outside. Fix it in .env:
SEARXNG_HOST=127.0.0.1
SEARXNG_VERSION=latest
The .env.example itself suggests pinning a tag such as 2026.3.25-541c6c3cb instead of latest. For an agent dependency, pin one, so an update never changes behavior on a Tuesday night. Then start it:
docker compose up -d
docker compose logs -f core
The docs say your settings live in core-config/settings.yml. I could not run the container, so check that the file appears after the first start. If it does not, create it yourself in the next step.
How do you turn on JSON output?
This is the step everyone misses. The Search API accepts format=json, but the docs say the format must be activated in the search: settings, and a disabled format returns HTTP 403. The example config lists only html.
SearXNG's settings docs describe use_default_settings: true, which loads your file on top of the built-in defaults so you write only what you change. Put this in core-config/settings.yml:
use_default_settings: true
server:
secret_key: "paste-a-long-random-string-here"
search:
formats:
- html
- json
Generate the secret with openssl rand -hex 32. The server docs say to change the example secret_key, and they note the example limiter is false and public_instance is false. Leave both alone for a private instance. One caveat: overriding a list like formats replaces the whole list, which is why html is repeated. Keep it so the web UI still works for debugging.
Restart and test:
docker compose restart core
curl -s "http://127.0.0.1:8080/search?q=caddy+reverse+proxy&format=json"
If you get 403, the JSON format is still off. If you get a JSON body, you have a search API. The documented parameters are q (required), categories, language, pageno (default 1), time_range (day, month or year, only for engines that support it) and format. The docs do not describe the response fields, so print one real response and read it before you build on it.
How do you lock it down?
A JSON search endpoint is a free search API for whoever finds it. Your IP also takes the rate limits upstream. Four rules:
- Bind to loopback.
SEARXNG_HOST=127.0.0.1as above. Verify from outside the box withcurl -m 5 http://YOUR_IP:8080, which should time out or refuse. - Do not put it on a public domain. If an agent on another machine needs it, use a VPN. Tailscale on a VPS gives you a private address with no open ports.
- Leave
public_instanceoff. The docs describe it as for public instances, not local use. - Treat results as untrusted input. Snippets come from arbitrary web pages. An agent that pastes them into its prompt is exposed to prompt injection. Strip the content to title, URL and a short snippet, as the tool below does, and do not let search results trigger tool calls on their own.
How does a Python agent call it?
This is a dependency-free tool function. It builds the query, asks for JSON, and returns trimmed results that you can hand to a model as a tool result. I ran it on 2026-10-23 against a stand-in server on port 8080 that returns a JSON body with a results list of title, url and content. That shape is my assumption from how SearXNG is commonly used, not from the docs, so confirm it against a real response from your instance.
import json
import os
import urllib.parse
import urllib.request
SEARXNG_URL = os.environ.get("SEARXNG_URL", "http://127.0.0.1:8080")
def web_search(query: str, max_results: int = 5, time_range: str = "") -> list[dict]:
params = {"q": query, "format": "json"}
if time_range:
params["time_range"] = time_range
url = f"{SEARXNG_URL}/search?{urllib.parse.urlencode(params)}"
req = urllib.request.Request(url, headers={"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.load(resp)
out = []
for r in data.get("results", [])[:max_results]:
out.append({
"title": r.get("title", ""),
"url": r.get("url", ""),
"snippet": (r.get("content") or "")[:300],
})
return out
Output against the stand-in server (not a real SearXNG instance):
- Caddy docs
https://caddyserver.com/docs/
Caddy is a web server.
- Second
https://example.org/
Register it as a tool following function calling: name web_search, one string parameter query. Cap max_results at 5, because every extra result is tokens you pay for on every turn. If your agent runs on a different machine from SearXNG, set SEARXNG_URL to the private VPN address.
How do you call it from n8n?
If n8n runs in Docker on the same VPS, 127.0.0.1 inside the n8n container is the n8n container, not SearXNG. The clean fix is a shared Docker network. This is unrun, so test it:
docker network create agents
Add the network to both stacks (SearXNG's core service, and n8n):
services:
core:
networks: [default, agents]
networks:
agents:
external: true
Do the same for the n8n service, then use an HTTP Request node with method GET, URL http://searxng-core:8080/search, and query parameters q (from your expression) and format=json. searxng-core is the container_name in the official compose file. If you only need a model to decide when to search, an n8n AI Agent node can call the same request as a tool. For how n8n is set up in the first place, see the n8n on a VPS guide.
What goes wrong in practice?
- 403 on format=json. JSON is not in
formats. See above. - Empty or thin results. Upstream engines rate limit or block datacenter IPs. SearXNG can only forward what they return. Test 20 of your real queries and count how many come back useful before wiring it into an agent.
- Settings ignored. A YAML indentation error or a file in the wrong place. Check
docker compose logs core. - Open port after a reboot. Re-run the outside
curltest after every compose change. - Stale facts. Search results are not verified. For anything the agent will state as fact, make it cite the URL.
Should you use SearXNG or a paid search API?
Use SearXNG when privacy and zero per-query cost matter and you can live with variable quality. Use a paid search API when you need a stable response contract and support. Both can sit behind the same web_search function, so you can swap later.
If your agent also calls a hosted model and you want to pay in rupees by UPI, AICredits offers an OpenAI-compatible endpoint at https://api.aicredits.in/v1 with a minimum top-up of Rs 50 (check their site for current terms). It is an affiliate link. The non-affiliate options are going direct to the model vendor, or running a local model with Ollama.
What I did not verify
I did not run the SearXNG containers, so I cannot show a real JSON response, the exact settings.yml it generates, or how many results your VPS gets before an engine blocks it. The compose file, ports and settings keys come from the SearXNG documentation read on 2026-10-23. The Python function ran only against a stand-in.



