
搞搜索这事我一直有个很直接的顾虑直接在浏览器里搜搜索记录、点击习惯、停留时长全被对方记在日志里时间一长就是一个完整的用户画像。SearXNG 解决的就是这个问题——它是一个开源的元搜索引擎可以自己部署一份把多个搜索引擎的结果聚合到一个页面里默认不记录查询日志也不给用户打标签。但自己部署服务紧接着就面对另一个问题只要服务暴露到网络上任何人都可能来访问你的搜索接口轻则被人拿来当免费搜索代理重则被刷出大量请求、拖垮机器。这篇文章我会完整走一遍用 Docker 部署 SearXNG 的流程然后在前方加一层自定义 Token 认证网关让只有带着合法 Token 的请求才能查到结果。适合已经会 Docker 基础操作、想私有化搜索入口或者想把搜索能力接到自己应用里的开发者照着做就能跑起来。1. 内容整体设计与思路拆解1.1 SearXNG 到底解决了什么问题SearXNG 的定位很清晰它是一个“元搜索”服务本身不维护搜索引擎的索引库而是把请求分发到多个上游搜索引擎再把返回结果聚合、去重后统一展示。你可以在界面里切换不同的引擎组合也可以只保留你需要的几个上游源比如 Wikipedia、Bing、GitHub、Stack Overflow 这类站点。我把它部署起来的最主要原因有三个第一隐私可控。SearXNG 默认不保存用户搜索历史也不会根据搜索记录生成个性化推荐账号体系第二聚合体验。一次搜索能看到多个来源的结果而不是被某个引擎的算法圈在一个信息流里第三接口化能力。SearXNG 支持 JSON 格式输出意味着它可以作为一个搜索 API 供其他程序调用比如接进大模型应用、自动化脚本、内部知识库工具。这里要特别提一句SearXNG 不是用来做大规模爬虫中转的也不该被当作绕过任何平台规则的通道。它更适合个人、小团队做成内部搜索入口既保留了搜索能力又不把隐私交给第三方统一收集。1.2 为什么部署方式选 Docker如果不用 Docker直接在主机上跑 SearXNG 也可以但你要自己处理 Python 版本、依赖库、系统级软件包、定时任务、进程守护、日志切割。SearXNG 的依赖面并不小每次升级如果依赖有变动很容易出现“本机能跑换台机器就炸”的情况。Docker 把这些全部封装到一个镜像里部署时只有一个 docker-compose 文件和一个配置目录。升级时拉新镜像、重启容器就行迁移时把配置目录拷走新机器上重新 compose up 就恢复。这个可复制性对我来说是最大的价值。网络层面也有好处SearXNG 容器可以只绑定在本地回环地址不直接对网络暴露端口。真正的入口由前面的认证网关接管这样攻击面从“一个搜索服务端口”变成了“一个需要 Token 的网关端口”安全模型干净很多。1.3 认证网关放在哪一层很多人以为 SearXNG 自带密钥配置就够了其实它自带的server.secret_key主要用于会话签名和可控的 API 访问管理并不能做到“每个搜索请求都必须携带有效 Token”。要控制所有外部请求必须在 SearXNG 前面加一层校验。这套架构的关键思路叫“前置认证后端隔离”SearXNG 容器只监听容器内部网络的 8080 端口宿主机上不直接映射 SearXNG 端口或者只映射到 127.0.0.1 方便本机调试对外只暴露认证网关的端口所有请求先经过网关网关校验 Authorization 头里的 Token校验通过以后才把请求转发到 SearXNG校验失败直接返回 401请求根本到不了 SearXNG。这样即使 SearXNG 本身出现未授权访问类漏洞外部请求也得先越过网关这一层纵深防御的意义就在这里。2. 环境准备与方案选型2.1 Docker 环境核查开始之前先确认你的 Docker 环境是好的不然后续所有问题都会被环境带偏。docker --version docker compose version docker info前两个命令能跑通说明客户端和 compose 插件都在。第三个命令docker info会显示 Docker 引擎是否正常运行如果这里报错后面的操作都无从谈起。如果你用的是 Docker Desktop在 Windows 或 macOS 上常见的启动失败原因是虚拟化支持没开。很多人卡在这个报错上Docker Desktop failed to start because virtualisation support wasnt detected这不是 Docker 本身的问题而是底层虚拟机环境的问题。Windows 上要先确认 BIOS/UEFI 里的虚拟化开关VT-x/AMD-V已经打开同时确认 WSL2 已经正确安装macOS 上则要确认当前机器架构和 Docker Desktop 版本匹配。如果这些你都搞不定我建议直接准备一台 Linux 服务器来跑少走很多弯路。2.2 目录与端口规划我习惯把服务目录规划成这样/opt/searxng/ ├── docker-compose.yml ├── searxng-config/ │ └── settings.yml └── authgw/ ├── Dockerfile ├── main.py └── requirements.txt端口规划上SearXNG 容器内部监听 8080宿主机的 8888 只绑定到 127.0.0.1供本机调试和查看状态认证网关监听 8000对外映射到宿主机的 8080。你如果已经有 Nginx 或者云平台的负载均衡也可以把网关的对外端口只暴露在内网由更外层的东西再统一接入灵活调整。2.3 认证网关的两种常见技术路线实现 Token 认证网关我实际用过两种方式各有取舍方案实现方式优点缺点OpenResty Lua在 Nginx 层用 Lua 脚本校验 Token性能好请求不进入额外应用层适合已经熟练使用 Nginx 的人需要维护 Lua 脚本调试相对麻烦Python FastAPI 独立服务单独起一个网关容器校验通过后转发到 SearXNG逻辑直白、改起来方便、容易加限流和审计日志多一层应用转发性能略低于纯 Nginx 方案个人用、小团队用我偏向第二种。理由很简单它代码可读性强出了问题好排查后续想加“按 Token 区分权限”或者“每个 Token 的调用次数统计”也更容易。本文后面就按 FastAPI 方案展开但也会把 OpenResty 的核心思路点一下方便有能力的朋友自行实现。3. 核心实操docker-compose 部署 SearXNG3.1 编写 compose 文件先创建一个工作目录然后新建docker-compose.ymlmkdir -p /opt/searxng cd /opt/searxng vim docker-compose.yml第一版 compose 只需要 SearXNG 服务内容如下services: searxng: image: searxng/searxng:2025.5.14-3c1952f container_name: searxng restart: unless-stopped ports: - 127.0.0.1:8888:8080 volumes: - ./searxng-config:/etc/searxng:rw environment: - SEARXNG_BASE_URLhttp://127.0.0.1:8888/ - SEARXNG_SECRET请替换为随机生成的密钥 cap_drop: - ALL cap_add: - CHOWN - SETGID - SETUID logging: driver: json-file options: max-size: 10m max-file: 3有几个点要解释一下。镜像 tag 我用的是具体日期版本而不是latest因为latest会产生“昨天还能跑今天 pull 下来就升级了”的不可控情况。自己用的服务固定版本最稳确认没问题后再手动升级。SEARXNG_SECRET用来给 SearXNG 的会话和加密功能做签名不要用默认值。生成方法openssl rand -hex 32那段cap_drop和cap_add是容器最小权限原则的实践。SearXNG 在容器里需要写/etc/searxng目录所以保留CHOWN、SETGID、SETUID这几个能力其他高风险能力全部去掉。实际上很多场景下直接:ro挂载目录也行但考虑到配置目录里会有运行时生成的文件我保留了读写权限同时在能力层面做了收紧。3.2 初始化 settings.yml第一次启动镜像时SearXNG 会在挂载目录里自动生成一份默认配置。docker compose up -d docker compose logs -f searxng看到日志提示配置文件已生成以后进入配置目录修改vim /opt/searxng/searxng-config/settings.yml重点看这三块server: secret_key: 替换成 openssl rand -hex 32 的结果 limiter: false image_proxy: true port: 8080 bind_address: 0.0.0.0 search: safe_search: 0 autocomplete: default_lang: zh-CN outgoing: request_timeout: 3.0 useragent_suffix: secret_key必须填否则 SearXNG 会拒绝启动。limiter我建议先关掉等确认网关能正常工作后再决定要不要启用限流它依赖 Redis启用前需要额外起一个 Redis 容器不是开了开关就能用的。如果你希望只通过 API 方式使用 SearXNG可以在search.formats里只保留search: formats: - html - jsonhtml留给人用浏览器访问json留给程序调用。不需要的格式不要开减少暴露面。3.3 启动与验证重新加载配置docker compose restart searxng然后验证服务状态curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1:8888/返回200说明就绪。再试一次带查询参数的 JSON 请求curl -s http://127.0.0.1:8888/search?qdockerformatjson | head -c 500如果返回了 JSON 数据说明 SearXNG 的聚合搜索能力已经正常工作可以进入下一节做认证网关。4. 核心实操Token 认证网关实现4.1 网关的整体工作流程先明确一个原则Token 认证不是“登录后才能访问”这种重流程而是“每个请求都必须带上有效凭证”的轻校验。一次正常请求的链路是这样的客户端 - 认证网关(校验 Authorization: Bearer xxx) - 校验通过 - 转发到 SearXNG - 返回结果网关不保存会话、不维护用户状态每次请求都解析 Token、验签名、查过期时间通过就放行。这种无状态设计的好处是网关可以横向扩展多放几个实例也没有会话同步问题重启网关不会把已签发的 Token 全部失效只要签名密钥不变客户端就不需要重新申请。4.2 Token 签发方式这里我没有用标准 JWT而是用了一个更轻的 HMAC 签名方案。Token 由三段组成过期时间戳.随机串.HMAC签名例如1782000000.8f3a1d9c4b2e4a6f9c0d2e1f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b过期时间负责控制 Token 生命周期随机串保证每次签发的 Token 都不一样HMAC 签名保证 Token 内容不能被篡改。签发脚本我放在网关服务里也可以单独跑下面是核心逻辑import os import time import uuid import hmac import hashlib def make_token(exp_hours: int 24) - str: key os.environ[GATEWAY_KEY].encode() exp int(time.time()) exp_hours * 3600 nonce uuid.uuid4().hex payload f{exp}.{nonce} sig hmac.new(key, payload.encode(), hashlib.sha256).hexdigest() return f{payload}.{sig}使用 HMAC 而不是明文 token好处是即使 Token 被截获别人也不能修改过期时间把短 Token 变成永久有效。4.3 认证网关容器实现网关我选择了 FastAPI代码量小、逻辑清楚、装依赖也方便。先建目录和文件mkdir -p /opt/searxng/authgw cd /opt/searxng/authgwmain.pyimport os import time import hmac import hashlib import httpx from fastapi import FastAPI, Request, Response from fastapi.responses import JSONResponse app FastAPI() GATEWAY_KEY os.environ.get(GATEWAY_KEY, ) SEARXNG_URL os.environ.get(SEARXNG_URL, http://searxng:8080) client httpx.AsyncClient(timeout30.0) def verify_token(token: str) - bool: if not GATEWAY_KEY: return False parts token.split(.) if len(parts) ! 3: return False exp, nonce, sig parts if not exp.isdigit(): return False if int(exp) time.time(): return False payload f{exp}.{nonce} expected hmac.new(GATEWAY_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(sig, expected) app.api_route(/{path:path}, methods[GET, POST, OPTIONS, HEAD]) async def gateway(request: Request, path: str): if request.method OPTIONS: return Response(status_code204) auth request.headers.get(Authorization, ) if not auth.startswith(Bearer ): return JSONResponse(status_code401, content{error: missing token}) token auth[len(Bearer ):].strip() if not verify_token(token): return JSONResponse(status_code401, content{error: invalid or expired token}) target_url f{SEARXNG_URL}/{path} resp await client.request( request.method, target_url, paramsrequest.query_params, contentawait request.body(), headers{ Content-Type: request.headers.get(Content-Type, ), Accept: request.headers.get(Accept, text/html), }, ) return Response( contentresp.content, status_coderesp.status_code, media_typeresp.headers.get(content-type), )requirements.txtfastapi0.115.12 uvicorn[standard]0.34.0 httpx0.28.1DockerfileFROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 8000 CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]然后把网关服务加进docker-compose.ymlservices: searxng: # ... 之前的内容保持不变 authgw: build: ./authgw container_name: searxng-authgw restart: unless-stopped ports: - 8080:8000 environment: - GATEWAY_KEY请替换成一个独立的长随机字符串 - SEARXNG_URLhttp://searxng:8080 depends_on: - searxng注意GATEWAY_KEY和 SearXNG 的SEARXNG_SECRET是两回事前者是网关签发和校验 Token 的签名密钥后者是 SearXNG 自身的会话密钥不要混用。生成方式同样是openssl rand -hex 32启动并验证docker compose up -d --build curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1:8080/search?qdocker没有带 Token预期返回401。签发一个临时 Token 再测试docker exec -it searxng-authgw python -c import os; import main; print(main.make_token(24))拿到 Token 后curl -s -H Authorization: Bearer 你的Token http://127.0.0.1:8080/search?qdockerformatjson | head -c 300能返回 JSON 结果说明网关已正常工作。4.4 Token 的续期、轮换与安全维护看到这里有人会问要不要学大厂那套 refresh token 来做自动续期我的建议是不要。自用或小团队场景下Token 体系应该追求简单可运维。你真正需要的是三件事短期 Token 定期重新签发密钥轮换机制遗失后的吊销手段。具体做法给每个使用者签发一个 24 小时有效的 Token大家各自保管到期后重新跑一次签发脚本不搞自动续期。为什么自动续期意味着你要额外实现 refresh token 的存储和校验一旦 refresh token 配置出错就会出现网上常见的这类报错failed to refresh token: 400 bad request: invalid refresh_token: empty string这类问题本质上是客户端配置不完整导致的而不是服务端坏了。如果你用的第三方客户端支持配置 token 或 API Key直接填固定 Token如果不支持才需要考虑更复杂的 OAuth 流程。自建网关场景真的没必要把自己卷进 OAuth 的坑里。密钥轮换上我的习惯是每月换一次GATEWAY_KEY换的时候在网关环境变量里改新值同时保留旧的密钥做短暂过渡但本文这个极简网关没做双密钥支持所以实际操作就是通知所有使用者旧 Token 即将失效、统一续期、改环境变量、重启网关。人少的时候直接换反而比过渡机制更好维护。安全性上有三条底线GATEWAY_KEY和SEARXNG_SECRET绝不能提交进 Git 仓库Token 只在 HTTPS 链路下传递明文 HTTP 会轻易被截获网关日志里不要记录 Authorization 头的完整内容最多记录末尾几位。4.5 如果你偏好 OpenResty 方案有些朋友已经用 Nginx/OpenResty 统一管理入口不想再多跑一个 Python 容器那可以用 Lua 完成同样的 HMAC 校验。核心逻辑是读取ngx.var.http_authorization切出 Bearer 后面的 Token用ngx.hmac_sha1或ngx.hmac_sha256校验签名和过期时间失败就ngx.exit(401)成功就proxy_pass到 SearXNG。OpenResty 方案在性能上确实更好请求不需要进入 Python 应用层直接由 Nginx Worker 转发。代价是 Lua 脚本调试不如 Python 直观而且你要额外维护 OpenResty 的编译镜像或依赖包。我的经验是如果已有 Nginx 基础设施愿意维护 Lua 脚本选这个方案如果没有基础设施包袱FastAPI 网关足够用。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因排查方向SearXNG 容器反复重启settings.yml语法错误secret_key为空docker compose logs查看启动日志逐行检查 YAML 缩进直接访问 8888 正常但 8080 网关超时网关容器无法解析searxng主机名确认 compose 里两个服务在同一网络docker compose ps看网络状态网关返回 401Token 缺失、签名错误、过期检查 Authorization 头格式检查服务器当前时间是否偏差过大网关返回 502后端 SearXNG 不可达确认 SearXNG 容器还活着确认SEARXNG_URL的端口写的是容器内端口 8080搜索返回结果质量明显下降某个上游引擎超时或返回异常在 SearXNG 页面的“引擎”列表里查看各引擎状态临时禁用异常引擎使用第三方应用接入时报token exchange failed第三方应用没把 Token 放到正确位置检查第三方应用配置的是 Header 还是 Body网关只认Authorization: Bearer上游返回 403 且提示地区不支持目标搜索引擎对出口地区有限制这是目标服务的地区可用性策略不是网关 Token 配置问题需从目标服务支持范围角度处理5.2 避坑经验我实际跑下来最深的几个坑按严重程度排一下。第一不要图省事直接暴露 SearXNG 端口。我在早期版本里把 8888 直接映射到公网结果不到半天就有人在扫描端口并尝试访问搜索接口。后来改成只绑定 127.0.0.1流量全部走网关清净了很多。第二SEARXNG_BASE_URL要按实际对外访问地址设置。如果你最终通过http://search.example.com访问服务那SEARXNG_BASE_URL就写成这个地址否则页面里的有些链接会跳错地方。第三升级 SearXNG 前先备份配置目录。settings.yml是手动改过的镜像升级不会覆盖它但万一配置和版本不兼容你至少能把配置恢复到上一版查问题。备份命令很简单cp -r /opt/searxng/searxng-config /opt/searxng/searxng-config.bak.$(date %F)第四不要忽略上游引擎超时设置。outgoing.request_timeout我设成 3 秒。如果不做限制某个上游引擎返回很慢时整个搜索请求会一直挂着客户端体验非常糟糕。第五Docker 日志要设置轮转。我给 SearXNG 容器加过logging配置限制单文件 10MB、保留 3 份不然长时间运行后/var/lib/docker/containers会被日志塞满。5.3 日志与调试技巧网关和 SearXNG 的日志分开看网关日志docker compose logs -f authgw主要看有没有请求进来、状态码是多少SearXNG 日志docker compose logs -f searxng主要看上游引擎调用是否正常。快速调试一条链路先看网关curl -i -X GET http://127.0.0.1:8080/search?qdocker \ -H Authorization: Bearer 你的Token \ -H Accept: text/html如果网关返回 401先检查 Token 超时时间和签发时的密钥是否一致。有个很容易忽略的问题服务器时钟和签发 Token 的机器时钟不一致会导致刚签出来的 Token 立刻“过期”。你可以先对比时间date %s再对比 Token 第一段里的过期时间戳确认偏差在可接受范围内。如果网关返回 200 但页面内容异常比如结果为空再去 SearXNG 页面手动搜索一次看看是哪个引擎报错。SearXNG 页面上有引擎状态提示比直接看日志直观得多。写在最后这套东西跑了几个月我实际的体会是SearXNG 值得部署但单独裸奔不值得网关这层不能省。用 FastAPI 做认证网关代码不过一百来行换来的是访问可控、日志可查、密钥可轮换这笔投入非常划算。常见的热搜问题像 token exchange failed、refresh token 报错、403 地区限制排查下来大半都不是网关本身的问题而是客户端配置和服务商策略的锅先把请求链路分清楚再逐层定位比一头扎进日志里瞎翻效率高得多。最后再分享一个小技巧把签发 Token 的脚本做成一个独立的小命令放到你自己的常用工具列表里每次使用者要到期了你一条命令就能发新 Token不用去翻容器里的 Python 代码。