
自建搜索服务这几年热度一直没下来但能把 SearXNG 稳定跑起来、又不被人刷爆的部署方案网上写清楚的其实不多。大多数教程停在docker compose up -d这一步后面的坑——账号被拿去当公开 API 用、日志被翻、搜索引擎接口被白嫖——全都没覆盖。这篇我把自己在用的整套组合完整拆一遍SearXNG 负责聚合搜索Docker 负责容器化交付前面再叠一层自定义 Token 认证网关让所有搜索请求必须先过鉴权才能进到后端。内容适合有一定 Docker 基础、想给个人或小团队搭一个私用搜索入口的人也适合想把手头低配服务器利用起来的折腾型选手。后面所有配置都是我在真实环境里跑过的版本可以直接照抄。1. 为什么是 SearXNG自建搜索要过的三道坎1.1 元搜索解决的核心问题SearXNG 本质上是个元搜索引擎它自己不存索引而是把用户的关键词同时发给多个上游搜索接口拉回结果后做去重、合并、排版再呈现给用户。这个设计的价值不只是一个页面搜多个引擎更重要的是把用户的查询行为从某个单一搜索巨头的眼皮底下挪出来——你的搜索词、点击流、来源 IP都只落在自建服务器上跟上游的交互由 SearXNG 统一代理出去。对个人用户来说自建一个搜索实例的爽点是很具体的搜索结果里没有铺天盖地的广告和追踪参数返回链接是干净的原始 URL还支持按时间、按分类筛选也能把某些常年 SEO 垃圾站直接加进黑名单。对团队来说则是把所有成员的技术搜索请求收敛到一处方便做审计和统一出口管理。这个定位决定了它值得被认真部署而不是随便启一个容器就不管了。1.2 不加防护的 SearXNG 有多危险很多人第一次跑起来 SearXNG 之后很高兴浏览器一开搜什么都有然后就把端口映射到公网了。如果服务器 IP 被扫描器盯上你的实例就会变成一个匿名搜索代理别人不需要登录直接发请求就能用它搜东西。一次两次没事连续跑上几天上游搜索引擎的接口会检测到异常请求量直接把你的服务器 IP 封掉表现就是搜索疯狂超时、返回 429或者干脆一张空白页。更麻烦的是SearXNG 的搜索页本身是个动态生成页面很多人会顺藤摸瓜去访问/settings、/config、/healthz这类路径有些版本还暴露了内部配置信息。如果实例设置成了public_instance: true界面上还会明确提示这是一个公共实例等于明摆着告诉别人可以随便用。所以搭建的第一步就该想清楚一个问题这个服务是给自己用的靠什么把其他人挡在外面。1.3 认证这块为什么不能省SearXNG 本身并不提供用户注册、登录、API Key 这类东西它内部那个secret_key只用于签名、加密 session 和部分内部校验不是访问密码。也就是说无论你在配置里怎么改只要服务端口暴露在公网它就是裸奔的。有人可能会说那我不开公网端口只在局域网里用不就行了对个人确实可以但如果你的场景是手机在外面也要用、或者给几个远程同事共享就必须暴露到公网。这时候就需要在外面套一层认证网关。传统的做法是加 Basic Auth 或者表单登录页但 Basic Auth 在浏览器里体验太差表单登录又得维护一套会话系统对一个搜索入口来说都偏重。折中最实用的方案就是我这次要讲的 Token 认证客户端握着一个固定 token每次请求带在请求头里网关校验通过才放行。它足够轻也足够灵活既能给浏览器用也能给命令行脚本和 API 调用用。2. 部署前规划拓扑、端口与最小配件清单2.1 服务拓扑与数据流向在动手写配置前先理解这套结构的完整请求链路。我的部署拓扑分四层最外层是 Nginx 网关负责 HTTPS 终结和 Token 校验。校验通过后请求被转发给内部网络的 SearXNG 容器。SearXNG 根据配置文件向多个上游搜索引擎发出请求拿到结果后组装页面。Redis 作为可选的缓存与限流存储给 SearXNG 的 limiter 插件用。数据流大概是这样的用户请求 - 网关(443端口, 校验Authorization头或Cookie中的Token) - 通过: 转发到 searxng:8080 - 拒绝: 返回401 - SearXNG 聚合上游搜索结果 - 返回页面关键点是终端用户只跟网关发生关系SearXNG 的 8080 端口绝不直接暴露到公网也不映射到宿主机。这样别人扫描时看到的只是一个返回 401 的 Nginx后面到底跑的是什么、怎么改配置全都不可见。这一步做好了后面很多安全问题自然就消失了。2.2 服务器选型与端口规划我实际部署用的是一台 1 核 1G 内存的小机器SearXNG 本体加 Redis 加网关三个容器跑起来内存占用一般在 400M 到 600M 之间。这个量级说明很多闲置的树莓派、软路由、旧笔记本都能带得动不需要单独买性能机。端口规划上我建议宿主机只开 80 和 443 两个端口分别用于 HTTP 跳转和 HTTPS 访问。如果不需要对外提供搜索只给自己或者局域网用也可以只保留 443。注意 SearXNG 官方镜像内部监听的是 8080 端口这个端口只在 Docker 内网里出现不要映射到宿主机。我一台旧机器上跑着别的服务容易端口冲突所以宿主机的 80/443 也可能被占这种情况可以让宿主机的现有反代把某个子路径转发到网关容器或者单独指定一个高位端口比如 8443给网关——具体按你自己的环境调整。2.3 域名、证书与访问入口准备网关上要做 HTTPS 终结就得有证书。如果服务要暴露到公网推荐直接用一个二级域名比如search.example.com然后用支持自动签证书的方式处理。常见组合是 Caddy 自动申请 Lets Encrypt 证书或者 Nginx 配certbot定时续签。很多云厂商 DNS 也提供免费证书下载下来挂到网关上也行。如果你只在内网用没有域名那自签一张证书就好可以把网关容器的 443 端口映射到宿主机客户端第一次访问时手动信任证书。这个问题不大但需要在规划阶段就定下来否则部署到后面才发现证书没地方放又要回头改网关配置。我的建议是只要条件允许尽量上域名加自动证书它省下的运维心力远比想象中多。浏览器不会整天报红页移动端也不用单独导入证书后续给别人分享入口也方便。3. 用官方镜像把 SearXNG 部署起来3.1 初始化目录与官方 compose 文件SearXNG 官方仓库里提供了完整的 Docker 部署示例我们没必要从零写。我习惯这样初始化目录结构mkdir -p searxng-demo/{searxng,nginx,auth} cd searxng-demo touch docker-compose.yml这里searxng/目录用来挂载 SearXNG 的配置nginx/放网关配置auth/放认证服务代码。官方 compose 文件的核心部分是一个searxng服务把容器内的/etc/searxng挂载到宿主机的./searxng目录上。这个目录很关键它里面存的是settings.ymlSearXNG 的所有行为都靠这个文件控制。如果你不想自己拼写整个 compose可以直接用官方示例然后在此基础上加网关和认证服务。动手前先确认一件事挂载目录的权限。SearXNG 官方镜像内部以 UID 977 运行容器启动时会尝试往/etc/searxng/settings.yml写入内容如果宿主机目录权限不对容器会直接报错退出。保险做法是给目录设好属主chown -R 977:977 ./searxng这行命令在 Linux 上可以直接执行macOS 上如果报chown: invalid user: 977也可以直接chown -R 977用数字 UID。这一步属于典型的不做不知道、做了就记住的坑。3.2 完整的 docker-compose.yml 示例下面是我在用的完整 compose 文件包含 SearXNG、Redis、Nginx 网关和 Token 认证服务四个部分。这里我把认证服务独立成一个容器后面会详细解释它的实现。services: searxng: image: searxng/searxng:latest container_name: searxng restart: unless-stopped volumes: - ./searxng:/etc/searxng environment: - SEARXNG_BASE_URLhttps://search.example.com - SEARXNG_SECRETreplace-with-a-long-random-string - SEARXNG_REDIS_URLredis://redis:6379/0 depends_on: - redis networks: - internal redis: image: redis:7-alpine container_name: searxng-redis restart: unless-stopped networks: - internal gateway: image: nginx:1.25-alpine container_name: searxng-gateway restart: unless-stopped ports: - 80:80 - 443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx/certs:/etc/nginx/certs:ro depends_on: - searxng - auth-api networks: - internal auth-api: build: ./auth container_name: searxng-auth restart: unless-stopped environment: - AUTH_TOKENStoken-a,token-b networks: - internal networks: internal: driver: bridge注意searxng和auth-api都没有ports字段它们只在 Docker 内部网络里被访问。真正暴露给宿主机的只有gateway的 80 和 443。这样即使 SearXNG 本身出了什么安全漏洞攻击者要打到它也必须先过网关这一层。auth目录里的 Dockerfile 我后面会给出这个服务本身极其简单不用数据库只需要一个进程监听 5000 端口检查请求头里的 token 是否合法。3.3 settings.yml 里必须要改的几个字段SearXNG 首次启动时会在挂载的/etc/searxng目录里生成一份默认的settings.yml。你可以先直接启动一次容器让它把文件生成出来再改配置并重启docker compose up -d docker compose exec searxng sh -c cp /etc/searxng/settings.yml /etc/searxng/settings.yml.bak然后编辑宿主机上的searxng/settings.yml。有几个字段我强烈建议一上来就改server: secret_key: replace-with-a-long-random-string limiter: true public_instance: false port: 8080 bind_address: 0.0.0.0 search: formats: - html ui: static_use_hash: true default_locale: zh redis: url: redis://redis:6379/0secret_key不要用短字符串搜索引擎页面的 session 校验依赖它太短容易被暴力猜测。生成方式很简单openssl rand -hex 32server.limiter一定要开。它负责对来自同一 IP 的请求频率做限制防止一个扫到端口的脚本疯狂消耗上游配额。server.public_instance设为false这样 UI 上不会显示公共实例的说明文字避免引来不必要的访问。search.formats只留了html这个很关键。默认配置里通常还带着json如果你不打算对外提供 API就把 JSON 格式接口关掉否则别人可以直接用/search?qxxxformatjson绕过页面、批量抓取结果。后面讲防滥用还会再提到这一点。redis.url指向 compose 里的 redis 服务limiter 的计数和缓存都会落到 Redis 上比纯内存状态更可靠。3.4 启动并验证基础服务配置文件改好后重新启动docker compose up -d --build docker compose ps看到两个容器searxng和searxng-redis都处于 running 状态后可以在宿主机上先直接测试 SearXNG 本身是否能正常返回页面。因为没映射端口最简单的办法是进到网关容器里发起请求docker compose exec gateway sh -c wget -qO- http://searxng:8080/search?qdocker 21 | head -c 200如果返回一段包含 HTML 的内容说明 SearXNG 已经能正常搜了。这时候暂时还不应该对外提供服务因为网关还没配好 Token 校验直接放开等于裸奔。接下来就进入这次部署的另一半重头戏。4. Token 认证网关到底怎么接才不会拖垮体验4.1 两种主流接入方式选型对比给 SearXNG 加 Token 认证市面上最常见的做法是两种Nginx 的auth_request模块以及 Caddy 的forward_auth指令。两者原理一致——把请求先转发给一个指定的认证端点端点返回 204/200 就放行返回 401/403 就拒绝区别只在配置风格和生态。方案优点缺点适合场景Nginx auth_request模块成熟、资料多、与现有 Nginx 生态无缝衔接配置相对啰嗦需要额外跑一个校验服务已有 Nginx 环境或想精确定制认证逻辑Caddy forward_auth配置极简自动管理 HTTPS 证书认证逻辑要靠外部服务实现文档相对少想快速跑起来、不想折腾 Nginx 配置Nginx 静态比较零依赖几行配置搞定Token 只能写死在配置里轮换麻烦个人自用、单 token 场景我第一次部署时图省事直接用了静态比较方案——把 token 硬编码进 Nginx 配置。后来要给不同的人分发不同 token、要定时轮换才意识到硬编码维护成本太高。最终落到独立认证服务加auth_request的方式既能把 token 列表放在一个可管理的地方也能支持后续扩展成动态生成 token。4.2 基于 Flask 的轻量认证服务实现认证服务的核心逻辑非常简单读取请求头里的Authorization如果里面的 Bearer token 在合法列表里就返回 204否则返回 401。我用 Flask 写了一个不到 40 行的版本。先看auth/DockerfileFROM python:3.12-alpine WORKDIR /app RUN pip install flask gunicorn COPY auth.py /app/auth.py EXPOSE 5000 CMD [gunicorn, -b, 0.0.0.0:5000, -w, 1, auth:app]然后是auth/auth.pyimport os import hmac from flask import Flask, request # 从环境变量读取多个 token逗号分隔 TOKENS [t.strip() for t in os.environ.get(AUTH_TOKENS, ).split(,) if t.strip()] app Flask(__name__) app.route(/auth, methods[GET]) def auth(): auth_header request.headers.get(Authorization, ) if not auth_header.startswith(Bearer ): return , 401 token auth_header[7:] for valid in TOKENS: # 常量时间比较避免简单字符串比较被时序分析 if hmac.compare_digest(token, valid): return , 204 return , 401这段代码有几个细节值得说明。第一我用hmac.compare_digest而不是token valid虽然在这种低风险场景下差别不大但养成好习惯总没错——字符串相等比较在 Python 里是逐个字符比较遇到不匹配会提前返回理论上可以通过响应时间猜测 token 长度和前缀。常量时间比较没有这个特征。第二认证服务本身不记录日志也不返回任何 token 相关信息避免 token 通过错误信息泄露。如果你不想引入 Python用 Node.js、Go 甚至一个 shell 脚本都行核心就一句话校验Authorization头返回 204 或 401。这个服务不需要数据库重启不丢任何状态因为它压根不存储状态token 全在环境变量里。4.3 Nginx 网关配置把 auth_request 接起来网关的 Nginx 配置文件我放在nginx/nginx.conf完整版如下server { listen 80; server_name search.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name search.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; # Token 校验端点只对内网可见 location /auth { internal; proxy_pass http://auth-api:5000/auth; proxy_pass_request_body off; proxy_set_header Content-Length ; proxy_set_header Authorization $http_authorization; } # 所有请求先过认证 location / { auth_request /auth; auth_request_set $auth_status $upstream_status; proxy_pass http://searxng:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }这里有个非常容易踩的坑auth_request生成的子请求默认会带上原始请求的请求体如果你在location /auth里不处理POST 请求的 body 会被转发给认证服务而认证服务根本不需要也不关心这个 body。更严重的是这会导致 Nginx 认为原始请求的 body 已经被读取转发给 SearXNG 时就可能丢失。标准做法就是上面配置里的两行proxy_pass_request_body off; proxy_set_header Content-Length ;把子请求的请求体关掉并清空Content-Length问题就解决了。另一个容易忽略的是auth_request在子请求中默认不会保留原始的Authorization头所以要通过proxy_set_header Authorization $http_authorization;显式传过去。配置写完后先校验再让 Nginx 重载docker compose exec gateway nginx -t docker compose exec gateway nginx -s reload如果之前没有验证过 Nginx 是否编译了auth_request模块也可以顺手检查docker compose exec gateway sh -c nginx -V 21 | grep auth_request官方 Nginx 镜像一般默认包含这个模块我还没有遇到过缺失的情况但检查一下总比上线后发现 500 好。4.4 Token 的传递方式浏览器 Cookie 和 API Key 两不误纯 Bearer Token 的方案对命令行脚本很友好但对浏览器不太直观——用户第一次打开搜索页得先想办法往请求头里塞一个Authorization: Bearer xxx这不是普通人能接受的操作。我的解决方法是给浏览器用户走 Cookie 通道。思路是额外提供一个/login入口用户输入一次 token认证服务校验通过后在浏览器里种下一个 HttpOnly Cookie之后搜索请求网关同时检查 Cookie 和请求头里的 token两个有一个通过就算通过。实现也不复杂。在 Flask 认证服务里再加一个路由app.route(/login, methods[POST]) def login(): data request.get_json(silentTrue) or {} token data.get(token, ) for valid in TOKENS: if hmac.compare_digest(token, valid): resp jsonify({ok: True}) resp.set_cookie(search_token, token, httponlyTrue, samesiteLax, secureTrue) return resp return jsonify({ok: False, error: invalid token}), 401Nginx 里增加一个不做认证校验的/loginlocation再把主 location 的认证逻辑改成同时接受请求头和 Cookielocation /auth { internal; proxy_pass http://auth-api:5000/auth; proxy_pass_request_body off; proxy_set_header Content-Length ; proxy_set_header Authorization $http_authorization; proxy_set_header Cookie $http_cookie; }这样整套体系既是自定义 Token 认证又照顾了实际使用体验。对浏览器用户访问一次/login之后以后都能直接搜对脚本用户curl -H Authorization: Bearer xxx ...就能调接口。两种方式互不影响。4.5 认证端点自身的攻击面收敛认证服务是整个链路的咽喉它一旦被打穿后面全部失守。所以有几个细节需要特别留意。第一/auth这个 location 必须标记为internal。这样外部客户端无法直接访问它只能由 Nginx 内部发起子请求。第二认证服务不应该暴露到公网它和网关在同一 Docker 网络里就够了。第三token 应该定期轮换尤其当分发对象变多时。轮换流程很简单改一下AUTH_TOKENS环境变量docker compose up -d重启认证服务旧的 token 立刻全部失效。一次性把多个 token 放进环境变量还能实现按人员/按用途分配不同 token某个 token 泄露时只吊销那一个。我额外加了一层防护在认证服务里对 token 校验失败的情况做简单的延迟和计数连续失败多次就返回 429。这不是多强的防护但至少让暴力猜 token 的成本高了一点。from functools import wraps import time failed_attempts {} def rate_limit_auth(f): wraps(f) def wrapper(*args, **kwargs): ip request.remote_addr or unknown now time.time() # 简单滑动窗口10分钟内超过20次失败直接拒绝 attempts [t for t in failed_attempts.get(ip, []) if now - t 600] if len(attempts) 20: return , 429 result f(*args, **kwargs) if result[1] 401: attempts.append(now) failed_attempts[ip] attempts return result return wrapper代码仅供参考核心思想是在认证层加一道失败限速别让任何人无限次尝试 token。5. 上线后的安全加固与防滥用清单5.1 搜索接口的暴露口径优先关闭 JSON 格式SearXNG 默认提供了/search?qxxxformatjson这类 JSON 输出这对自用脚本非常方便但也容易成为被滥用的重灾区。很多人部署完不知道这个开关等被人拿去批量采集搜索结果、把服务器当免费 API 用的时候才后悔。我的建议是在settings.yml里只保留html格式也就是前面提到的那段配置。如果你想保留 JSON 给自用脚本也不要把整个 JSON 通道公开更合理的做法是保持settings.yml只开 html同时让脚本模拟浏览器请求去抓取 HTML 页面再自行解析——虽然笨一点但搜索入口并不需要对外暴露结构化接口。网关层也可以拦截明显带 JSON 格式参数的请求if ($arg_format json) { return 403; }这个规则对带 API 需求的人可能会误伤所以要不要加完全取决于你的使用场景。自己权衡就好,但前提是必须清醒意识到 JSON 接口是个多大的攻击面。5.2 限制 User-Agent 与请求特征网关层可以做一层粗粒度 UA 过滤。搜索引擎爬虫、各种扫描工具通常会在 UA 里暴露身份。最简单的方式是在网关配置里屏蔽掉常见的非浏览器请求特征但要注意很多命令行工具和脚本也会用到常见的 UA比如python-requests、curl一眼就能认出来。我的做法是认证网关算第一道关已经拦掉了非 token 持有者在 token 持有者内部不再对 UA 做过分严格的过滤因为能拿到 token 的人基本可信。相反我会用 SearXNG 的 limiter 对已认证请求做频率控制——即使有 token 的人想刷接口也刷不太狠。5.3 防止内部路径被直接访问默认情况下SearXNG 的一些路径如/config、/healthz、/stats等会暴露运行状态。在网关层可以做一个路径白名单或黑名单。考虑到搜索场景合理的暴露路径其实只有/、/search、/login这几个其余一律拒绝。我的 Nginx 配置大概是这样的思路location ~ ^/(config|healthz|stats|settings) { return 404; }当然这个正则也要看你实际部署的 SearXNG 版本不同版本的内部路径略有差异。更稳妥的做法是把server.public_instance保持为false同时在网关层禁止这些路径的外部访问双保险。另外SearXNG 的/settings页面在public_instancefalse时不会对匿名用户展示但我还是建议网关把它挡掉。5.4 日志审计与异常流量告警Token 认证网关的另一个好处是Nginx 的访问日志现在能记录到每一次实际被放行的请求配合X-Real-IP和$http_authorization基本还原了谁在什么时候搜了什么。我在网关配置里加了这样一段日志格式log_format searxng_log $remote_addr - [$time_local] $request status$status token$http_authorization rt$request_time ua$http_user_agent; access_log /var/log/nginx/searxng-access.log searxng_log;注意$http_authorization字段会直接记录完整的 Bearer token。虽然这个 token 本身需要保密但把完整的 token 写进日志等于把钥匙挂在门边上。更合理的做法是在日志里只记录 token 的哈希值或前缀或者干脆不记。我实际用的是只记录前 8 位log_format searxng_log $remote_addr - [$time_local] $request status$status token_prefix${request_uri}_x rt$request_time ua$http_user_agent;这里只是示意真实实现可以用 map 对$http_authorization做截断再输出。总之原则是日志里不要出现可用的完整密钥。6. 排错实录部署中翻车最多的五个场景6.1 初始化报错secret_key 没设置或格式不对SearXNG 启动后页面直接提示please set a secret_key说明环境变量里的SEARXNG_SECRET没有被正确读到。最常见的原因有两个一个是环境变量名拼写错比如把SEARXNG_SECRET写成了SEARXNG_SECRET_KEY另一个是.env文件里的值带了引号导致设置进去的值跟预期不一致。验证方法是直接在看容器内环境变量docker compose exec searxng env | grep SEARXNG_SECRET正常情况应该能看到一长串随机 hex 字符串。如果输出为空或值太短就去检查 compose 文件的environment段和.env文件。注意.env文件里不要用引号包值最好写成SEARXNG_SECRETxxxxx这样的裸格式。6.2 容器启动后退出挂载目录权限问题如果你看到searxng容器不断重启docker compose logs searxng里出现permission denied或类似报错十有八九是挂载目录权限不对。如前文所说SearXNG 容器内部以 UID 977 运行需要宿主机挂载目录对 UID 977 可写。解决办法就一行chown -R 977:977 ./searxng执行后重启容器。这个坑在云服务器和 NAS 环境里特别常见因为前者的用户一般不会去改目录属主后者的 Web 管理界面又经常自动重置文件权限。6.3 auth_request 导致 POST 请求丢失数据网关配置好之后如果你需要往 SearXNG 发 POST 请求比如某些高级搜索参数可能发现请求过了认证却收不到任何参数。原因就是我前面讲的那个坑auth_request的子请求在默认情况下会把请求体一并读走导致后端上游拿不到原始 body。解决方案是确认location /auth里的两行配置出现了proxy_pass_request_body off; proxy_set_header Content-Length ;这两行一加上原始请求体就不会被子请求消耗POST 就能正常工作了。这个坑不只在 SearXNG 场景出现任何用auth_request做认证的 Nginx 反代都会遇到值得记牢。6.4 认证通过后仍然 502/504认证服务返回 204请求也放行了但浏览器出现 502 或者 504。这时候问题基本出在 Nginx 到 SearXNG 这段内网通信上。常见原因有三个。其一Nginx 容器里没有加 DNS 解析规则proxy_pass http://searxng:8080时找不到名为searxng的容器。虽然 Docker 内置 DNS 在多容器同一网络时通常能正常工作但前提是 Nginx 容器确实连接了internal这个网络。可以用docker compose exec gateway getent hosts searxng验证。其二SearXNG 容器没有正常启动或者它和网关不在同一个 Docker 网络里。检查docker compose ps和docker network inspect都能确认。其三SearXNG 内部端口不是 8080如果你的settings.yml里把端口改成了别的值Nginx 后端的地址也要同步改。6.5 上游搜索引擎返回 429分清是自己的限流还是上游的限流这可能是部署完最容易让人迷惑的问题。搜索时偶尔超时日志里出现429 Too Many Requests很多人第一反应是 SearXNG 的 limiter 把自己限制了于是去调server.limiter。但实际上429 可能来自上游搜索引擎而不是 SearXNG 自己。区分方法很简单看 SearXNG 日志里的报错上下文。如果错误信息出现在 SearXNG 调用某个引擎的环节且只有那个引擎失败、其他引擎正常返回结果那基本是上游在限流。处理方法是在settings.yml里给对应引擎降低请求权重、拉长timeout或者干脆临时禁用这个引擎。如果所有引擎同时失败、且访问频率并不高那才需要回头查自己的 limiter 和 Redis 配置。顺带说一句SearXNG 对上游的请求频率控制是保守的默认值已经留了余量除非你主动调大了并发否则个人使用很少触发自家限流。不要一见到 429 就去调 limiter排查顺序永远是从上游到下游。7. 后续还能怎么演进这套反向代理加认证网关的架构稳定跑了挺久之后我陆续做了一些小改动每一个都在原有基础上只增加少量复杂度但体验提升很明显。第一个是把 Token 改成按用途拆分。我给浏览器用户、命令行脚本、手机 App 分别发了不同 token这样哪个 token 被用到异常频率、哪天被人拿去刷接口一眼就能从日志里定位出来吊销也只需要改一个环境变量。第二个是在网关层加了个简单的 IP 黑名单把某些扫描源 IP 直接挡在认证之前减少无意义的 401 日志量。第三个是把 Nginx 的访问日志做了 JSON 格式化塞进日志收集系统这样搜索入口的使用情况可以做成简单的看板。如果你打算长期维护这个服务我的建议是尽早给镜像版本打固定 tag而不是长期追latest。SearXNG 迭代很快某次升级可能会改动默认引擎配置或插件行为不锁定版本的话一次docker compose pull就可能让你的页面样式和搜索结果出现不可预期变化。我给自己的规矩是每次升级前先拉新镜像跑个临时容器确认搜索结果页和 API 行为正常再切流量出了任何问题都能秒回滚。这也是我为什么一直强调自托管服务的核心不在于第一次跑通而在于之后的每次变更都可以预期、可以回退。把这一层想清楚了SearXNG 这套组合就不只是一个玩具而是真正能长期用的私人搜索基础设施。