All articles
TutorialsJun 25, 2026 · 21 min read · By The RDP.sh Team

Self-Host SearXNG on a VPS for a Private Metasearch Engine

Self-Host SearXNG on a VPS for a Private Metasearch Engine

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.

TL;DR

  • Install Docker and Docker Compose on a small VPS
  • Point a subdomain like search.example.com at the server
  • Run searxng/searxng plus a Valkey cache behind Caddy in one docker-compose.yml
  • Generate a secret key and set the public base URL in settings.yml
  • Enable the limiter so bots can't abuse your instance
  • Set it as your browser's default search engine via OpenSearch
  • Lock it down with HTTP auth or a VPN if it's just for you

Total time: about 20 minutes.

What You Need

  • A VPS with at least 1 GB RAM running Ubuntu 22.04 or 24.04
  • A domain or subdomain you can point at the server
  • Ports 80 and 443 open to the internet for Let's Encrypt
  • Root or sudo access

SearXNG 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.

Why Self-Host Instead of Using a Public Instance

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:

  • Public instances get rate-limited fast. Google and Bing throttle shared IPs that send a lot of queries, so popular public instances frequently return partial or empty results. Your own server's IP sends only your traffic.
  • You still have to trust the operator. A malicious or careless public instance could log queries just like the engines you're avoiding. On your own box, you are the operator.
  • You control the engine list and preferences. Disable the engines you don't want, tune safe-search, set your default language, and pin your preferences without a cookie that gets wiped.

The trade is that you run the updates and the box. On a VPS you already pay for, that's close to free.

Step 1: Point a Subdomain at Your VPS

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.

Step 2: Install Docker and Docker Compose

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

Step 3: Open the Firewall

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.

Step 4: Create the Project Directory

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.

Step 5: Write the Compose File

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:

  • We don't publish SearXNG's port on the host. Caddy reaches it over the Docker network on the internal port 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.
  • Valkey runs with persistence disabled (--save "" --appendonly no) because the cache is disposable. Losing it just means a cold start, not lost data.
  • The 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.

Step 6: Generate the Config and Secret Key

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
The `secret_key` signs SearXNG's internal tokens. If it stays at the default `ultrasecretkey`, anyone can forge requests against your instance. Generate a random one before you expose the server, and never reuse it across instances.

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.
  • Adding json to formats lets tools and browser extensions query your instance programmatically. Leave it off if you only ever use the web UI.

Step 7: Write the Caddyfile

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.

Step 8: Start the Full Stack

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.

Step 9: Make It Your Default Search Engine

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.

Step 10: Lock Down Access

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.

Step 11: Keep It Updated

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.

SearXNG pins `latest` to a rolling release, not a stable channel. If an update ever breaks an engine you rely on, you can pin a specific dated tag like `searxng/searxng:2025.6.1-abc1234` from the registry instead of `latest`, then bump it deliberately.

Troubleshooting

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.

Going Further

  • Tune the engine list. Open 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.
  • Add a custom theme and bangs. SearXNG supports DuckDuckGo-style !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.
  • Run it behind your other services. This same Caddy reverse-proxy pattern powers our guides for Vaultwarden and FreshRSS. One small VPS happily hosts all three on separate subdomains.
  • Monitor it. A search engine you depend on is worth watching. Point an Uptime Kuma check at 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.