Every search you type is a tiny confession. Where you're traveling, what you're buying, which symptoms you're worried about, who you're hiring - the big engines log all of it, tie it to your account and IP, and sell the profile to advertisers. Switching to a privacy front-end helps, but most of them are themselves hosted by someone you have to trust.
SearXNG cuts the middleman out. It's an open-source metasearch engine: instead of crawling the web itself, it forwards your query to dozens of upstream engines (Google, Bing, DuckDuckGo, Brave, Wikipedia, GitHub, and more), strips the tracking, merges the results, and hands them back to you. Run it on your own VPS and every search leaves from your server's IP, with no cookies, no logs, and no profile. This guide takes you from a fresh box to a working private search engine on your own domain, with HTTPS, rate limiting, and bot protection.
search.example.com at the serversearxng/searxng plus a Valkey cache behind Caddy in one docker-compose.ymlsettings.ymlTotal time: about 20 minutes.
80 and 443 open to the internet for Let's EncryptSearXNG is light but not featherweight - it runs several Python workers and benefits from the Valkey cache. On a 1 GB box it idles around 150-200 MB and handles a single household easily. If you expect more than a handful of concurrent users, give it 2 GB.
There's a public list of SearXNG instances, and they're genuinely useful for a quick anonymous search. But for daily driving, your own instance wins:
The trade is that you run the updates and the box. On a VPS you already pay for, that's close to free.
In your DNS provider, add an A record:
search.example.com → YOUR_VPS_IPV4
Add an AAAA record if you use IPv6. Verify it resolves:
dig +short search.example.com
DNS has to point at your VPS before Caddy can fetch a certificate.
On a fresh Ubuntu box:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Confirm:
docker --version
docker compose version
If you use UFW:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Caddy needs port 80 for the ACME HTTP challenge and 443 for HTTPS.
sudo mkdir -p /opt/searxng
cd /opt/searxng
sudo mkdir -p searxng caddy-data caddy-config
Everything persistent lives under /opt/searxng. The searxng subdirectory holds your config; the two caddy-* directories hold the TLS certificates and state.
SearXNG ships as a single image, paired with a Valkey (a drop-in Redis fork) instance that powers the rate limiter and result caching. Create /opt/searxng/docker-compose.yml:
services:
searxng:
image: searxng/searxng:latest
container_name: searxng
restart: unless-stopped
environment:
SEARXNG_BASE_URL: "https://search.example.com/"
SEARXNG_REDIS_URL: "redis://valkey:6379/0"
UWSGI_WORKERS: 4
UWSGI_THREADS: 4
volumes:
- ./searxng:/etc/searxng:rw
networks:
- searxng-net
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
valkey:
image: valkey/valkey:8-alpine
container_name: searxng-valkey
restart: unless-stopped
command: valkey-server --save "" --appendonly no
volumes:
- ./valkey-data:/data
networks:
- searxng-net
cap_drop:
- ALL
cap_add:
- SETGID
- SETUID
- DAC_OVERRIDE
caddy:
image: caddy:2
container_name: searxng-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- searxng-net
networks:
searxng-net:
A few notes:
8080.SEARXNG_BASE_URL must be the exact public URL with a trailing slash. SearXNG uses it to build the OpenSearch descriptor and image proxy links - get it wrong and "add as search engine" breaks.--save "" --appendonly no) because the cache is disposable. Losing it just means a cold start, not lost data.cap_drop/cap_add blocks drop every Linux capability and add back only the few the images actually need. It's a cheap hardening win for an internet-facing service.SearXNG needs a settings.yml. The easiest path is to let the container generate a default one, then edit the two values that matter. Start it once so it writes the file:
cd /opt/searxng
sudo docker compose up -d searxng
sleep 5
sudo docker compose down
You'll now have /opt/searxng/searxng/settings.yml. Generate a strong secret key and patch it in:
SECRET="$(openssl rand -hex 32)"
sudo sed -i "s|ultrasecretkey|$SECRET|g" /opt/searxng/searxng/settings.yml
Now open /opt/searxng/searxng/settings.yml and adjust a few keys. Find the server and search sections and make them look like this:
server:
secret_key: "the-random-value-you-just-generated"
limiter: true
image_proxy: true
search:
safe_search: 0
autocomplete: "duckduckgo"
default_lang: "en"
formats:
- html
- json
What these do:
limiter: true turns on the bot limiter backed by Valkey. This is what stops scrapers from hammering your instance and getting your server's IP blocked by upstream engines.image_proxy: true routes thumbnail images through your server so the original sites never see your visitors' IPs.autocomplete enables the search suggestions dropdown.json to formats lets tools and browser extensions query your instance programmatically. Leave it off if you only ever use the web UI.Create /opt/searxng/Caddyfile:
search.example.com {
encode zstd gzip
reverse_proxy searxng:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
Those forwarded headers matter: the limiter decides who to throttle based on the real client IP, so Caddy has to pass it through. Without them, every request looks like it comes from the Docker gateway and the limiter can't tell users apart.
cd /opt/searxng
sudo docker compose up -d
sudo docker compose logs -f
Watch the logs until Caddy reports the certificate was issued and SearXNG's uWSGI workers are ready. Then open https://search.example.com in a browser. You should see the clean SearXNG search box. Type a query and confirm you get merged results from multiple engines.
This is the payoff - searching from the address bar without thinking about it.
Firefox auto-detects the OpenSearch descriptor. Visit your instance once, then go to Settings → Search → Add, or right-click the address bar and choose Add Search Engine. Set it as default.
Chrome / Brave / Edge usually pick it up under Settings → Search engine → Manage search engines after you visit the site. If it doesn't appear, add it manually with this query URL:
https://search.example.com/search?q=%s
Now every address-bar search runs through your own private engine. To carry your preferences (engine selection, language, theme) across devices without a cookie, open Preferences in SearXNG, scroll to the bottom, and copy the settings URL or hash - paste it on each device.
A public SearXNG instance is a magnet for bots that want a free, anonymous search proxy. You have three good options depending on who needs it.
Option A - Keep it public, lean on the limiter. The limiter: true you set in Step 6 plus Valkey blocks most abusive traffic. Fine for a low-profile instance, but expect occasional bot noise in your logs.
Option B - HTTP basic auth in Caddy. If the instance is just for you and a few people, put a password in front of the whole thing. Generate a hash:
sudo docker run --rm caddy:2 caddy hash-password --plaintext 'your-strong-password'
Then wrap the site in basic_auth in your Caddyfile:
search.example.com {
encode zstd gzip
basic_auth {
admin $2a$14$...the-hash-you-generated...
}
reverse_proxy searxng:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
Reload with sudo docker compose restart caddy. The downside: basic auth and browser "add as search engine" don't always play nicely, since the address-bar search can't prompt for credentials.
Option C - VPN only. The cleanest answer for a personal instance is to never expose it publicly at all. Pair this with our Tailscale guide so search.example.com only resolves inside your tailnet. No public attack surface, no bots, and it still works as a default search engine on every device joined to the VPN.
SearXNG ships frequently, and upstream engines change their markup often enough that an old image will quietly stop returning results from some sources. Update on a schedule:
cd /opt/searxng
sudo docker compose pull
sudo docker compose up -d
Because your config lives in the mounted searxng/ directory and the cache is disposable, upgrades are low-risk - your settings.yml survives the pull. Still, copy settings.yml somewhere safe before a major version jump so you can diff it against the new defaults.
Caddy returns 502 Bad Gateway. SearXNG hasn't finished booting or crashed on a bad config. Check sudo docker compose logs searxng. A YAML syntax error in settings.yml is the usual cause - validate indentation, since YAML is whitespace-sensitive.
Results pages are empty or say "no results." Upstream engines are rate-limiting your IP, or the limiter is misconfigured. Confirm Valkey is up with sudo docker compose ps, check that SEARXNG_REDIS_URL matches the Valkey service name, and try a different engine via the search tabs to isolate which source is blocked.
"Forbidden" or instant blocks on every search. The limiter is throttling you because Caddy isn't forwarding the real client IP. Confirm the header_up X-Forwarded-For lines are present in your Caddyfile and restart Caddy.
Browser won't add it as a search engine. The SEARXNG_BASE_URL is wrong or missing its trailing slash, so the OpenSearch descriptor points at the wrong host. Fix it in the compose file, recreate the container, and revisit the site.
Images don't load in image search. image_proxy is off or the instance can't reach the source sites. Set image_proxy: true in settings.yml and restart SearXNG.
settings.yml and disable engines you never use or that are slow, and enable niche ones (Stack Overflow, GitHub, Wikidata, scientific databases). Fewer engines means faster, cleaner results.!bang shortcuts out of the box - type !gh searxng to jump straight to a GitHub search. Set your preferred theme and result layout under Preferences.https://search.example.com so you know the moment it stops responding.Self-hosted SearXNG is the rare privacy upgrade that costs you nothing in convenience. The results are as good as the engines behind them, the tracking is gone, and the only IP doing the searching is your own server's.
Need a small VPS to run a stack like this? Our Linux plans include fast NVMe storage, IPv6, and plenty of headroom for Docker workloads. See the options.