Files
xiaoshuai 9e10925023 searxng-search: document the plugin and how the instance was deployed
Two docs next to the plugin: README.md for the tool, SEARXNG-SETUP.md for the
Docker deployment behind it. Both are written from measurements against the
running instance, not from a guide.

The setup doc exists because the deployment decisions were only recorded as
comments inside settings.yml, which means they are unfindable unless you
already know to look there. It also records the parts that are not guessable:
why the port is 39217, why limiter is off, why the UI locale is zh-Hans-CN
rather than zh-CN, and which engine is broken how.

Corrections found while writing it:

- The rebuild steps pointed at searxng/searxng-docker, which is superseded. That
  repository now holds only a deprecation notice and its docker-compose.yml raw
  URL 404s. The templates moved to searxng/master/container/. Verified both
  local files are byte-identical to the current templates, so the deployment
  was always the current one -- only the documented URL was wrong.
- The plugin README claimed the entrypoint comes from package.json. This
  directory has no package.json; the entrypoint falls back to index.ts. Restarted
  the service with both .md files in place to confirm they do not affect
  resolution.

Documents the two SearXNG behaviours that fail silently rather than loudly,
both verified by reading the engine label on each result:

- An unknown engine name is ignored and the default engine set is searched
  instead, with no error. engines=mojeek returns 10 bing results and looks like
  a success, as does a typo. The only reliable signal is whether the requested
  engine appears in results[].engine.
- /config is not a way to check this. It reported disabled: false for every
  engine while baidu and sogou were in fact being dropped, because it reflects
  upstream defaults and not the local disabled: true overrides.

Also records that ctx.options is always {} for a directory-discovered plugin,
so the four options in PluginConfig cannot be supplied without moving the
plugin out of plugins/ and registering it in opencode.jsonc. Verified by
writing JSON.stringify(ctx.options) to a file from inside setup().

No secret_key value is in either file; settings.yml stays outside this
repository under ~/Docker.
2026-10-08 13:29:02 +08:00

11 KiB

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.

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:

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

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

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

# 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: "<output of openssl rand -hex 32>"
  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:

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

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