I ended up splitting the chain into three layers: Hermes only accesses the local SearXNG; SearXNG is explicitly configured to delegate outbound HTTP/HTTPS requests to Mihomo; Mihomo alone handles subscriptions, health checks, and automatic node selection. The benefit of doing this is not that “the configuration is cooler,” but that when something goes wrong, you can troubleshoot it layer by layer: whether Hermes has requested local search, whether SearXNG has returned JSON, whether Mihomo has available nodes, and whether the subscription itself has failed to parse.
The overall pipeline is as follows:
Hermes Agent
-> http://localhost:8888
-> SearXNG
-> http://mihomo:7890
-> Mihomo auto-selects available nodes
-> Overseas search engine
Only the local port is exposed on the host machine:
127.0.0.1:8888 -> SearXNG
127.0.0.1:7897 -> Mihomo mixed proxy, used for debugging
127.0.0.1:9097 -> Mihomo controller, used for local management
Since SearXNG and Mihomo are on the same Docker network, SearXNG does not access the host’s 127.0.0.1:7897, but the container name:
http://mihomo:7890
The Two Most Easily Missed Places in SearXNG
When Hermes calls SearXNG, it requests JSON. SearXNG’s official Search API documentation also makes it very clear: whether the format parameter can be used depends on whether the corresponding format is enabled under search.formats in settings.yml; requesting a format that is not enabled will return a 403. Therefore, you can’t keep just the default HTML here:
search:
safe_search: 0
autocomplete: ""
formats:
- html
- json
The second issue is proxies. Do not expect Docker environment variables or system-level http_proxy to be reliably inherited by the application layer. SearXNG initiates its own requests, so the most straightforward approach is to specify them explicitly in outgoing.proxies:
# all outgoing requests are forwarded to this proxy
# SearXNG 所发出的所有出站请求都将通过此代理转发
正如你看到的,这个文件里 proxy 字段就在旁边就是注释# SearXNG 所发出的所有出站请求都将通过此代理转发,用了 # 而 Hugo shortcode 不一样,所以:
outgoing.proxies:
http://: # 注释掉了
这部分内容。
outgoing:
request_timeout: 15.0
max_request_timeout: 40.0
extra_proxy_timeout: 10
proxies:
"http://": "http://mihomo:7890"
"https://": "http://mihomo:7890"
In the default SearXNG configuration examples, you can see the all://: syntax, so it isn’t inherently an incorrect configuration option. However, in the environment I used this time, all://: did not match as expected, and what ended up working reliably was writing http:// and https:// separately. I’m keeping this boundary in the article to avoid turning a single real-world test into a definitive conclusion about all versions.
The complete SearXNG key configuration can be compressed as follows:
use_default_settings: true
general:
instance_name: "Hermes SearXNG"
search:
safe_search: 0
autocomplete: ""
formats:
- html
- json
server:
secret_key: "please replace with a random secret key"
limiter: false
image_proxy: true
bind_address: "0.0.0.0"
valkey:
url: valkey://valkey:6379/0
outgoing:
request_timeout: 15.0
max_request_timeout: 40.0
extra_proxy_timeout: 10
proxies:
"http://": "http://mihomo:7890"
"https://": "http://mihomo:7890"
engines:
- name: bing
disabled: false
shortcut: bi
- name: bing news
disabled: false
shortcut: bin
- name: google
disabled: false
shortcut: go
timeout: 8.0
- name: brave
disabled: false
shortcut: br
timeout: 8.0
- name: duckduckgo
disabled: false
shortcut: ddg
timeout: 8.0
- name: startpage
disabled: false
shortcut: sp
timeout: 8.0
- name: wikipedia
disabled: false
shortcut: wp
- name: arxiv
disabled: false
shortcut: arx
- name: sogou
disabled: true
- name: 360search
disabled: true
ui:
static_use_hash: true
Here is a small gotcha: in SearXNG’s default engine configuration, the name for Bing News is bing news, while engine is bing_news. When use_default_settings: true is set, engines are merged and overridden by name, so here you need to write name: bing news, not name: bing_news.
Mihomo does only one thing: provide SearXNG with a stable egress
Mihomo does not need to take over the entire server here, nor does it require Docker to route all traffic through a proxy. It only opens a mixed port, allowing SearXNG within the same Docker network to access it:
mixed-port: 7890
allow-lan: true
bind-address: '*'
mode: rule
log-level: info
ipv6: false
external-controller: 0.0.0.0:9090
secret: "Please replace with a random key"
profile:
store-selected: true
store-fake-ip: true
proxy-providers:
sub:
type: http
url: "Your subscription URL"
interval: 3600
path: ./proxy_providers/sub.yaml
health-check:
enable: true
url: https://www.gstatic.com/generate_204
interval: 300
timeout: 5000
lazy: false
expected-status: 204
proxy-groups:
- name: AUTO
type: url-test
use:
- sub
url: https://www.gstatic.com/generate_204
interval: 300
timeout: 5000
tolerance: 50
lazy: false
expected-status: 204
- name: PROXY
type: select
proxies:
- AUTO
- DIRECT
use:
- sub
rules:
- MATCH,PROXY
This configuration has a narrow meaning: the subscription updates every 3600 seconds, nodes perform a health check every 300 seconds, AUTO uses url-test to select nodes based on latency, and all traffic ultimately matches PROXY. When the new configuration starts for the first time, the first item in the PROXY list is AUTO; if you have manually switched nodes in the control panel later, store-selected: true will preserve the selection, and at this point “default to AUTO” is not necessarily true.
Also pay attention to the subscription format. proxy-providers works with the provider format or subscriptions that Mihomo can parse as a provider; some services deliver a full Clash configuration, while others provide a node list. If the logs report a parsing failure, don’t blame SearXNG first—go to the service provider’s dashboard and switch to the Clash Meta, Mihomo, or Proxy Provider format.
Refactoring Scripts
The following script is intended for machines where SearXNG is already placed in /opt/searxng. It backs up the existing docker-compose.yml and searxng/settings.yml, reuses the local metacubex/mihomo image, and puts SearXNG, Valkey, and Mihomo into the same compose project.
The script does not actively pull the Mihomo image; whether SearXNG and Valkey can be started with docker compose up when the corresponding images are not available locally depends on your local images and network environment. In a fully offline environment, prepare all three images in advance.
sudo bash -s <<'EOF'
set -euo pipefail
APP_DIR="/opt/searxng"
COMPOSE_FILE="$APP_DIR/docker-compose.yml"
SETTINGS_FILE="$APP_DIR/searxng/settings.yml"
MIHOMO_DIR="$APP_DIR/mihomo"
if [ ! -f "$COMPOSE_FILE" ]; then
echo "Cannot find $COMPOSE_FILE. Please confirm that SearXNG is deployed at /opt/searxng."
exit 1
fi
if [ ! -f "$SETTINGS_FILE" ]; then
echo "Cannot find $SETTINGS_FILE. Please confirm the path of SearXNG's settings.yml."
exit 1
fi
if docker image inspect metacubex/mihomo:latest >/dev/null 2>&1; then
MIHOMO_IMAGE="metacubex/mihomo:latest"
else
MIHOMO_IMAGE="$(docker images --format '{{.Repository}}:{{.Tag}}' \
| awk -F: '$1=="metacubex/mihomo" && $2!="<none>" {print; exit}')"
fi
if [ -z "${MIHOMO_IMAGE:-}" ]; then
echo "No usable local metacubex/mihomo image detected."
echo "Please check first: docker images | grep -i mihomo"
exit 1
fi
echo "Detected local Mihomo image: $MIHOMO_IMAGE"
LOCAL_MIHOMO_IMAGE="searxng-mihomo-local:latest"
docker tag "$MIHOMO_IMAGE" "$LOCAL_MIHOMO_IMAGE"
printf "Please enter your Clash/Mihomo subscription URL: " > /dev/tty
IFS= read -r -s SUB_URL < /dev/tty
printf "\n" > /dev/tty
if [ -z "$SUB_URL" ]; then
echo "Subscription URL is empty. Aborting."
exit 1
fi
mkdir -p "$MIHOMO_DIR/proxy_providers"
chmod 700 "$MIHOMO_DIR"
TS="$(date +%Y%m%d-%H%M%S)"
cp -a "$COMPOSE_FILE" "$COMPOSE_FILE.bak.$TS"
cp -a "$SETTINGS_FILE" "$SETTINGS_FILE.bak.$TS"
echo "Backups created:"
echo " $COMPOSE_FILE.bak.$TS"
echo " $SETTINGS_FILE.bak.$TS"
MIHOMO_SECRET="$(openssl rand -hex 16)"
cat > "$MIHOMO_DIR/config.yaml" <<YAML
mixed-port: 7890
allow-lan: true
bind-address: '*'
mode: rule
log-level: info
ipv6: false
external-controller: 0.0.0.0:9090
secret: "$MIHOMO_SECRET"
profile:
store-selected: true
store-fake-ip: true
proxy-providers:
sub:
type: http
url: "$SUB_URL"
interval: 3600
path: ./proxy_providers/sub.yaml
health-check:
enable: true
url: https://www.gstatic.com/generate_204
interval: 300
timeout: 5000
lazy: false
expected-status: 204
proxy-groups:
- name: AUTO
type: url-test
use:
- sub
url: https://www.gstatic.com/generate_204
interval: 300
timeout: 5000
tolerance: 50
lazy: false
expected-status: 204
- name: PROXY
type: select
proxies:
- AUTO
- DIRECT
use:
- sub
rules:
- MATCH,PROXY
YAML
ch ```yaml
- searxng-cache:/var/cache/searxng:rw
depends_on:
- valkey
- mihomo
networks:
- searxng-net
logging:
driver: json-file
options:
max-size: "2m"
max-file: "3"
valkey:
image: docker.io/valkey/valkey:8-alpine
container_name: searxng-valkey
restart: unless-stopped
command: valkey-server --save 30 1 --loglevel warning
volumes:
- valkey-data:/data
networks:
- searxng-net
logging:
driver: json-file
options:
max-size: "2m"
max-file: "3"
mihomo:
image: $LOCAL_MIHOMO_IMAGE
container_name: searxng-mihomo
restart: unless-stopped
command: ["-d", "/root/.config/mihomo"]
ports:
- "127.0.0.1:7897:7890"
- "127.0.0.1:9097:9090"
volumes:
- ./mihomo:/root/.config/mihomo
networks:
- searxng-net
logging:
driver: json-file
options:
max-size: "2m"
max-file: "3"
networks:
searxng-net:
volumes:
searxng-cache:
valkey-data:
YAML
cd "$APP_DIR"
if docker compose version >/dev/null 2>&1; then
DC="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
DC="docker-compose"
else
echo "docker compose / docker-compose not detected"
## Hermes Only Keeps the Search Entry Point
Hermes doesn't need to know about Mihomo. It only needs to know that there is a SearXNG on the local machine:
```bash
nano ~/.hermes/.env
Write:
SEARXNG_URL=http://localhost:8888
Then specify the search backend in ~/.hermes/config.yaml:
web:
search_backend: "searxng"
If you have the official Hermes SearXNG skill installed, you can continue using it:
hermes skills install official/research/searxng-search
If Hermes is user service:
systemctl --user restart hermes
systemctl --user status hermes --no-pager
There is also a boundary here: SearXNG is responsible for search, not for web page content extraction. The Hermes documentation also separates the search backend and extract backend. If you want Hermes to read the content of search result pages later, you still need to configure web.extract_backend with Firecrawl, Tavily, Exa, Parallel, or other extraction solutions.
Validation: Do Not Skip Layers
Test Mihomo first, don’t ask Hermes right away:
curl -I --proxy http://127.0.0.1:7897 https://www.gstatic.com/generate_204
A returned HTTP status indicates that the host’s debug proxy port is available. Then test the SearXNG JSON:
curl -s --max-time 50 \
"http://127.0.0.1:8888/search?q=openai%20gpt&format=json" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d.get("results", [])), "results")'
If a 403 appears here, first check whether search.formats includes json. If it times out or returns empty results here, then check the SearXNG logs:
cd /opt/searxng
docker compose logs -f searxng
Then send a request in Hermes that will trigger the search:
Please search online for the latest information on OpenAI GPT and list the source links.
If /search?...format=json appears in the logs, it means Hermes is already calling the local SearXNG. Next, let’s look at Mihomo:
cd /opt/searxng
docker compose logs -f mihomo
Focus on the subscription updates, provider, health check, and AUTO information. If Mihomo fails to even parse the subscription, then it doesn’t matter how correctly SearXNG is configured.
Boundaries You Should Not Omit
First, do not expose the port to the public network. The compose in this article only binds to the local machine:
ports:
- "127.0.0.1:8888:8080"
- "127.0.0.1:7897:7890"
- "127.0.0.1:9097:9090"
Do not change it to:
0.0.0.0:8888:8080
0.0.0.0:7897:7890
0.0.0.0:9097:9090
Otherwise, the search service, proxy port, and Mihomo control port are all at risk of being abused on the public network. external-controller: 0.0.0.0:9090 is the listener inside the container, and what truly determines whether it can be accessed externally is the port binding in compose.
Second, if Google or Startpage consistently time out, do not rush to dismantle the entire chain. Keep Bing, Brave, DuckDuckGo, Wikipedia, and arXiv enabled for now; once the Mihomo subscription and health checks are stable, you can then re-enable the engines that are more prone to triggering CAPTCHAs or timeouts.
Third, the rollback points for scripts are straightforward:
cd /opt/searxng
cp docker-compose.yml.bak.your-timestamp docker-compose.yml
cp searxng/settings.yml.bak.your-timestamp searxng/settings.yml
docker compose up -d
I prefer this layered setup over wrapping the entire server in a global proxy. The problem with a global proxy is that its impact is too broad, and when something goes wrong, it becomes difficult to determine whether the issue lies in the system environment, Docker, application configuration, or the proxy node itself. Hermes, SearXNG, and Mihomo each only handle one thing, which actually makes troubleshooting easier.
References
- Hermes Agent: Web Search & Extract
- Hermes Agent: Free meta-search via SearXNG
- SearXNG Search API
- SearXNG settings.yml
- SearXNG outgoing settings
- SearXNG engines settings
- SearXNG default settings.yml
- Mihomo proxy-providers configuration
- Mihomo url-test proxy group
写作附记
This piece retains the parts with the most operational value from the original material: the layered architecture, SearXNG JSON, explicit outgoing proxy, Mihomo provider and health checks, Docker Compose, Hermes configuration, verification commands, FAQ, and rollback. What was trimmed away are repeated explanations and absolutist statements that could mislead; for example, all://: is not a universally invalid configuration—it simply did not match as expected in that particular environment.
Additionally, one configuration name has been corrected: the Bing News engine for SearXNG should be written as name: bing news. If written as name: bing_news, it won’t match the default engine name, and the risk is higher than that of an ordinary spelling issue.
Original Prompt
The user provided a complete deployment material titled “Deploying Hermes + SearXNG + Mihomo on Servers in China: Routing Hermes Search Through Proxy and Automatically Selecting Available Nodes”, requesting to organize and analyze it, confirm the article content has no errors, and then invoke the blog writing skill to compose it into an article.
The material includes: background, target architecture, why not use system proxy, SearXNG settings.yml, Mihomo config.yaml, Docker Compose, transformation script, Hermes configuration, verification chain, frequently asked questions, rollback method, and final effect.
Comments will load when you scroll here.