先把结论放在前面:OpenWebUI 和 SearXNG 是目前自建 AI 服务里非常经典的一对组合。OpenWebUI 负责把本地模型包装成一个能对话、能联网检索的聊天界面,SearXNG 则是一个聚合搜索引擎,通过它,模型才能在回答问题时拿到实时信息。这个组合的部署门槛不高,真正劝退多数人的是接入过程中的一堆“软配置”。最近我自己在一个新环境里重新搭了一套,又踩了一遍当年踩过的坑,索性把它们整理成这篇避坑指南。如果你正准备在 Docker 里部署 OpenWebUI + SearXNG,或者已经接上了但搜索总是不稳定,这篇文章应该能帮你省下至少一个晚上的排查时间。
1. 两个容器互相访问不了:Docker网络配置才是第一道坎
1.1 现象:OpenWebUI日志里的Connection refused
不少人照着网上教程用 docker run 分别启动 OpenWebUI 和 SearXNG,启动都成功,浏览器也能打开两个独立页面,但 OpenWebUI 的联网搜索就是报错。打开 OpenWebUI 容器日志,常见的错误是:
httpx.ConnectError: [Errno 111] Connection refused我当时第一次遇到这个报错时,第一反应是 SearXNG 没启动,于是去浏览器里开了 SearXNG 页面,发现明明能打开。这就形成了第一个认知冲突:服务明明活着,为什么另一个容器说连不上?
1.2 根因:容器网络命名空间隔离
Docker 容器的网络和宿主机不是同一套网络栈。每个容器默认有自己独立的网络命名空间,localhost 在容器内部指的是“这个容器自己”,不是宿主机,也不是其他容器。如果你在 OpenWebUI 容器里配置 SearXNG 地址为http://localhost:8080,那 OpenWebUI 容器会尝试连接自己容器内的 8080 端口,而它自己根本没有监听这个端口,所以立刻 Connection refused。
这类问题的根源不是你配置的端口错了,而是你默认了两个容器可以通过 localhost 互通。这是容器网络隔离开带来的必然结果,几乎所有刚开始用 Docker 部署多容器服务的人都会在这里栽一次。
1.3 完整排查链路:从docker inspect到network列表
遇到连接问题,别急着改配置,先按下面顺序快速排查:
- 确认两个容器都在运行:
docker ps - 查看 OpenWebUI 和 SearXNG 分别挂在哪个网络下:
docker inspect openwebui-container --format '{{json .NetworkSettings.Networks}}',SearXNG 同理。对比 networks 的 key 是否一致。 - 查看当前宿主机上有哪些自定义网络:
docker network ls。看到bridge是 Docker 默认网络,openwebui_default、searxng_default这类是各自 compose 项目创建的网络。 - 在一个容器里测试另一个容器的网络连通性:
docker exec openwebui-container ping searxng-container,如果容器里没有 ping 命令,可以用docker exec openwebui-container wget -qO- http://searxng-container:8080/search?q=test&format=json看有没有响应。
排查到这里,十有八九你会发现两个容器根本不在同一个网络里。它们各自使用了自己的默认 bridge 网络,彼此之间没有通信通道。
1.4 修复方案:显式声明共享网络
我用的是 docker-compose,把 OpenWebUI 和 SearXNG 放在同一个 compose 文件里,并显式声明网络。下面是精简后的示例:
services: searxng: image: searxng/searxng:latest container_name: searxng networks: - ai-net volumes: - ./searxng:/etc/searxng ports: - "8080:8080" openwebui: image: ghcr.io/open-webui/open-webui:main container_name: openwebui networks: - ai-net ports: - "3000:8080" environment: - SEARXNG_QUERY_URL=http://searxng:8080/search?q=<query> depends_on: - searxng volumes: - ./openwebui:/app/backend/data networks: ai-net: driver: bridge关键点就是在 compose 文件底部声明一个名为ai-net的自定义网络,然后两个服务都挂到这个网络下。这样 OpenWebUI 容器访问 SearXNG 时,直接用服务名http://searxng:8080就能解析到 SearXNG 容器的 IP,不再需要关心容器的具体 IP 是否变化。
如果你已经用两个独立的 compose 文件管理服务,就需要把一个网络声明为 external(外部网络),另一个服务加入这个外部网络。例如在 OpenWebUI 的 compose 文件里加:
networks: default: external: name: searxng_default这种做法的好处是网络由 SearXNG 侧管理,OpenWebUI 作为客户端加入,职责更清晰。
1.5 一个容易被忽略的连带问题:宿主机防火墙
容器网络打通之后,还有一层网络关口是宿主机防火墙。尤其是 SearXNG 部署在同一台服务器上、你在浏览器里直接访问http://宿主机IP:8080时,如果发现一直超时,大概率是防火墙没放行 8080 端口。Ubuntu 上用 ufw 的话,记得执行:
sudo ufw allow 8080/tcpCentOS/RHEL 系用 firewall-cmd 的话,对应命令是:
sudo firewall-cmd --permanent --add-port=8080/tcp sudo firewall-cmd --reload这里有个容易混淆的地方:容器端口映射到宿主机端口后,从宿主机访问需要防火墙放行,但从 OpenWebUI 容器内部访问 SearXNG 走的是 Docker 网络,不受宿主机防火墙策略约束。所以可能出现“OpenWebUI 日志正常但浏览器打不开 SearXNG”这种割裂现象,排查时不要只盯着一个方向。
2. SearXNG的JSON输出被关着:OpenWebUI一直提示找不到搜索接口
2.1 现象:Web Search功能显示异常或返回404
容器网络通了之后,OpenWebUI 已经能访问到 SearXNG,但日志里又开始报新的错误。一种情况是直接报 404,另一种情况是 OpenWebUI 界面上搜索开关打开,但每次问答都提示搜索失败,返回内容里看不到任何联网检索的结果。
这个阶段很多人会怀疑 OpenWebUI 的配置模板写错了,于是反复修改 URL 模板,但问题其实根本不在 OpenWebUI 这一侧。
2.2 根因:formats里没开json,SearXNG 默认不提供 API 结构化输出
SearXNG 本质上是一个网页搜索引擎,默认情况下它会把搜到的结果渲染成 HTML 页面,供你在浏览器里直接浏览。但 OpenWebUI 需要的是结构化的 JSON 数据,而不是一个 HTML 页面。SearXNG 的配置文件settings.yml中有一个关键节点:
search: formats: - html如果 formats 里只有 html,没有 json,那么 SearXNG 收到format=json的请求时不会返回 JSON 结果,而是返回 404 或者直接把 HTML 页面丢给你。OpenWebUI 解析不到目标数据,自然就会显示搜索失败。
2.3 用一句话验证问题是否在这个环节
在你桌面的浏览器里手动访问:
http://localhost:8080/search?q=test&format=json如果你看到的是一个正常的 HTML 搜索页面,或者页面上出现 “JSON not supported” 之类的提示,那就说明 formats 里缺了 json;如果你看到一长串带results字段的 JSON 内容,说明这一关你已经过了。这个方法整个排查链路里非常实用,十五秒就能定位问题是不是出在这里。
2.4 修复:settings.yml调整与容器重启
修改你挂载到容器里的 SearXNG 配置目录下的settings.yml,把 formats 改成:
search: formats: - html - json这里建议不要把 html 删掉。因为你会遇到需要直接在浏览器里打开 SearXNG 调试搜索结果的场景,如果只剩 json 格式,浏览器访问会非常难用。两者保留是兼容性最好的选择。
改完配置后重启 SearXNG 容器:
docker restart searxng重启后再执行一次上面的验证 URL,能正常输出 JSON 就说明修改生效了。要特别留意一点:如果容器里的配置文件是通过 volume 挂载的,改的是宿主机上的文件,重启容器后会重新读取,没问题;但如果你之前是直接 docker exec 进容器改的文件,容器一重建改动就没了,重新部署前要注意把改动同步到宿主机挂载目录中。
2.5 版本差异:不同OpenWebUI版本搜索设置入口
OpenWebUI 的搜索配置入口在不同版本里变化比较大。旧版本中,你需要在环境变量里配置SEARXNG_QUERY_URL,很多教程也会让你这么做。但新版 OpenWebUI 改成了在管理面板里通过界面配置:
Admin Panel -> Settings -> Search
界面里需要填的内容包括:搜索提供方(可以选择 SearXNG)、API 地址(比如http://searxng:8080/search?q=<query>)。填写完之后,还要在 “Web Search” 开关处启用联网搜索。
如果你用的是旧版环境变量方式却一直不生效,优先去界面里看看到底配置的是什么。新版 WebUI 的配置优先级通常高于环境变量,两处配置互相冲突时,界面配置会覆盖环境变量,这也是一个容易让人误判的点。
3. 浏览器跨域拦截:CORS配置里最容易被照抄翻车的细节
3.1 现象:接口正常但浏览器拒绝读取
JSON 输出打开后,OpenWebUI 和 SearXNG 之间看似通了,但如果你在本地开发时用的是“OpenWebUI 页面在一个端口,SearXNG 直接暴露在另一个端口”的架构,浏览器里会报出这样一条错误:
Access to XMLHttpRequest at 'http://localhost:8080/search?q=test&format=json' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.注意这个问题的特殊性:服务端接口完全正常,你直接用 curl 或浏览器地址栏访问都拿到了 JSON 数据,但前端页面里的 JS 代码就是读不到数据。这是因为浏览器强制执行同源策略,来自http://localhost:3000的页面去请求http://localhost:8080的接口时,必须确认对方允许跨域访问,否则浏览器会在中间层把响应拦截掉。
3.2 根因:同源策略和SearXNG默认CORS策略
同源策略可以理解为浏览器给你上的一道保险:只有协议、域名、端口完全一致的请求,才能直接共享数据。OpenWebUI 默认跑在 3000 端口,SearXNG 跑在 8080 端口,两者端口不同,天然跨域。
SearXNG 的默认配置里,CORS 相关的设置不一定满足 OpenWebUI 的跨域需求,需要你主动在settings.yml中开启并声明允许的来源。很多部署教程对这一块一笔带过,导致不少人的 SearXNG 一直没返回 CORS 头,浏览器自然拦截。
3.3 修复:server.cors的具体配置
在 SearXNG 的settings.yml中,添加或修改以下内容:
server: cors: allow_origin: - http://localhost:3000 - http://192.168.1.100:3000allow_origin是一个列表,里面可以写多个来源地址。需要注意,这里不能简单写成*通配符。原因是如果你同时开启了allow_credentials: true(允许请求携带凭据、Cookie 等),浏览器会直接拒绝通配符形式的 CORS 头。
修改完成后同样需要重启 SearXNG 容器。重启后可以在浏览器里打开 OpenWebUI 的页面,重新触发一次联网搜索,再按 F12 找到对应请求,查看响应头里是否出现了:
Access-Control-Allow-Origin: http://localhost:3000如果出现了这个头,说明 CORS 已经放行。
3.4 风险点:反射Origin + credentials=true为什么是错的
网上不少教程为了省事,把 CORS 配置简化成“把请求里的 Origin 原样返回”,再配合Access-Control-Allow-Credentials: true使用。这种配置危险在哪儿呢?我在实际排查过一个朋友的项目后,发现他对“反射 Origin + credentials=true”这个组合完全没有风险意识。如果服务器无条件反射任意 Origin,同时允许携带凭据,那么任何恶意网站都可以在你的登录态下向该服务发起跨域请求。恶意网站只需要在自己的页面里发起一个指向你服务的请求,浏览器会因为 CORS 配置而允许这次跨域读取,攻击者就能拿到你的会话数据。
SearXNG 本身的凭据体系可能不那么敏感,但这个错误思路一旦沿用到同一个配置文件里的其他服务上,风险就会被放大。稳妥的做法就是像上面那样,明确列出允许的来源域名,不要用反射,也不要随便开 credentials。如果你确实需要携带凭据,那更要严格控制 allow_origin 列表,只添加你信任的站点。
3.5 我在实际配置中踩过的小坑:改完配置文件但忘记重启容器
这一点看似低级,但真的很容易犯。我之前在一台长期运行的服务器上改完 CORS 配置后,顺手用 OpenWebUI 测了一下,发现还是跨域错误。当时第一反应是“配置没写对”,于是反复检查和调整 allow_origin 的格式,折腾了半个多小时。最后才想起来是容器根本没重启,配置没有被重新加载。
CORS 配置不像有些应用支持热更新,SearXNG 的 settings.yml 是在容器启动时加载的。所以记住一个流程:改配置 -> 重启容器 -> 验证响应头。不要先怀疑配置语法,先确认你有没有重启。
4. BASE_URL写成localhost:镜像环境里的网络指向偏差
4.1 现象:宿主机访问一切正常,OpenWebUI就是连不上
这个问题和第一个容器网络问题非常像,但场景更隐蔽。有些人的 SearXNG 不是跑在容器里,而是直接装在宿主机上;OpenWebUI 仍然跑在容器里。这时候按照本机部署的习惯,你可能会在 OpenWebUI 的搜索配置中填:
http://localhost:8080/search?q=<query>填完之后发现宿主机浏览器能打开 SearXNG,OpenWebUI 却始终搜索失败。这种现象特别有迷惑性,因为你的第一反应通常是“SearXNG 是不是有问题”,但事实是 Searcher 在宿主机上跑得好好的。
4.2 根因:localhost 在容器内的含义
问题的核心还是容器网络命名空间。当 OpenWebUI 运行在 Docker 容器里时,它访问localhost:8080实际上访问的是容器自身的回环地址,而不是宿主机的回环地址。容器自己并没有监听 8080 端口,所以请求必然失败。
这就好比你在一个小区里,每家每户都有自己的门牌号,但你在邻居家喊“我家的门牌号”,邻居听到的是他家自己的门牌号。localhost 在容器环境里就是最典型的“各喊各的门牌号”。
4.3 修复:三种可行写法及适用场景
根据 SearXNG 部署方式的不同,BASE_URL 有三种正确写法:
| 部署方式 | 正确写法 | 说明 |
|---|---|---|
| SearXNG 也跑在 Docker,且与 OpenWebUI 同网络 | http://searxng:8080/search?q=<query> | 使用 Docker 服务名,最稳定,IP 变化不影响 |
| SearXNG 跑在宿主机上(非容器) | http://host.docker.internal:8080/search?q=<query> | Docker Desktop 平台特有,能访问宿主机回环地址 |
| SearXNG 跑在另一台机器上 | http://192.168.1.50:8080/search?q=<query> | 使用对方机器的实际 IP 地址 |
第三种写法最直观,但要注意 IP 地址写死之后,如果 SearXNG 所在机器的 IP 变化了,配置也需要同步更新。第一种写法是生产环境里最推荐的,因为 Docker 内置的 DNS 解析会自动把服务名解析成正确的容器 IP,不怕 IP 漂移。
4.4 附赠:host.docker.internal 在不同平台的差异
我在 macOS 和 Windows 上用 Docker Desktop 时,host.docker.internal是开箱即用的。但如果你在 Linux 服务器上部署,就有个坑:原生 Docker Engine 在 Linux 上默认不支持host.docker.internal。需要在启动容器时额外加参数:
docker run --add-host=host.docker.internal:host-gateway ...或者在 compose 文件里这样写:
extra_hosts: - "host.docker.internal:host-gateway"加上这个参数之后,容器里的host.docker.internal才能正确解析到宿主机。这个问题在 Linux 上特别常见,我见过不少人在 Linux 服务器上配完发现 host.docker.internal 不生效,然后开始怀疑防火墙配置不对,白白浪费了不少时间。
5. 搜得到搜索页却搜不到结果:上游引擎限流、超时与引擎清单
5.1 现象:SearXNG 能打开,搜索返回0条结果
前面几关全都过了,OpenWebUI 也能正常调用 SearXNG 了,但搜索结果还是不如预期。常见现象是:手动访问 SearXNG 首页,页面正常;输入关键词搜索,页面顶部确实有分类栏,但下面一条结果都没有,或者显示超时错误。在 OpenWebUI 里表现为模型回答“无法获取搜索结果”。
5.2 根因:上游引擎的429/403与SearXNG并发策略
SearXNG 自己不抓取网页内容,它只是把请求转发给上游搜索引擎(Google、Bing、DuckDuckGo 等),再把各个来源的结果聚合后返回。如果上游引擎拒绝或限流了 SearXNG 的请求,SearXNG 能做的只是把错误状态透传给你。
上游引擎拒绝的原因主要有几类:
- 服务器 IP 被识别为数据中心流量,搜索引擎出于反爬策略直接返回 403。
- 搜索请求太频繁,触发了 429 限流。
- 搜索请求携带的 User-Agent 被识别为非常规浏览器。
SearXNG 默认会对多个底层引擎发起并发请求,这个并发行为如果没控制好,更容易触发上游限流机制。
5.3 排查链路:先看SearXNG自身的网络日志
遇到搜索结果为空,先在 SearXNG 日志里找线索:
docker logs searxng --tail 200日志里如果大面积出现429 Too Many Requests、403 Forbidden,就说明上游限制了这个出口 IP 的访问;如果出现超时类错误,说明上游地址本身不可达或响应太慢。
你还可以配合 SearXNG 自带的调试入口,在浏览器打开:
http://localhost:8080/search?q=test&format=json返回的 JSON 里如果results数组是空的,但unresponsive_engines里列出了一堆引擎名,基本可以确定问题出在上游引擎这一层。
5.4 修复:精简引擎清单、调整超时与连接池
SearXNG 的配置文件中有一个engines列表,里面默认启用了不少引擎。这些引擎在你所在的网络环境下不一定都可用,建议只保留真正能出结果的几类。
在settings.yml的engines部分,你可以通过把某个引擎的disabled: true来关闭它。举例:
engines: - name: google disabled: false - name: duckduckgo disabled: false - name: bing disabled: true如果你不想一个一个调整引擎,也可以在 SearXNG 的界面右上角选择“搜索引擎分类”,或者直接通过配置把不可达的引擎标记为禁用。
同时,在outgoing节点下合理设置超时时间和连接池参数:
outgoing: request_timeout: 5.0 pool_connections: 100 pool_maxsize: 20request_timeout设置单次请求的最大等待时间;pool_connections和pool_maxsize控制连接池的数量。建议不要一次性把并发调得过大,否则更容易触发对端限流。参数改完后,同样是重启容器生效。
如果你探测到某个引擎总是超时或者被限制,就把它禁用掉,换另一个可用的。这个方法成本低、效果直接,比反复调请求头的体验好太多。实际上,在普通网络环境下,一个可选引擎只要有一个稳定返回结果,就已经足够支撑 OpenWebUI 的日常联网搜索了。
5.5 关于出站代理:合规HTTP代理的配置位置
如果你所在的企业内网或实验室网络有合规的出站 HTTP 代理(用于访问外网),SearXNG 也支持在settings.yml中配置代理出口:
outgoing: proxies: http: http://your-proxy-server:port https: http://your-proxy-server:port请注意,这里的前提是你使用的是你所在网络环境提供或认可的正规代理服务,而不是来路不明的公共代理。把搜索流量交给一个不可信的代理,等于把你的搜索关键词和 IP 关联信息全部暴露给第三方,风险远大于收益。如果没有这类代理,就跳过这个配置,直接把不可达的引擎禁用掉,不要为了追求“引擎全开”而去做冒险的操作。
5.6 经验:把SearXNG当独立服务先跑通
最后分享一个我自己固定的调试经验。接入 OpenWebUI 之前,先把 SearXNG 当独立服务跑起来,并且在浏览器里完成一次完整搜索,确认能正常出结果。这个步骤看着简单,但能帮你把问题边界划分清楚:
- 如果 SearXNG 页面本身能搜索出结果,说明上游引擎、网络、配置都没问题。
- 如果 SearXNG 页面本身都没有结果,先解决搜索源问题,再回来折腾 OpenWebUI 的接入。
这样排错时永远只有一个变量。很多人习惯直接改 OpenWebUI 的配置,来回试了好几种写法都不行,最后才发现 SearXNG 压根搜不到东西,既浪费了时间又把问题搞得一团糟。
我自己的习惯是:SearXNG 单独跑通后,再到 OpenWebUI 里配置搜索提供方,然后用一个冷门但具体的关键词测试,比如“某个软件的官方文档地址”,这样能明确判断搜索结果是不是真的来自实时互联网,而不是模型靠训练数据现编的。联网功能真正跑通是一个很爽的瞬间,因为模型终于从“记忆问答”变成了“检索回答”。
最后再补充一个实际运维中的建议:SearXNG 的配置改动虽然不频繁,但每次改动前建议先备份一份settings.yml。这个文件里已经踩过的坑、调好的引擎清单、CORS 白名单,都是你花时间调出来的成果,容器重建时如果没有备份,这些配置很可能在参数组合上漏一项,又要重新排查一轮。留好备份,部署和迁移的效率会明显不一样。