A hardened SearXNG metasearch instance for one person. It runs with Docker Compose. It resolves every name over DNS-over-TLS, it can send every engine request through Tor, and it can generate decoy searches that hide how often you search.
SearXNG is a metasearch engine. It sends your query to other search engines and it returns the results. The engines see the instance. They do not see your browser.
This repository adds four things to a plain SearXNG instance.
- A local CoreDNS resolver. It forwards every name lookup to Mullvad over DNS-over-TLS.
- A settings file that removes identifying details from the outgoing requests.
- An optional Tor container. It hides your address from the search engines.
- An optional cover traffic generator. It sends decoy searches at random times.
Items 3 and 4 are off by default. Read the warnings before you turn them on.
| Path | Purpose |
|---|---|
docker-compose.yml |
The service definitions. |
searxng/settings.yml |
The SearXNG configuration. |
coredns/Corefile |
The DNS resolver configuration. |
cover/cover.py |
The decoy search generator. |
cover/test_cover.py |
The tests for the generator. |
.env.example |
The template for your local settings. |
Makefile |
The check command. |
- Docker Engine with the Compose plugin.
- Python 3.13 or later, to run the tests. The containers do not need it.
Step 1. Create your .env file. The instance does not start without a
secret key.
cp .env.example .env
echo "SEARXNG_SECRET=$(openssl rand -hex 32)" >> .envStep 2. Start the containers.
docker compose up -dStep 3. Open http://localhost:47821
docker compose down # stop the containers
docker compose down -v # stop and delete the Valkey volumedocker compose logs -f searxng # show the logs
docker compose restart searxng # apply a change to settings.yml
docker compose pull && docker compose up -d # update the imagesSet these values in your .env file. Only the first one is required.
| Variable | Default | Purpose |
|---|---|---|
SEARXNG_SECRET |
none | The session key. Generate it with openssl rand -hex 32. |
SEARXNG_PORT |
47821 |
The host port for the web interface. |
SEARXNG_UWSGI_WORKERS |
4 |
The uWSGI worker count. |
SEARXNG_UWSGI_THREADS |
4 |
The uWSGI thread count. |
COVER_MEAN_INTERVAL |
180 |
The mean seconds between decoy searches. |
COVER_USER_AGENT |
a Chrome string | The User-Agent for decoy searches. |
COVER_LOG_QUERIES |
0 |
Set to 1 to write the decoy text to the log. |
The .env file stays out of git. See .gitignore.
The json format is on. Use it from a script or an agent.
curl -s 'http://localhost:47821/search?q=docker+compose&format=json' | jq '.results[0]'The csv and rss formats are also on.
- Open http://localhost:47821 in Chrome.
- Run one search on the page.
- Open
chrome://settings/searchEngines. - Find Local SearXNG under Inactive shortcuts.
- Click the three dots. Select Activate.
Chrome reads the OpenSearch descriptor at /opensearch.xml. This method also
gives you search suggestions as you type.
- Open
chrome://settings/searchEngines. - Click Add beside Site search.
- Enter these values:
| Field | Value |
|---|---|
| Name | SearXNG |
| Shortcut | sx |
| URL with %s in place of query | http://localhost:47821/search?q=%s |
- Click Save.
To search, type sx in the address bar, press Tab, then type your words.
- Open
chrome://settings/searchEngines. - Find your entry under Site search.
- Click the three dots. Select Make default.
Warning. Chrome shows an error page for every search when the containers do not run. Start the containers before you make this change.
All name lookups from the SearXNG container go to base.dns.mullvad.net
over DNS-over-TLS on port 853. The mullvad-dns container runs CoreDNS and
does the encryption. See coredns/Corefile.
base.dns.mullvad.net blocks advertisements, trackers, and malware.
Check that it works. The first command gives no result. The second gives an address.
docker exec searxng getent hosts doubleclick.net # blocked
docker exec searxng getent hosts wikipedia.org # allowedTo use a different Mullvad profile, change the address in coredns/Corefile.
Then run docker compose restart mullvad-dns.
| Address | Profile | Blocks |
|---|---|---|
| 194.242.2.2 | dns |
Nothing |
| 194.242.2.3 | adblock |
Advertisements, trackers |
| 194.242.2.4 | base |
Advertisements, trackers, malware |
| 194.242.2.5 | extended |
The above, and social media |
| 194.242.2.6 | family |
The above, and adult content, gambling |
| 194.242.2.9 | all |
The above, and social media |
The certificate name must agree with the address. Change tls_servername in
the same file.
The instance already does these things:
| Measure | Effect |
|---|---|
| DNS-over-TLS to Mullvad | Your network operator cannot read the names you look up. |
query_in_title: false |
The query stays out of the page title, the browser history, and screen recordings. |
enable_http2: false |
HTTP/1.1 gives a more common TLS fingerprint than HTTP/2. |
useragent_suffix: "" |
Nothing identifies this instance to the search engines. |
image_proxy: true |
Image hosts see the instance, not your browser. |
| SearXNG itself | The search engines see the instance, not your browser. |
Two more measures are available. Both are off by default.
Every engine request leaves through the Tor network. The search engines then see a Tor exit address, not your address.
- Open
searxng/settings.yml. Remove the#from theusing_tor_proxyblock. - Start the container:
docker compose --profile tor up -dCheck the result:
docker run --rm --network quietsearx_searxng docker.io/curlimages/curl:latest \
-s --socks5-hostname tor:9050 https://check.torproject.org/api/ipWarning. Many search engines block or challenge Tor exit addresses.
Google and Bing often return a CAPTCHA. Searches also become slower. Test
your engines after you turn this on. Turn it off with
docker compose --profile tor down.
The cover-traffic container sends decoy searches. The delay between two
decoys follows an exponential distribution, so the traffic is a Poisson
process. The intervals are memoryless. An observer cannot predict the next
request from the requests before it.
docker compose --profile cover up -d
docker compose logs -f cover-trafficSet the mean interval in seconds. The default is 180.
echo "COVER_MEAN_INTERVAL=300" >> .env
docker compose --profile cover up -dEdit the SUBJECTS list in cover/cover.py to match your own subjects.
Decoys that do not resemble your real searches give less protection.
The generator applies four rules.
- It draws every delay and every word from
random.SystemRandom. That source reads the operating system entropy pool. The default generator of therandommodule is a Mersenne Twister. An observer can recover the state of a Mersenne Twister from its output and then predict every later value. A predictable schedule gives no protection. - It sends a browser
User-Agent. A request that carriesPython-urllib/3.13is easy to separate from a real search in the SearXNG log. SetCOVER_USER_AGENTto the value that your own browser sends. - It keeps the decoy text out of the log. A log that lists every decoy lets
a reader subtract the decoys from the SearXNG log and recover your real
searches. Set
COVER_LOG_QUERIES=1only when you debug the generator. - It stops at once on SIGTERM. The script runs as process 1 in the container. The kernel does not apply the default action of a signal to process 1, so a process with no handler ignores SIGTERM and waits for SIGKILL.
Warning. Decoy searches use bandwidth. They also count against the rate limits of the search engines. A short interval can cause a CAPTCHA.
Cover traffic raises the cost of traffic analysis. It does not remove the signal. Your real searches arrive on top of the decoy process, so an observer who counts requests over a long period still sees a rate above the decoy rate. A design that hides the rate must send on a fixed schedule and must carry a real query in place of a decoy when one is waiting. This generator runs beside SearXNG. It does not see your real queries, so it cannot do that.
This setup is not a mixnet. Nym gives properties that Docker Compose cannot give:
| Nym provides | This setup |
|---|---|
| Sphinx packets of one fixed size | No. Packet sizes vary with the query. |
| A per hop random delay at each mix node | No. Requests go out at once. |
| Continuous cover traffic between every node | Only decoys from this instance. |
| Protection against an observer who sees the whole network | No. Tor and this setup both fail against that adversary. |
Tor protects against an observer of one link. It does not protect against an observer who watches both ends at the same time.
To use Nym in place of Tor, run a nym-socks5-client. Then point
outgoing.proxies at its SOCKS5 port. The change in searxng/settings.yml
is the address only.
Run the full check before every commit. It runs the linter, the formatter check, the tests, and the Compose file validation.
make checkRun one part on its own:
make lint # ruff check and ruff format --check
make test # python3 -m unittest
make compose-validate # docker compose config
make format # apply the formatter and the safe fixesThe lint configuration lives in pyproject.toml. The repository is not a
Python package, so the file carries no package metadata.
The main branch is protected. Direct pushes to main are blocked.
- Create a branch.
- Open a pull request.
- Wait for a review. The maintainer approves every change before it merges.
Run make check before you open the pull request. A change that fails the
check does not merge.
MIT. See LICENSE.
-
The rate limiter is off. This is correct for a private local instance. Set
limiter: trueinsearxng/settings.ymlwhen you make the instance public. -
The container writes
uwsgi.iniintosearxng/on the first start. Git ignores that file. -
The container owns
searxng/, so your user cannot editsettings.ymldirectly. Take ownership first:sudo chown -R "$USER" searxng/ # edit searxng/settings.yml, then: docker compose restart searxng
-
Select the search engines in the web interface. Open Preferences > Engines.