如果你手头有几台完全访问不到公网的机器,却想把内网搜索体验做得像平时用网页搜索一样顺滑,SearXNG 是个被低估的好东西。它本身是一个聚合搜索服务,把所有上游搜索结果拉到统一页面里展示,用 Docker 部署其实不复杂,真正让人头疼的是“离线”这两个字。
我最近帮一个客户在隔离网段搭过类似的环境:机器在独立局域网,能访问内部 wiki、代码库和知识库,但外网基本没通。SearXNG 的容器不会自带数据,默认它得调用一堆外部搜索来源才能出结果。所以在这种环境里,光把官方镜像拉下来只是让页面能打开,真正能让它“搜出东西”的,是后面接入内网数据源的那一整套配置。这篇文章我把完整流程重新梳理了一遍,包含离线镜像搬运、Compose 编排、内网搜索引擎接入和常见故障排查,基本照着做就能复现。
1. 先分清:离线部署的“离线”指的是哪一层面
SearXNG 常被归类为“元搜索引擎”,它的工作方式更像一个调度中心:收到你的关键词后,并行去问几十个上游搜索引擎或 API,再把第一页结果汇总、去重、渲染。因为结果本身由上游提供,它自己不怎么保留搜索历史和用户行为,这也是很多人偏爱它的原因。
1.1 离线场景里真正缺的东西
离线环境里,SearXNG 的前端业务逻辑是完整的,页面、配置、请求调度都能正常跑。真正缺的是“数据来源”。它自己不抓取网页、不维护索引,离了外部搜索接口就只会返回空结果或者报错。
所以部署之前必须想明白:你要搜索的是哪些数据?这些数据放在哪里?如果答案是一个内部知识库、一个内网文档系统、或者一批内部 API 接口,那 SearXNG 完全可以当做一个统一搜索入口,把这些来源聚合起来。如果只是希望“有一个搜索框,但不知道数据源在哪”,那这个项目大概率跑起来也没意义。
做离线部署时,我一般会先画一张很简单的逻辑图:用户请求先到 SearXNG,SearXNG 根据配置去访问内网里已存在的各类搜索接口,拿到 JSON 结果再统一渲染。SearXNG 不负责造数据,它只负责把多个数据源的结果整合成一个页面。
1.2 三种常见离线级别
离线不是一个绝对状态,它至少分成三种层次,我建议你先对号入座。
| 离线级别 | 典型场景 | SearXNG 的发挥空间 |
|---|---|---|
| 完全断网 | 物理隔离机房,无任何外网权限 | 必须接入内部数据源,否则搜索一定为空 |
| 白名单网络 | 仅允许少数端口访问外部服务 | 可以接入可访问的外部搜索结果源,但不建议依赖 |
| 半离线 | 外部访问不稳定或成本高 | 以内部数据源为主,外部来源作为补充 |
大多数人的需求其实是第一种或第二种。准备阶段先确认你的网络边界,不要等容器跑起来才发现上游全超时。
2. Docker 镜像离线托运:从联网机到隔离机的完整操作
在离线机器上跑 SearXNG,第一个绕不过去的问题就是镜像怎么进去。如果目标机器完全没法访问镜像仓库,就得在一台联网机器上把镜像先拉下来,再通过 U 盘、内网传输或刻盘的方式带过去。
2.1 先锁版本,再拉镜像
SearXNG 官方提供的是镜像,一般配合 Redis 一起用。Redis 承担了缓存和限流存储,没有它 SearXNG 也能启动,但很容易碰到限流和缓存相关的奇怪问题,所以建议一起打包。
在联网机器上执行:
docker pull searxng/searxng:latest docker pull redis:7-alpine我个人的习惯是不直接用 latest,除非你很清楚自己要跟进最新功能。离线环境里升级很麻烦,越稳定越好。你可以先拉 latest,再把镜像 ID 记下来:
docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}'如果目标机器架构和联网机器不一样,比如一个 amd64 一个 arm64,拉镜像时要指定平台:
docker pull --platform linux/amd64 searxng/searxng:latest docker pull --platform linux/amd64 redis:7-alpine架构不匹配是离线部署里很容易踩的坑,后面排查不出来往往就是它。
2.2 save / gzip / load 一条龙
镜像拉下来之后,把它保存成 tar 文件,再压缩带走。这是最直接的方式。
docker save searxng/searxng:latest redis:7-alpine -o searxng-stack.tar gzip -9 searxng-stack.tarsearxng-stack.tar.gz 就是你要传输的文件。文件可能比较大,可以走内网共享、U 盘拷贝,或者用 rsync 分块传输。
到了离线机器上,先解压再导入:
gzip -dk searxng-stack.tar.gz docker load -i searxng-stack.tar导入完成后确认一下:
docker images | grep -E 'searxng|redis'看到对应的镜像和 tag 就说明离线导入成功。tar.gz 文件本身不占容器空间,但如果长期留着会浪费磁盘,确认无误后建议删掉。
2.3 需要本地仓库时的另一种走法
如果离线环境里自己维护了一个 Docker Registry,导入镜像可以做得更规范。把镜像推送到内网仓库,离线机器直接从仓库拉取。
docker tag searxng/searxng:latest registry.internal/searxng/searxng:latest docker tag redis:7-alpine registry.internal/searxng/redis:7-alpine docker push registry.internal/searxng/searxng:latest docker push registry.internal/searxng/redis:7-alpine离线机器只需要把仓库地址配置好,然后正常 docker pull。这种方法适合需要多台机器部署、后续还要升级的场景,比每次拷 tar 文件省事。
3. 离线机器上的 Compose 编排与关键配置
镜像就位之后,下一步就是把容器跑起来。SearXNG 官方推荐用 Docker Compose 编排,这样配置清晰,以后重启、升级都比较方便。
3.1 最小可用配置清单
我用的最小 compose 文件是这样的:
services: searxng: image: searxng/searxng:latest container_name: searxng restart: unless-stopped ports: - "127.0.0.1:8080:8080" networks: - searxng-network environment: - SEARXNG_BASE_URL=http://127.0.0.1:8080/ - SEARXNG_SECRET_KEY=请改成随机长字符串 - SEARXNG_REDIS_URL=redis://redis:6379/0 volumes: - ./searxng-data:/etc/searxng:rw depends_on: - redis redis: image: redis:7-alpine container_name: searxng-redis restart: unless-stopped networks: - searxng-network volumes: - ./redis-data:/data command: ["redis-server", "--appendonly", "yes"] networks: searxng-network: driver: bridge这里有几个关键点:
SEARXNG_SECRET_KEY不能省略,也不能用固定的字符串。它负责签名和会话安全。可以用openssl rand -hex 32生成,然后填进环境变量。SEARXNG_BASE_URL要写成用户实际访问的地址。如果你通过 nginx 反代,就写反代后的地址,不要写容器内部地址。SEARXNG_REDIS_URL是 SearXNG 连接 Redis 的关键,写错了搜索页面可能一直转圈。- 数据目录用 volume 挂载到宿主机,这样 settings.yml 和其他配置不会被容器重建冲掉。
3.2 初始化 settings.yml 的正确姿势
第一次启动后,SearXNG 会在挂载目录里自动生成一套默认配置文件,就是searxng-data/settings.yml。你要修改配置,不要直接改容器里的文件,而是改宿主机挂载的这份。
docker logs searxng -f看到类似“loaded settings”的日志,说明默认配置已经生成。也可以直接检查文件:
ls -l searxng-data/ cat searxng-data/settings.yml如果要用环境变量覆盖 secret,可以在 settings.yml 里写成:
server: secret_key: "__env__:SEARXNG_SECRET_KEY"这样敏感信息保留在 compose 的 environment 里,不会因为共享 settings.yml 而泄露。修改配置之后重启容器:
docker compose restart searxng3.3 对外统一入口:nginx 反代小抄
离线环境虽然不走公网,但内部用户还是希望拿浏览器直接访问,而不是带着端口号访问。用 nginx 在目标机器上做一个反向代理,把http://127.0.0.1:8080暴露到内网域名或固定 IP 上。
server { listen 80; server_name search.internal; client_max_body_size 10m; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }如果 SearXNG 部署在别的机器上,proxy_pass改成对应的内网 IP 即可。同时记得把 compose 里的SEARXNG_BASE_URL改成http://search.internal/,否则页面里的相对链接可能出现跳转问题。
4. 让搜索真正出结果:内网数据源的接入方式
这是整个离线部署里最核心的一步,也是很多人卡住的地方。镜像导好了,容器跑起来了,搜索框也出来了,但怎么搜都是空结果。原因很简单:默认配置里的那些外部搜索引擎,在你的网络环境里根本不可达。
4.1 为什么默认引擎在离线环境必然空转
SearXNG 自带一堆引擎配置,比如各类公开搜索服务。这些默认引擎要求能访问外网,离线机器上自然是超时或连接失败。如果不想让页面错误堆积,可以在 settings.yml 里明确禁用它们,或者干脆只保留内网引擎。
我处理这类环境时,通常把默认引擎全部关闭,只保留自定义引擎。这样可以避免每次搜索都等待超时,页面响应会快很多。
4.2 统一 JSON API 的假设
离线环境下,最稳妥的接入方式就是让 SearXNG 去请求一个内网 HTTP API,这个 API 返回固定结构的 JSON。假设你有一个内部文档检索服务,它收到查询参数q后返回如下格式:
{ "items": [ { "url": "http://wiki.internal/doc/12", "title": "Nginx 配置文档", "content": "本文档介绍如何配置反向代理和负载均衡" } ] }如果你的内部数据源不是这种格式,比如返回的是 Elasticsearch 的复杂嵌套结构,我一般建议在中间加一个很薄的服务转换一下,而不是强行让 SearXNG 去解析各种嵌套路径。用一个小服务做转换,后面维护会轻松很多。
一个最简单的转换服务可以是这样的:
from flask import Flask, request, jsonify import requests app = Flask(__name__) @app.route("/search") def search(): q = request.args.get("q", "") resp = requests.get( "http://internal-es:9200/docs/_search", params={"q": q}, timeout=5 ) hits = resp.json().get("hits", {}).get("hits", []) items = [ { "url": hit["_source"].get("url"), "title": hit["_source"].get("title"), "content": hit["_source"].get("content"), } for hit in hits ] return jsonify({"items": items}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)这个服务可以部署在同一台离线机器上,只要 SearXNG 能访问到它的 5000 端口就行。
4.3 settings.yml 里增加自定义引擎
拿到统一 JSON 结构之后,在 settings.yml 里增加一个自定义引擎:
use_default_settings: true server: secret_key: "__env__:SEARXNG_SECRET_KEY" limiter: false image_proxy: true default_locale: "zh" redis: url: "redis://redis:6379/0" engines: - name: internal_doc engine: json_engine search_url: "http://127.0.0.1:5000/search?q={query}" query_type: query disabled: false categories: general results_container: items result_key: items title_attr: title url_attr: url content_attr: content几个字段要解释一下:
search_url里的{query}会被替换成搜索关键词。SearXNG 会自动做 URL 编码,不要自己再手动转一圈。results_container定义结果列表在 JSON 里的位置,这里是根节点下的items数组。result_key表示每个结果对象里,从哪个字段读取作为独立结果项。title_attr、url_attr、content_attr分别对应标题、链接、摘要的字段名。
配置完成后重启 SearXNG:
docker compose restart searxng然后在搜索框输入关键词测试。如果页面出了结果说明整条链路通了,如果仍然空白,可以用 curl 直接看 API 返回是否符合预期:
curl "http://127.0.0.1:5000/search?q=test"用这种方式,可以一次接入多个内网搜索来源。比如一个来源是 wiki,一个来源是代码仓库,一个来源是工单系统,每个来源各建一个 json_engine,然后通过 categories 分类展示。SearXNG 的聚合能力在这一步才真正体现出来。
5. 上线后必看的排障清单与优化建议
离线部署容易出问题的地方其实不多,但每个都挺隐蔽。下面是我在这些环境里实际踩过的坑。
5.1 最常见故障速查表
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 容器一直重启 | secret key 为空或格式不对 | 用 openssl rand 重新生成并填到环境变量 |
| 页面转圈但不返回 | Redis 地址配错或 Redis 没起来 | 检查 SEARXNG_REDIS_URL 和 redis 容器状态 |
| 搜索页面 429 | 限流被触发 | 内网环境把 limiter 关掉,或调高请求限制 |
| 每次都搜索失败 | 默认引擎全部不可达 | 只保留内网自定义引擎,禁用外部默认引擎 |
| 自定义引擎返回空 | JSON 字段映射不对 | 先用 curl 看返回格式,再回填 attr 字段 |
| 镜像能 load 但启动报错 | 架构不匹配 | 检查镜像平台,重新在联网机器按目标架构拉取 |
| 设置改了没生效 | 容器没有重启 | 修改宿主机 settings.yml 后执行 docker compose restart |
这些坑里最隐蔽的是 JSON 字段映射。SearXNG 不会帮你检查返回结构对不对,配置写错了它也只是静默返回空结果,不会报错。所以排查顺序一定是:先 curl 数据源,再查 settings.yml 映射,最后才动 SearXNG 本身。
5.2 两个直接影响体验的配置
一个是限流。SearXNG 默认对公共实例有比较严格的限流,防止别人滥用。离线内网环境通常没这个必要,反而会误伤同事。在 settings.yml 里设置:
server: public_instance: false关闭后限流逻辑基本就不起作用了,内部使用会更顺手。
另一个是图片代理。如果内网搜索结果里包含图片,建议开启image_proxy: true,这样图片会经由 SearXNG 中转,浏览器拿到的是同一个域名下的资源,不会因为内网证书、外网访问原因导致图片裂开。
5.3 日志与备份的日常动作
离线环境出问题的时候,第一优先看日志:
docker logs searxng -f --tail 100日志里会直接显示引擎连接错误、配置加载错误等信息。Redis 的日志也一样查看。
配置备份这件事,我吃过一次亏才长记性。settings.yml 改完后如果源数据接口出了问题,想回退到之前的状态,如果没有备份就只能凭记忆往回改。建议每次改动前复制一份:
cp searxng-data/settings.yml searxng-data/settings.yml.bak镜像文件如果重新导入过,也可以把关键 tar 文件留一份放在专门目录里,方便之后快速重建环境。
6. 最后分享一点个人习惯
每次到一个离线环境,我第一件事不是写 compose,也不是改 settings.yml,而是先拿 curl 把内网数据源的接口测通。只要能把一条 JSON 数据拿到手,后面的 SearXNG 配置就是纯粹的字段映射体力活。很多搜索为空的问题,最后都发现不是 SearXNG 的锅,而是上游接口返回结构悄悄变了。
还有一个习惯是给每个自定义引擎加上明确的 categories,搜索框旁边就能按“文档”“代码”“工单”分类切换,使用体验会好很多。离线环境里的 SearXNG 没必要追求把所有数据都塞进一个搜索框,把入口做清楚,把每个数据源的边界定义好,比表面上的强大更重要。