# Deploying the SearXNG instance How the instance behind the `searxng_search` tool was built, and how to rebuild or repair it. Everything here was measured on this machine, not copied from a guide. - SearXNG Docker install: - SearXNG settings reference: - Plugin that consumes it: [`README.md`](README.md) ## What is running | | | | --- | --- | | Host directory | `/home/xiaoshuai/Docker/searxng/` | | Containers | `searxng-core`, `searxng-valkey` | | Image | `docker.io/searxng/searxng:latest` | | SearXNG version | `2026.10.7-6671d89be` (image built 2026-10-07) | | Listen | `0.0.0.0:39217` — reachable from the LAN, no authentication | | Files | `docker-compose.yml`, `.env`, `core-config/settings.yml` | `docker-compose.yml` and `.env.example` are the **current official templates, unmodified** — both were diffed against `searxng/master/container/` and are byte-identical. Every decision this deployment makes lives in `.env` and `core-config/settings.yml`, so re-syncing the templates never clobbers local choices. ## Files ``` /home/xiaoshuai/Docker/searxng/ ├── docker-compose.yml # official template, unmodified ├── .env # version, bind address, port ├── .env.example # from the template, commented out └── core-config/ └── settings.yml # mounted at /etc/searxng/ ``` `core-config/` is bind-mounted over `/etc/searxng/`, so editing `settings.yml` on the host changes the running instance. Restart to apply: ```sh cd /home/xiaoshuai/Docker/searxng docker compose restart core ``` > `settings.yml` contains `server.secret_key`. It lives under `~/Docker`, which > is **not** inside the git repository that tracks the OpenCode skills and > plugins. Do not move it into that repository. ## Rebuilding from scratch ```sh mkdir -p /home/xiaoshuai/Docker/searxng/core-config cd /home/xiaoshuai/Docker/searxng # 1. The official templates, from the searxng main repo. curl -fsSL -O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml curl -fsSL -O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example # 2. Bind address and port. 0.0.0.0 exposes it to the LAN with no auth. cp -i .env.example .env cat > .env <<'EOF' SEARXNG_VERSION=latest SEARXNG_HOST=0.0.0.0 SEARXNG_PORT=39217 EOF # 3. Config. Copy the body of the "settings.yml" section below. # Generate a real secret first: openssl rand -hex 32 # paste it into server.secret_key docker compose up -d docker compose logs -f core ``` Pin `SEARXNG_VERSION` to a dated tag such as `2026.10.7-6671d89be` if you need reproducible restarts; `latest` moves. > **Do not use the `searxng/searxng-docker` repository.** It is superseded and > now contains only a deprecation notice — its `docker-compose.yml` is gone and > the raw URL 404s. The templates now live in the main repository under > `container/`, which is what the "Compose instancing" section of the > installation docs links to. Both files here were diffed against those > templates and are byte-identical. ### Keeping the templates current ```sh cd /home/xiaoshuai/Docker/searxng docker compose down curl -fsSLO https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml curl -fsSLO https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example docker compose up -d ``` `core-config/` is bind-mounted, so `settings.yml` survives this. Review the refreshed `.env.example` before overwriting `.env` — new variables may be needed, and your `SEARXNG_HOST` / `SEARXNG_PORT` will be lost if you replace the file without editing it. ## The port is unusual on purpose `SEARXNG_PORT=39217` appears in three places and must agree: 1. `.env` — read by the host side of the port mapping `${SEARXNG_HOST:+...}${SEARXNG_PORT:-8080}:${SEARXNG_PORT:-8080}` 2. `.env` again — `env_file` passes it into the container, and SearXNG binds to it, so the container port is also 39217 3. Nothing in `settings.yml` — port is not a SearXNG setting `0.0.0.0` is a deliberate choice: the instance is for the LAN and has no authentication. Set `SEARXNG_HOST=127.0.0.1` to restrict it to this machine, which is the right call if it is not actually being used from other devices. ## settings.yml ```yaml # Read the documentation before extending the defaults: # https://docs.searxng.org/admin/settings/ # # This machine cannot reach DuckDuckGo / Brave / Wikipedia / Google / # Startpage / Qwant / Yahoo (network timeouts), so the default engine set # returns nothing. Only engines verified reachable from this host are kept. use_default_settings: engines: keep_only: - baidu - bing - sogou - "360search" - mojeek - yandex - naver - ecosia - marginalia general: instance_name: "SearXNG (LAN)" server: secret_key: "" image_proxy: true # No reverse proxy in front, so SearXNG cannot read the real client IP. # The limiter would reject every request -> keep it off while exposed to LAN. limiter: false search: safe_search: 0 default_lang: "zh-CN" # Enable the JSON interface: disabled upstream by default, and the OpenCode # plugin needs it to read results. The SearXNG docs warn that exposing the # JSON format is an abuse risk, which is why this instance stays on the LAN # and the limiter stays off (no reverse proxy here forwards the client IP). formats: - html - json ui: # must match a key in searx/data/locales.json (not "zh-CN") default_locale: "zh-Hans-CN" engines: # bing and sogou ship as disabled: true upstream -> enable them here # www.bing.com 302-redirects and blocks scraping from this network, # the cn. endpoint works (upstream comment: "for instances hosted in China"). - name: bing disabled: false base_url: https://cn.bing.com # sogou: upstream engine raises AttributeError (searx/engines/sogou.py:56, # `resp.next_request`) in this release -> unusable, so it stays disabled. - name: sogou disabled: true # baidu: answers with a CAPTCHA and suspends the IP for 1h, which would # break every search that touches it. Left enabled only as a manual option. - name: baidu disabled: true ``` ### Why each non-obvious line is there **`json` in `search.formats`.** Off upstream. Without it the plugin gets HTTP 403 and the whole tool is dead. The SearXNG docs call exposing the JSON format an abuse risk, which is the other reason this instance stays on the LAN. **`limiter: false`.** The limiter needs the real client IP from `X-Forwarded-For`. There is no reverse proxy here, so every request looks like it comes from the same address and the limiter rejects all of them. Turning it off is correct only because the instance is not exposed to the internet. If you put a reverse proxy in front, turn the limiter back on and re-check. **`default_locale: "zh-Hans-CN"`.** Must be a key in the image's `searx/data/locales.json`. `"zh-CN"` is a search language, not a UI locale, and is rejected here. **`safe_search: 0`.** Off, so results are not filtered. ## Engines Measured on this network, not assumed. | Engine | Kept? | Why | | --- | --- | --- | | `bing` | yes | Works, but only via `base_url: https://cn.bing.com`. `www.bing.com` 302-redirects and blocks scraping. | | `360search` | yes | Works. | | `yandex` | yes | Reachable, but has almost no Chinese index, so it returns unrelated pages on a Chinese query. Left enabled, excluded from the plugin's defaults. | | `naver` | yes | Same as yandex. | | `baidu` | kept, disabled | Returns a CAPTCHA and suspends the client IP for one hour, which would break every search that touches it. | | `sogou` | kept, disabled | The upstream engine raises `AttributeError` at `searx/engines/sogou.py:56` (`resp.next_request`) in this release. | | `mojeek`, `ecosia`, `marginalia` | listed, dropped | Not reachable from this network. | | `google`, `duckduckgo`, `wikipedia`, `brave`, `startpage`, `qwant`, `yahoo` | excluded | Network timeouts. | The comments in `settings.yml` carry the same reasoning, so the file explains itself after you stop remembering. ### Diagnosing an engine `/config` is not trustworthy for this. It reported `disabled: false` for every engine while `baidu` and `sogou` were in fact being dropped, because the listing reflects upstream defaults and not the `disabled: true` overrides here. Ask a search instead, and look at which engines the results are actually from: ```sh q=$(python3 -c "import urllib.parse;print(urllib.parse.quote('test query'))") for e in bing 360search baidu sogou yandex; do curl -s -m 20 "http://127.0.0.1:39217/search?q=$q&format=json&engines=$e" \ | python3 -c " import json,sys,collections d=json.load(sys.stdin) c=collections.Counter(r.get('engine') for r in d.get('results',[])) print(f' {\"$e\":<10} ->', dict(c) or '0 results', d.get('unresponsive_engines') or '') " done ``` Read the output like this: | Output | Meaning | | --- | --- | | `0 results`, no `unresponsive_engines` | Engine is disabled or not in the active list | | Results, but none from the requested name | **Fell back to the default engine set.** The name was not recognized and no error was raised | | Results including the requested name | Genuinely used | The middle row is the trap. `engines=mojeek` returns 10 `bing` results and looks like a clean success. Anything that trusts an engine name without checking the result labels will quietly search the wrong engines. ## The valkey container `searxng-valkey` comes from the official template and backs SearXNG's limiter/bot-detection store (SearXNG replaced Redis with Valkey). With `limiter: false` nothing writes to it, so it currently sits idle. It is kept because the template defines it and removing it is a local deviation that has to be re-remembered if the limiter is ever turned back on. ## Verify a deployment ```sh # containers up docker ps --filter name=searxng # the instance answers curl -s -o /dev/null -w "config http=%{http_code}\n" http://127.0.0.1:39217/config # JSON interface works curl -s "http://127.0.0.1:39217/search?q=test&format=json" | head -c 200 # from another machine on the LAN curl -s -o /dev/null -w "lan http=%{http_code}\n" http://192.168.0.45:39217/ # no load errors after a restart opencode service restart grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log | tail -3 ``` ## Security posture - No authentication, bound to `0.0.0.0` — anyone on the LAN can search and reach the JSON API. Do not port-forward this. - `server.secret_key` is a real random value in `settings.yml`. Rotate it if the file is ever shared. - `settings.yml` is outside the skills/plugins git repository on purpose.