How to Self-Host SearXNG for Private Search
Run SearXNG for private search on your VPS with Docker Compose, settings.yml, limiter, nginx TLS, and a JSON API wey your scripts fit call.
Wetin you dey build
Self-hosting SearXNG go give you private search engine wey dey run for your own server. SearXNG na metasearch engine: e go take your query, ask other engines like Google, Bing, DuckDuckGo and Wikipedia, then join wetin dem return into one result page. E no dey build profile and e no dey set tracking cookie, because na only your machine dey keep your query. If you don see older guides for something wey dem just call Searx, na this project fork from there. Dem no maintain am since September 2023, when the project repository announce say dem no dey maintain Searx again. The one commit wey come after that, na documentation note for May 2026, and e no change the situation. So check the state of both before you follow one.
The stack small. Two containers, one settings file, and one reverse proxy. E go share small VPS without wahala. But no be every self-hosted service dey do that: the photo libraries wey dem compare for PhotoPrism against Immich set their RAM floor based on the indexer, no be the web app. The main decision na whether the instance go be private, meaning say na only you and your own scripts fit reach am, or public, meaning say anybody for internet fit query am. This choice go change the security settings, so decide am before you type anything. The default answer na private.
Another reason to run one dey. SearXNG instance dey provide JSON, so any script or AI agent wey you write go get search API wey you own, without key, per-query billing, or quota mail.
Install SearXNG with Docker Compose
The project dey publish a container image and a Compose file. Pull both go fresh Ubuntu 24.04 server wey already get Docker Engine and the Compose plugin. If Docker still new to you, start with Docker Compose basics on a VPS and come back.
sudo install -d -o "$USER" -g "$USER" -m 750 /opt/searxng
cd /opt/searxng
mkdir -p core-config
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 .envThe Compose file define two services. core na SearXNG itself, and valkey na in-memory data store wey dem dey use for rate limiting and short-lived state. E mount ./core-config/ for /etc/searxng/ inside the container, so everything wey you configure dey inside that one directory for the host.
Now edit .env. Every line for the example wey dem ship dey commented out. Na why the container dey start for port 8080 on every address. Uncomment and set these three.
SEARXNG_VERSION=latest
SEARXNG_HOST=127.0.0.1
SEARXNG_PORT=8080SEARXNG_HOST=127.0.0.1 na the important one. E make the published port 127.0.0.1:8080:8080 instead of bare 8080:8080. Docker dey publish bare 8080:8080 for every address, so the container go answer only on the loopback address and internet no fit reach am directly. If you skip this, the container go expose immediately when e start, because Docker dey insert published port before your firewall rules. This trap worth reading fully: published Docker ports bypass ufw.
SEARXNG_VERSION=latest dey okay while you dey learn. For server wey matter to you, pin the tag. As of October 2026, the release tags dey use dates and dey look like 2026.10.4-d48c4b555. This mean pinned deployment go upgrade when you decide, not when registry change things behind you. The same discipline dey help for anything else wey go run long-term for the box. Na why a self-hosted RustDesk relay dey pin its image tags too: unattended upgrade to remote access service fit show itself for the worst possible moment.
settings.yml: wey parts matter
Create core-config/settings.yml before the first start. use_default_settings: true tells SearXNG make e load the defaults wey come with am, then apply only the keys wey you write. This keeps your file short and lets am survive upgrades wey add new options.
Generate the secret first, because the value go enter the file directly.
openssl rand -hex 32use_default_settings: true
general:
instance_name: "search.example.com"
server:
base_url: "https://search.example.com/"
secret_key: "paste-the-openssl-output-here"
limiter: false
public_instance: false
image_proxy: true
valkey:
url: valkey://valkey:6379/0
search:
safe_search: 0
autocomplete: "duckduckgo"
formats:
- html
- jsonsecret_key signs session and token data. The shipped default na the exact string ultrasecretkey. If you leave am like that, anybody wey know the default fit forge those tokens. Replace am once, then leave am unchanged: if you change am later, every saved preference go disappear.
base_url must be the public HTTPS address, with the trailing slash. Na this address SearXNG writes into the links wey e renders. If you leave am pointing to localhost, the "next page" link for a remote browser go point to the reader's own machine and fail.
formats decides the output types wey the web endpoint go produce. json no dey the default list, so a JSON request go return 403 until you add am. image_proxy: true routes result thumbnails through your server, so the sites wey host those images no go see your visitors' addresses.
The valkey.url uses the hostname valkey because na the service name for the Compose file. Compose puts both containers for one network where service names dey resolve. If you point am to localhost, the limiter go fail, because inside the core container, localhost na that same container.
The secret dey inside a plain file, so protect the directory around am too. chmod 750 /opt/searxng keeps other host users out. You fit also tighten core-config/settings.yml to mode 600 without breaking anything. The official container's entrypoint starts as root and, with FORCE_OWNERSHIP still set to the default true, e hands the mounted config directory to the container's own searxng user before SearXNG starts. This makes the file remain readable to am. The side effect dey show for the host: that same step changes the owner of core-config/ to the container's user ID, so expect to edit the file with sudo afterwards.
Start the stack and check am.
cd /opt/searxng
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8080/docker compose ps suppose show both containers for state running. The curl suppose answer HTTP/1.1 200 OK. If e no answer, read docker compose logs core, because YAML mistake for settings.yml go show there as a parse error wey names the line.
Put am behind nginx with TLS
The container dey listen for loopback only, so na nginx dey make am reachable. Na nginx still dey add transport layer security (TLS). Write /etc/nginx/sites-available/searxng.
server {
listen 80;
server_name search.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}sudo ln -s /etc/nginx/sites-available/searxng /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d search.example.comnginx -t dey print syntax is ok and test is successful before you reload. Certbot dey rewrite the same file to listen for 443 with certificate, and e dey add redirect from port 80. DNS record for search.example.com must already point to this server, because certificate authority dey prove ownership by fetching file over HTTP. Full walkthrough, including renewal, dey for Certbot and nginx guide for Ubuntu 24.04.
The two forwarding headers no be decoration. Without X-Forwarded-For and X-Real-IP, every request wey arrive for SearXNG dey carry proxy address. So rate limiter go see one client dey make all the traffic and e no fit tell visitors apart.
Why scripts and agents dey need JSON search API
With json for formats, na the same endpoint wey render the page dey return structured data.
curl -s 'http://127.0.0.1:8080/search?q=wireguard+mtu&format=json' \
| jq -r '.results[0:5][] | .url'You go receive one object wey get results array. Each entry carry url, title, content and the engine wey supply am, together with answers, infoboxes and suggestions. This one don enough to feed summariser, link checker, or research loop. To hand those results give language model na bigger step than e first look, because search results na untrusted text wey fit carry their own instructions. Na this issue pointing an AI agent at your SearXNG instance dey explain well.
This one matter for anything wey get agent shape. Language model get training cutoff, so e need live search to answer questions about wetin dey happen now. Commercial search APIs dey charge for every query and dem dey rate limit heavily. Local instance cost na one container for server wey you already dey pay for, and the queries no dey leave am. If you dey connect tools to model, na the same reasoning dey support running MCP servers on a VPS, where search tool usually be the first one people add.
Two rules dey for API use. Keep the instance private. Bind the API side to loopback address or private network, and allow only your own hosts reach am. Na the same arrangement a self-hosted location tracker need for the endpoint wey your phone dey report its position to. Then query am gently. SearXNG dey forward your request to real search engines, so script wey dey run hundred queries every second na like say you dey ask Google to block your server.
The limiter, and wetin change for a public instance
The limiter na SearXNG bot defence. E dey watch request headers, addresses, and request rates, then e dey drop traffic wey look automated. E need Valkey to hold that state, na why Compose file include am.
For private instance, keep limiter: false. Your own scripts na automated traffic by definition, so limiter go block exactly the JSON calls wey you build the instance for. Access control na reverse proxy work instead: an allow and deny pair for nginx location, HTTP basic authentication, or firewall wey only allow your other servers. If you need reach private instance from laptop wey dey move between networks, putting a v3 onion address in front of am na fourth option, because tor dey connect to the same loopback port without exposing anything new to internet.
If you publish the instance for other people, turn both switches on.
server:
limiter: true
public_instance: trueFiner control dey inside core-config/limiter.toml, wey the container dey read from /etc/searxng/limiter.toml. Write only the keys wey you want change. If proxy dey in front, you must declare the proxy, or limiter go treat your nginx address as the one abusive client.
[botdetection]
trusted_proxies = [
'127.0.0.0/8',
'::1',
]
[botdetection.ip_limit]
link_token = truelink_token = true dey make SearXNG issue a token wey only real browser session go fetch, and this dey stop most simple scrapers. Expect public instance to attract dem within days. Expect engine errors too, because the more traffic you forward, the sooner upstream engines go start return CAPTCHAs to your server address. Public SearXNG instance na ongoing job. Private one no be so, na why e dey most short lists of things wey worth self-hosting for 2026. No be every entry for those lists be infrastructure either: rebuilding a Jellyfin library as a walkable 90s rental store na the same one container behind the same nginx block, pointed at evening instead of workflow.
Search wey no dey return anything
Open /stats for your instance. E list every engine with im error rate and response time. Na the first place to check when results look small.
Engine wey show "Access denied" or "CAPTCHA" error don block your server address. This one common for address wey dey data centre ranges, because search engines assume say scrapers dey use dem. SearXNG go then suspend the engine wey dey fail for some time instead of retrying am. So one blocked engine fit quietly comot from your results. Disable am for settings.yml or accept the loss. But na not only these two options dey, because some CAPTCHA blocks get fix wey go survive restart. The engines wey remain still dey answer. 429 na the unclear case, because e fit come from your own limiter or from upstream engine wey dey refuse your server. The log line go tell you which one dey happen before you start changing settings.
If every engine fail at the same time, the container no get working outbound name resolution or route to the internet. Test am from inside the container.
docker compose exec core wget -qO- https://duckduckgo.com > /dev/null && echo okNothing for the box go tell you when this check start failing. So run am from cron and make failure push alert go your phone from your own ntfy server instead of waiting until you notice say the results don become few.
FAQ
SearXNG dey make my searches anonymous?
E dey hide who you be from the engines wey e dey query, because na your server dem dey see as e dey make the request, no be your browser. E no dey hide the query from your server, and e no dey hide your server from dem. For instance wey na one person dey use, all traffic from that address na your own, so the address itself become the identifier. TLS certificate dey protect traffic between your browser and your instance. How this affect your ISP, the operator of a public instance, and the engines themselves dey explain for wetin SearXNG actually dey hide.
Why JSON request dey return 403 Forbidden?
Na two causes, and both one concern configuration. Either json no dey inside the formats list under search: for settings.yml, and na the default state be that, or limiter dey on and e classify your script as bot. Add the format first, restart with docker compose restart core, then try again. If e still fail, set limiter: false and control access for the reverse proxy instead.
I need Valkey container if I keep limiter off?
Make e continue to run. SearXNG fit work without am, but you no fit turn limiter on later without am, and e still dey hold other short-lived state. The container small and e only dey store cached data, so removing am go save very little but e go remove that option from you.
How I fit update SearXNG?
Run docker compose pull then docker compose up -d inside /opt/searxng. Compose go recreate any container wey image change, and e go leave your core-config/ directory untouched, so settings.yml go remain. Because use_default_settings: true dey merge your keys over the shipped defaults, options wey upstream add go arrive with sensible values instead of breaking the file.
Several people fit share one instance?
Yes, and na this situation make you turn limiter on and set public_instance: true. Preferences dey store inside each visitor own browser, so no accounts dey to manage. Monitor /stats for one week after you open am, because upstream engines go start rejecting your server long before you notice say results dey miss.