How to Self-Host a Private Search Engine with SearXNG (Google's Results, Nobody's Tracking)

Every search you type builds a profile somewhere. SearXNG is a metasearch engine: a page on your server that forwards your query to Google, Bing, DuckDuckGo, Wikipedia, GitHub, Stack Overflow and seventy other sources, merges the results, and shows them without adverts, tracking cookies or a logged-in identity. The engines see a request from your server's IP with no history attached; you see clean results. Set it as the default search in every browser in the house.
Quick reference
| Task | What |
|---|---|
| Folder | ~/docker/searxng/compose.yaml + settings.yml |
| Web | http://server-ip:8083 (pick a free port) |
| Needs | a secret_key in settings, Redis for rate limiting |
| Engines | Preferences → Engines or engines: in settings.yml |
| Browser default | https://search.example.com/search?q=%s |
| Keep private | LAN/VPN only, or behind the proxy with the limiter on |
1. The compose file
mkdir -p ~/docker/searxng/config && cd ~/docker/searxng
nano compose.yaml
services:
searxng:
image: searxng/searxng:latest
container_name: searxng
ports:
- "127.0.0.1:8083:8080"
environment:
SEARXNG_BASE_URL: https://search.example.com/
SEARXNG_SECRET: ${SEARXNG_SECRET}
volumes:
- ./config:/etc/searxng
depends_on: [redis]
cap_drop: [ALL]
cap_add: [CHOWN, SETGID, SETUID]
restart: unless-stopped
redis:
image: redis:alpine
container_name: searxng-redis
command: redis-server --save 30 1 --loglevel warning
cap_drop: [ALL]
cap_add: [SETGID, SETUID, DAC_OVERRIDE]
restart: unless-stopped
.env with SEARXNG_SECRET (openssl rand -hex 32). On first start it writes a default config/settings.yml.
docker compose up -d
2. settings.yml
nano config/settings.yml
The lines worth setting:
use_default_settings: true
server:
base_url: https://search.example.com/
limiter: true
image_proxy: true
redis:
url: redis://searxng-redis:6379/0
search:
safe_search: 0
autocomplete: "duckduckgo"
default_lang: "en-GB"
formats: [html, json]
ui:
default_theme: simple
infinite_scroll: true
engines:
- name: google
disabled: false
- name: bing
disabled: false
- name: duckduckgo
disabled: false
- name: brave
disabled: false
image_proxy fetches thumbnails through the server so the image hosts don't see you; formats: json lets Homepage (94) and scripts query it. Restart after editing.
3. Reverse proxy
search.example.com → 127.0.0.1:8083 (28) with HTTPS, and pass the real IP (X-Forwarded-For, X-Real-IP) so the limiter can tell devices apart. Behind the VPN is the private default; if you make it public, keep limiter: true on or bots will use your instance to scrape Google and get your IP blocked.
4. Make it the default search
- Firefox: visit the page, then Settings → Search → it appears in the engines list (OpenSearch); set as default. Or Add Custom Search with https://search.example.com/search?q=%s.
- Chrome/Edge/Brave: Settings → Search engine → Add: URL https://search.example.com/search?q=%s, then Make default.
- iOS Safari can't set a custom engine; use a shortcut, or the xSearch/Hyperweb apps. Android Firefox/Chrome: as above.
- Homepage's search widget (94): provider: custom, that URL.
! bangs work (!w linux searches Wikipedia, !gh GitHub, !yt YouTube — routed to Invidious if you set it, 181). Categories (images, videos, news, map, files, science) are tabs.
5. Tune it
Preferences (cog) per browser: engines on/off, results per page, dark theme, open in new tab, the language. Preferences are stored in a cookie or a URL you can bookmark — nothing on the server. Disable engines that keep failing (the stats page shows per-engine response times).
Why it's private — and what it isn't
Your searches leave the server with no cookie and no account, from one IP for the whole household, mixed together. Google can't profile you; it sees a server asking varied questions. It isn't anonymous — your server's IP is still the requester, and a public instance would make that IP look like a bot. For true anonymity you'd route SearXNG's outbound through Tor or a VPN container (136); for a household it's already a large step up.
Troubleshooting
"Too many requests" / engines failing. Google rate-limits the server's IP; disable Google for a while, or route via Gluetun (136). Keep limiter: true if the instance is public.
Images don't load. image_proxy needs outbound access; or disable it.
Results in the wrong language. default_lang and the browser's preference.
Secret key error on start. SEARXNG_SECRET missing; settings.yml's secret_key must not be the default.
Wrapping up
Two containers, a secret, a settings file with the engines you want, HTTPS, default search in every browser. The results you're used to, with the tracking removed at the source — and !w, !gh and the rest still at your fingertips.
