SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-09-05

How to Self-Host SearXNG with Docker Compose

Run private SearXNG for your VPS with Docker Compose. Configure settings.yml, the limiter, nginx TLS, and a JSON search API for your own scripts.

Wetin you dey build

To self-host SearXNG give you private search engine wey dey run for your own server. SearXNG na metasearch engine: e take your query, ask other engines like Google, Bing, DuckDuckGo and Wikipedia, then merge wetin dem return into one result page. E no build profile and e no set tracking cookie, because na only your machine dey keep your query. If you don see older guides about something wey dem just call Searx, na this project fork from there, and e never get any commit since 2023, so check the state of both before you follow one.

The stack small. Two containers, one settings file, and one reverse proxy. E go run well for small VPS, but no be every self-hosted service be like 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 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, with no key, no per-query billing, and no quota email.

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 don already get Docker Engine and the Compose plugin. If Docker still new to you, start with Docker Compose basics for 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 .env

The Compose file define two services. core na SearXNG itself, while valkey na in-memory data store wey dem dey use for rate limiting and short-lived state. E mount ./core-config/ as /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=8080

SEARXNG_HOST=127.0.0.1 na the important one. E make the published port 127.0.0.1:8080:8080 instead of [::]:8080:8080, so the container go answer only on the loopback address and internet no go reach am directly. If you skip this, the container go expose immediately e start, because published Docker port dey insert before your firewall rules. This trap worth reading well: 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 July 2026, the release tags dey use dates and dem look like 2026.3.25-541c6c3cb, so pinned deployment go upgrade when you decide, not when the registry change by itself. This same discipline dey useful for anything wey go stay long for the box. Na why self-hosted RustDesk relay dey pin its image tags too: unattended upgrade to remote access service fit show itself for the worst possible time.

settings.yml: parts wey matter

Create core-config/settings.yml before the first start. use_default_settings: true tell SearXNG make e load the defaults wey e ship with, then apply only the keys wey you write. This make your file short and help am survive upgrades wey add new options.

Generate the secret first, because the value go enter the file directly.

openssl rand -hex 32
use_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
    - json

secret_key dey sign session and token data. The shipped default na the literal string ultrasecretkey. If you leave am like that, anybody wey know the default fit forge those tokens. Replace am once, then leave am. 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 dey write inside the links wey e render. If you leave am pointing to localhost, the "next page" link for remote browser go point to the reader own machine and e go fail.

formats decide the output types wey the web endpoint go produce. json no dey inside the default list, so JSON request go return 403 until you add am. image_proxy: true route result thumbnails through your server, so the sites wey host those images no go see your visitors' addresses.

The valkey.url dey use hostname valkey because na the service name for the Compose file. Compose put both containers for one network where service names fit resolve. If you point am to localhost, the limiter go fail, because inside the core container, localhost na that container.

The secret dey inside a plain file, so protect the directory around am instead of the file itself. chmod 750 /opt/searxng keep other host users out. No tighten core-config/settings.yml to mode 600: the container dey run as its own unprivileged user, and if the file no readable by am, SearXNG no go start at all.

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 anything, read docker compose logs core, because YAML mistake for settings.yml go show there as parse error wey name 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.com

nginx -t go print syntax is ok and test is successful before you reload. Certbot go rewrite the same file to listen on 443 with certificate, and e go add redirect from port 80. DNS record for search.example.com must already point to this server, because certificate authority go 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 reach SearXNG go carry proxy address. So rate limiter go see one client dey generate all the traffic, and e no go fit tell visitors apart.

Why scripts and agents need one JSON search API

With json for formats, 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. Every entry carry url, title, content and the engine wey supply am, together with answers, infoboxes and suggestions. This dey enough to feed summariser, link checker, or research loop. To hand these results to language model na bigger step pass as 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 explain well.

This 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 impose strict rate limits. Local instance cost one container for server wey you already dey pay for, and the queries no dey leave am. If you dey connect tools to model, this same reason support running MCP servers on a VPS, where search tool usually na the first tool people dey add.

Follow two rules when you dey use API. Keep the instance private. Bind the API side to loopback address or private network, and allow only your own hosts reach am. Then query am gently. SearXNG dey forward your request to real search engines, so script wey dey run hundred queries every second fit make Google block your server.

The limiter, and wetin change for public instance

The limiter na SearXNG bot defence. E dey monitor 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 dey ship with 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 v3 onion address for 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: true

More detailed control dey inside core-config/limiter.toml, wey 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. Otherwise 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 = true

link_token = true make SearXNG issue token wey only real browser session go fetch, and this dey stop most simple scrapers. Expect public instance to attract dem within days. Also expect engine errors, 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 among most short lists of things wey worth self-hosting for 2026. No be every entry for those lists be infrastructure either: rebuild Jellyfin library as walkable 90s rental store na the same one container behind the same nginx block, pointed to an evening in instead of a workflow.

Wetin make search return nothing

Open /stats for your instance. E dey list every engine with its error rate and response time. Na the first place to check when results dey too few.

Engine wey show "Access denied" or "CAPTCHA" errors don block your server address. This one dey common for addresses wey dey data centre ranges, because search engines assume say scrapers dey use dem. SearXNG then suspend the engine wey 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 remaining engines still dey answer. A 429 na the unclear case, because e fit come from your own limiter or from upstream engine wey refuse your server, and the log line go tell you which one you dey face before you start changing settings.

If every engine fail at the same time, the container no get working outbound name resolution or no route reach the internet. Test am from inside the container.

docker compose exec core wget -qO- https://duckduckgo.com > /dev/null && echo ok

Nothing for the box go tell you when that 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 results don too few.

FAQ

SearXNG dey make my searches anonymous?

E hide who you be from the engines wey e dey query, because na your server dem dey see as e make the request, no be your browser. E no hide the query from your server, and e no hide your server from dem. For single-user instance, all traffic from that address na your own, so the address itself become the identifier. TLS certificate protect traffic between your browser and your instance. How this affect you with your ISP, operator of public instance, and the engines themselves dey explain for wetin SearXNG really dey hide.

Why JSON request dey return 403 Forbidden?

Na 2 configuration causes fit cause am. Either json no dey the formats list under search: for settings.yml, and na the default state be this, or limiter dey on and e don 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 keep running. 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 dey store only cached data, so removing am go save very little and you go lose the option.

How I go update SearXNG?

Run docker compose pull then docker compose up -d inside /opt/searxng. Compose go recreate any container wey image don change and 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 come with sensible values instead of breaking the file.

Several people fit share one instance?

Yes, and na this situation you go turn limiter on and set public_instance: true. Each visitor own browser dey store their preferences, so no accounts dey to manage. Monitor /stats for 1 week after you open am, because upstream engines go start rejecting your server long before you notice say results dey missing.