要说最近这一年我复用率最高的自建服务,SearXNG 一定排前三。起初只是想在局域网里搭一个不记录搜索历史的聚合搜索页,后来发现它顺手还能当 JSON 搜索 API 用,于是就把整套服务搬到了公网。结果第二天日志里全是陌生 IP 的搜索请求,有人直接把它当免费搜索接口薅。所以在认真讲 Docker 部署 SearXNG 之前,我先把最重要的话放在开头:如果是公网部署,自定义 Token 认证网关不是可选项,是必选项。
这篇文章我会完整拆解整个落地过程,包括目录规划、容器网络设计、SearXNG 的 settings.yml 配置、Nginx 认证网关实现、令牌轮换和常见问题排查。适合已经知道 Docker 基本操作、想把 SearXNG 私有化部署并对接自动化调用的读者。纯内网使用的人也可以参考,网关那部分哪怕只做局域网保护,也能帮你挡住不少安全事故。
1. 为什么我要给 SearXNG 加一层认证网关
1.1 SearXNG 能做什么,部署前想清楚
SearXNG 是一个元搜索引擎,它本身不维护索引,而是把多个上游搜索引擎的搜索请求汇总到一起,做过去重、聚合和排序后再展示给用户。它通常以两种形态工作:一是浏览器网页界面,适合日常查资料;二是 JSON 格式的搜索接口,适合接进自己的脚本、聊天机器人、自动化流程里。
这个“既能当页面用、又能当 API 用”的双重属性,正是它适合自建的核心原因。你不需要自己写爬虫,不需要维护索引库,只需要维护一个实例,就能获得一个相对统一的搜索入口。隐私方面也比直接访问搜索引擎更好一些,搜索行为和浏览器不直接绑定,很多情况下不会留下账号级的搜索记录。
适合自建 SearXNG 的人无非三类:不想被搜索引擎跟踪的个人用户,需要一个内网统一搜索接口的开发团队,以及做自动化采集或内容分析、需要批量调用搜索结果的开发者。如果你只是偶尔搜一次,那个部署成本和维护成本确实不划算,但如果你每天都要消耗几十上百次搜索请求,自建实例的价值就体现出来了。
1.2 裸奔的 SearXNG 有多危险
我最初部署时图省事,只把容器端口映射到宿主机然后直接暴露公网,没有加任何认证。头一天还好,第二天打开 Nginx 访问日志,发现大量来自陌生 IP 的请求,路径全部是 /search?q=...,频率稳定在每分钟好几次,有些 IP 还会改变 q 参数的值,明显是拿我的实例当免费搜索代理用。
裸奔的后果不是“被用一下”那么简单。上游搜索引擎会对发出搜索请求的来源做风控,你的公网 IP 会因为在短时间内向多个搜索引擎发起大量请求而被限流,甚至被暂时封禁。一旦被封,你自己反而没法正常搜索了。更麻烦的是,第三方怎么使用你的实例你是完全不可控的,他可能用来批量采集、可能用来跑违规查询,这些行为产生的负载和风险最后都会落在你这个部署者的头上。
所以结论很直接:SearXNG 绝不能裸奔公网。无论是加 Basic Auth、加 Token 还是加其他策略,至少要保证“只有知道凭据的人才能访问”。
1.3 认证网关方案怎么选
给 SearXNG 加认证有几个层次的做法。第一是改 SearXNG 自己的源码或插件,比如在应用层拦截请求;第二是给容器内部再加一个代理进程;第三是在容器前面挂一层独立的反向代理网关。第一种侵入性太强,升级版本时容易出问题;第二种等于自己重新造了一个网关,工作量不小;第三种用 Nginx 单独做认证层,不侵入应用,SearXNG 随意升级,这是个合理的平衡点。
具体到网关内部,我又对比了几种方案。最省事的是 Nginx Basic Auth,配置一个密码文件就行,但浏览器弹窗体验一般,自动化脚本还得维护用户名密码,不适合做成 API 网关。更灵活的是用 Nginx 的 auth_request 模块,让 Nginx 在转发前先向一个认证服务发内部子请求,根据子请求返回的 2xx 还是 401 决定是否放行。这个方案既绕开了应用层,又能完全自定义认证逻辑,所以我最终选了它。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Docker 环境与整体架构规划
2.1 基础环境准备
我用的是一台 2 核 4G 内存的 Linux 服务器,系统为 Debian 系。SearXNG 本身占内存不算大,默认配置下大约 300~500MB,再加一个 Nginx 和一个轻量认证服务,2G 内存也能跑,但 4G 会比较从容。
Docker 方面,需要安装 Docker Engine 和 Docker Compose 插件。新版本 Docker 通常已经集成了 compose 子命令,如果还没有启用,可以参考下面的步骤:
bash复制# 确认 docker 与 compose 可用
docker --version
docker compose version
# 如果没有 compose 插件,用 apt 安装 docker-compose-plugin 即可
sudo apt install docker-compose-plugin -y
我建议统一使用 docker compose 命令而不是老的 docker-compose。compose 文件格式上,新版也不再需要写 version 字段,直接把 services 写在最外层即可,避免格式过时导致报错。
2.2 目录结构与端口规划
部署前先规划目录,避免后面配置散落一地。我习惯把整个项目放在一个独立目录里,目录结构大致如下:
text复制~/searxng/
├── docker-compose.yml
├── .env
├── searxng/
│ └── settings.yml
├── auth-gateway/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── auth_service.py
└── nginx/
└── searxng.conf
端口规划方面,我的原则是:对外只暴露一个入口,其他服务只在 Docker 内部网络通信。具体映射如下:
| 服务 | 容器内端口 | 是否暴露到宿主机 | 说明 |
|---|---|---|---|
| Nginx 网关 | 80 | 是,映射为宿主机 8080 | 唯一外部入口 |
| SearXNG | 8080 | 否 | 仅内部网络访问 |
| 认证服务 | 8081 | 否 | 仅内部网络访问,由 Nginx 调用 |
这样设计的好处是外部请求必须经过 Nginx,SearXNG 没有独立的公网入口,即使有人扫描端口也扫不到应用本身。认证逻辑和安全策略集中在网关层,主服务保持干净。
2.3 容器网络的设计思路
同一台机器上三个容器需要一个共享网络。Compose 默认会给每个项目创建一个 bridge 网络,服务之间可以通过服务名互相解析。我这里显式声明一个网络,方便后续扩展:
yaml复制networks:
searxnet:
driver: bridge
在这个网络里,Nginx 可以用 http://searxng-app:8080 访问 SearXNG 主服务,也可以用 http://auth:8081 访问认证服务。服务名作为主机名直接解析,不需要关心容器 IP 变化,这在容器重启后尤其省心,因为 Docker 内部的 DNS 会自动更新解析记录。
3. 编排 SearXNG 主服务
3.1 主编排文件怎么写
完整 compose 文件大概是这样的,我会在下面逐步解释关键点:
yaml复制# ~/searxng/docker-compose.yml
services:
searxng-app:
image: searxng/searxng:latest
container_name: searxng-app
restart: unless-stopped
volumes:
- ./searxng:/etc/searxng
environment:
- SEARXNG_BASE_URL=http://searxng-app:8080/
networks:
- searxnet
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
auth:
build: ./auth-gateway
container_name: searxng-auth
restart: unless-stopped
environment:
- AUTH_TOKEN=${AUTH_TOKEN}
networks:
- searxnet
cap_drop:
- ALL
nginx:
image: nginx:1.27-alpine
container_name: searxng-nginx
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./nginx/searxng.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- searxng-app
- auth
networks:
- searxnet
networks:
searxnet:
driver: bridge
.env 文件里写令牌变量,避免直接写死在 compose 文件里:
text复制# ~/searxng/.env
AUTH_TOKEN=请替换成你的随机令牌
SearXNG 官方镜像会读取挂载目录里的配置文件。第一次启动时,如果 ./searxng 目录下还没有 settings.yml,镜像会自动生成一份默认配置。你可以先启动一次再改配置,也可以直接丢一份默认配置进去再改,两条路都行。关键是后续所有配置修改都要在宿主机这个目录里改,改完重启容器生效。
3.2 settings.yml 关键配置解读
SearXNG 的配置集中在 /etc/searxng/settings.yml 文件里。对 Docker 部署来说,有四个参数必须看:
第一是 server.secret_key。这个密钥用于会话签名和加密相关功能,不设置或设置成默认值会导致警告。生成方式很简单:
bash复制openssl rand -hex 32
把输出填进 settings.yml:
yaml复制server:
secret_key: "生成的随机值"
bind_address: "0.0.0.0"
port: 8080
第二是 server.bind_address。默认配置里可能是 127.0.0.1,但在容器里这样设置会导致容器外部无法访问。Docker 部署必须改成 0.0.0.0,它表示监听容器内所有网卡接口。
第三是 server.base_url 或环境变量 SEARXNG_BASE_URL。这个值会影响搜索结果里生成的链接地址。我在 compose 里设置了 http://searxng-app:8080/,这是容器网络内部地址;如果你后面搭配了域名和 HTTPS,可以改成你的实际外部地址。
第四是 JSON API 的开关。默认网页搜索没问题,但如果要启用 /search?format=json 这种 API 形式,需要确认配置里有 json 格式:
yaml复制search:
formats:
- html
- json
改完配置后重启 SearXNG 容器,API 就能返回 JSON 结果了。这一步是很多人容易漏掉的,不改的话网页搜索正常,但调用 API 会一直报格式错误。
3.3 启动与自检
一切就绪后启动服务并观察日志:
bash复制cd ~/searxng
docker compose up -d
docker compose logs -f searxng-app
日志里如果没有明显的 ERROR 或 Traceback,说明主服务启动成功。由于我设计成不对外暴露 SearXNG 端口,自检时可以在宿主机上用一次性容器或通过 Nginx 容器内部访问:
bash复制# 在 Nginx 容器里测试主服务是否可达
docker compose exec nginx sh -c "wget -qO- http://searxng-app:8080/ | head -n 20"
如果能正常返回页面内容,说明容器网络是通的。此时可以先临时放行一次请求,把 X-Search-Token 认证加上之前,先确认 SearXNG 自身能响应 JSON API 请求。这个自检步骤很关键,它可以帮你把“主服务问题”和“网关问题”隔离开,后面排错会快很多。
4. 实现自定义 Token 认证网关
4.1 方案选型:auth_request 为什么比 Basic Auth 更合适
Nginx 的 auth_request 机制并不复杂。它在处理请求时会向一个内部地址发起一个子请求,如果子请求返回 2xx,则继续处理原始请求;如果返回 401 或 403,则直接拒绝。这个子请求的地址不会直接暴露给客户端,配置了 internal 标识后只能由 Nginx 内部调用。
它比 Basic Auth 强的点在于,认证逻辑完全解耦。你想校验 Header 里的 Token、想校验多个令牌、想对接自己的用户系统,都可以通过认证服务实现,不需要动 Nginx 配置之外的业务逻辑。Basic Auth 密码文件虽然也行,但用户名密码的方式对 API 场景不友好,Token 语义更清晰,也更容易做轮换和多密钥管理。
在这里我给认证服务定义为:读取请求头 X-Search-Token,和预设的令牌做比对,一致就返回 200,否则返回 401。令牌放在自定义 Header 里而不是 URL 参数里,是因为 URL 参数很容易出现在 Nginx 访问日志、浏览器历史和各类代理缓存里,Header 方式相对干净。
4.2 轻量级认证服务实现
认证服务我用一个极简的 Flask 应用。文件结构很简单:
text复制auth-gateway/
├── Dockerfile
├── requirements.txt
└── auth_service.py
requirements.txt 只需要一行:
text复制flask>=3.0
核心代码 auth_service.py:
python复制import os
import secrets
from flask import Flask, request
app = Flask(__name__)
VALID_TOKENS = [t.strip() for t in os.environ.get("AUTH_TOKEN", "").split(",") if t.strip()]
@app.route("/auth", methods=["GET", "POST"])
def auth():
provided = request.headers.get("X-Search-Token", "")
if not VALID_TOKENS:
return "auth server not configured", 500
for token in VALID_TOKENS:
if secrets.compare_digest(provided, token):
return "OK", 200
return "invalid token", 401
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8081)
两个细节值得说道。第一,secrets.compare_digest 是恒定时间比较,比直接 == 更不容易被时序攻击试探出令牌。这个服务虽然长度很短,但既然要暴露公网,防小人不防君子。第二,允许通过逗号分隔配置多个令牌,这样后续给不同的人或不同场景发不同令牌时,不需要改代码,只需要改环境变量然后重启容器。
Dockerfile 可以写成:
dockerfile复制FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY auth_service.py .
RUN adduser -D -u 10001 authuser
USER authuser
EXPOSE 8081
CMD ["python", "auth_service.py"]
加一个非 root 用户运行是个好习惯,认证服务本身不该有 root 权限,减少被攻击后的影响面。
4.3 Nginx 网关配置
Nginx 的配置文件 nginx/searxng.conf 是核心,完整内容如下:
nginx复制limit_req_zone $binary_remote_addr zone=searxng_limit:10m rate=5r/s;
server {
listen 80;
server_name _;
location = /auth {
internal;
proxy_pass http://auth:8081/auth;
proxy_set_header X-Search-Token $http_x_search_token;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
limit_req zone=searxng_limit burst=10 nodelay;
auth_request /auth;
auth_request_set $auth_status $upstream_status;
proxy_pass http://searxng-app: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-Search-Token "";
}
}
逐行解释重点。location = /auth 是精确匹配的认证端点,internal 表示该地址只能被 Nginx 内部访问,外部 HTTP 请求这个路径会被拒绝。认证子请求里把客户端传入的 X-Search-Token 头原样转发给认证服务。
主请求到了 location / 时,auth_request /auth 会先发内部子请求。注意我在 location = /auth 里用了 $http_x_search_token,它会自动读取原始客户端请求的 X-Search-Token 头,不需要在主 location 里手动塞,这个变量是 Nginx 内置的。
proxy_set_header X-Search-Token "" 这一行很容易被忽略,它的作用是清空传给 SearXNG 的认证头,避免 Token 被透传到上游服务。SearXNG 不认识这个头,留着没用,清掉更安全。
外加了 limit_req 限流,基于客户端 IP 限制为每秒 5 次、突发 10 次。这个限流不严格,主要是防止恶意脚本把实例流量打满。如果你要批量调用自己的 API,可以调高到 10r/s 或者按自己的实际阈值配置。
配置完成后,重新加载 Nginx 并测试:
bash复制docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
不带 Token 请求时应该被拒绝:
bash复制curl -i 'http://localhost:8080/search?q=docker&format=json'
# 预期返回 401
带正确 Token 请求时应该正常返回:
bash复制curl -H "X-Search-Token: 你的令牌" \
'http://localhost:8080/search?q=docker&format=json'
# 预期返回 200 和 JSON 数据
到这一步,一个可用的公网搜索网关就算搭起来了。日常网页搜索同样通过这个入口,浏览器客户端需要在请求里带上自定义 Header 才能访问。所以如果你的主要使用场景是浏览器页面,建议配合一个浏览器扩展或者临时用 curl 验证。
4.4 免代码方案:Nginx map 直通令牌校验
如果你不想引入 Python 服务,只想在 Nginx 层做一个最简单的一对一令牌校验,可以用 map 指令完成。它的原理是把请求头里的令牌映射成判断结果,在 server 块里拦截失败的请求。
nginx复制map $http_x_search_token $token_valid {
default 0;
"你的随机令牌" 1;
}
server {
listen 80;
location / {
if ($token_valid = 0) {
return 401;
}
proxy_pass http://searxng-app:8080;
}
}
这个方案代码量为零,部署最快,但缺点也同样明显:令牌变更要改 Nginx 配置,多个令牌要写多行映射,不具备混淆保护,也无法做超时判定之类的复杂逻辑。它最适合临时应急,或者一个团队里只有少数几个固定调用方。如果是长期服务,我还是推荐用带认证服务的 auth_request 方案,扩展性好太多。
5. 安全加固与日常运维
5.1 令牌管理与轮换
Token 生成最简单的方式是随机长串,长度建议至少 32 字节:
bash复制openssl rand -hex 32
令牌放在 compose 文件同目录的 .env 里,用 ${AUTH_TOKEN} 引用,避免把秘密写进代码仓库或 Dockerfile。轮换令牌时,直接修改 .env 文件里的值,然后重启认证服务即可:
bash复制docker compose up -d auth
认证服务支持逗号分隔多令牌,这给了我很顺滑的轮换思路:先在 AUTH_TOKEN 里追加新令牌,重启认证服务;等所有客户端都切换到新令牌后,再删掉旧令牌,重启一次。整个过程不需要中断服务,客户端也不会出现空白期。
还有一条经验:绝对不要让 Token 直接出现在访问日志里。Nginx 默认不会记录请求头,但在调试阶段,很多人会自定义 access log 格式把 headers 打进去,这就把令牌暴露了。如果确实要审计调用方,建议记录令牌的哈希而不是明文。
5.2 最小化容器权限
容器权限上,我建议在 compose 里加 cap_drop: - ALL,再按需添加必要能力。SearXNG 镜像需要写配置缓存,所以加 CHOWN、SETGID、SETUID 三个能力够用;认证服务直接全部能力丢弃。这样即使某个容器被攻破,攻击者能利用的内核能力也很有限。
认证服务的 Dockerfile 里已经加了非 root 用户,Nginx 官方镜像默认以 root 启动 master 进程,worker 进程则是 nginx 用户,这个还好。SearXNG 镜像内部也做了降权。整体来说,这三个容器没有特殊的高危权限需求,最小化之后可以明显降低风险面。
5.3 限流与日志监控
Nginx 的 limit_req 是比较粗糙的限流,但很有效,能把扫描器的流量挡掉大半。我上面配置的是 5r/s,实际部署时建议根据团队规模调一下。如果主要给脚本调用,可以提高到 20r/s;如果只给自己用,5r/s 已经足够。
日志监控方面,Nginx 的 access log 能看出访问来源和路径。我习惯每天瞄一眼:
bash复制docker compose logs --tail=200 nginx | grep 401
如果大量 401,说明有人在穷举令牌或者扫接口。此时可以结合 fail2ban 之类工具做进一步封禁,也可以直接在网关层加 IP 黑名单。对普通个人部署来说,401 观察足够,毕竟令牌是长随机串,穷举成功的概率低到可以忽略。
5.4 升级与备份
SearXNG 和 Nginx 都是官方镜像,升级流程很常规:
bash复制docker compose pull
docker compose up -d
升级前最好先把 searxng/settings.yml 备份一份。SearXNG 升级偶尔会调整默认配置项,如果新版本认为某参数不合法,容器会起不来。此时直接看 docker compose logs searxng-app,按报错提示改配置即可。认证服务文件也要备份,好在它很小,打包存一个目录就行。
我自己的习惯是每周做一次配置目录的 tar 打包,保留最近三份:
bash复制cd ~/searxng && tar czf backup_$(date +%F).tar.gz searxng auth-gateway nginx .env
这个粒度对个人和小团队足够,没必要上正式备份系统。
6. 常见问题与排查实录
6.1 SearXNG 容器一直重启
最多的问题是 SeaxrNG 容器启动后反复退出。常见原因集中在 settings.yml 上:YAML 缩进错误、secret_key 没设置、改挂载目录后权限不对。排查顺序建议如下:
bash复制docker compose logs --tail=50 searxng-app
如果是配置文件报错,日志会直接指出是哪一行。如果是权限问题,通常提示 cannot open /etc/searxng/settings.yml。这时检查宿主机目录权限,确保容器内用户能读写:
bash复制chown -R 1000:1000 ~/searxng/searxng
不同镜像版本使用的 UID 不同,可以用 docker run --rm -v ~/searxng/searxng:/tmp searxng/searxng ls -ln /tmp 反过来看容器内用户权限,匹配 UID 即可。
6.2 认证一直返回 401
Nginx 和认证服务都启动正常,但带 Token 请求依然 401,这个我踩过几次。首先看认证服务日志,确认请求有没有到达它那里:
bash复制docker compose logs auth
如果没有新的日志输出,问题大概率出在 Nginx 没有把 Token 头传给认证服务。检查 Nginx 配置里 proxy_set_header X-Search-Token $http_x_search_token 是否写对了,是不是放在了错误的 location 里。还有一个容易忽略的点:如果用了 HTTPS 且外层还有一层反代,请求头的下划线可能被过滤。Nginx 默认会忽略请求头里带下划线的字段,X-Search-Token 其实是安全的,但如果你改成了 X_Search_Token,就会中招。可以用 underscores_in_headers on; 打开,或者干脆坚持连字符命名。
6.3 搜索接口频繁报限流错
SearXNG 本身不产生搜索内容,它依赖上游搜索引擎。如果你频繁调用,上游会判断为异常请求,返回 429 或直接弹出验证码。SearXNG 的日志里常出现类似“429 Too Many Requests”的记录。
解决办法有几个方向。一是降低自己的调用频率,对批量任务加退避重试;二是在 settings.yml 里调整 outgoing 参数,比如池大小、超时时间:
yaml复制outgoing:
pool_size: 20
request_timeout: 5.0
三是为不同搜索任务配置不同的引擎组合,减少对单一引擎的依赖。引擎配置在 settings.yml 的 engines 模块里,可以按需启用或停用。SearXNG 本身就是聚合,多个引擎分担压力是最有效的做法。
6.4 内存占用持续走高
SearXNG 容器默认内存占用可能在 300~500MB,长时间运行后会缓慢升高。如果机器内存紧张,可以在 compose 里给容器加资源限制:
yaml复制deploy:
resources:
limits:
memory: 768M
不过在无 Swarm 模式下的 compose 里,deploy 资源配置不直接生效,更稳妥的方式是用 mem_limit 字段:
yaml复制services:
searxng-app:
mem_limit: 768m
设置后再观察日志,如果频繁触发 OOM,就需要调高限制或减少同时启动的上游引擎数量。
6.5 快速问题速查表
| 现象 | 可能原因 | 快速处理 |
|---|---|---|
| 容器反复重启 | settings.yml 语法或权限问题 | 看日志,逐行排查配置,检查目录 UID |
| 网页能打开但 API 返回格式错误 | search.formats 未包含 json | settings.yml 中启用 json 后重启 |
| 带 Token 仍 401 | Header 名不一致或未转发 | 确认 X-Search-Token 且 Nginx 已转发 |
| 请求被上游 429 | 调用频率太高 | 降低频率、调 outgoing、切换引擎 |
| 没有认证也放行了 | Nginx 配置未重载 | nginx -s reload 后重新验证 |
| 令牌忘了存在哪 | 环境变量管理混乱 | 统一写入 .env,避免散落各处 |
写在最后
我在这套方案上踩过不少坑,最后沉淀下来一条最重要的经验:网关要和主服务解耦。一开始我也试过直接在 SearXNG 容器里塞认证逻辑,升级镜像时改动的文件全被覆盖,花了一晚上重新整理。后来改成独立的 Nginx 加认证服务后,SearXNG 升级只是 docker compose pull && docker compose up -d 两条命令的事,认证层完全不受影响。
另外一个小建议是,先把 SearXNG 部署到内网,用一个月熟悉它的配置行为和 API 返回格式,再对外开放。内网阶段可以省掉认证,但公网阶段一定要补上。等到你真正依赖这个搜索服务的时候,就会发现加这一层认证网关花掉的两小时,和被人薅一天带来的清理成本相比,实在算不了什么。
这套架构后续还有很多可扩展空间:认证服务可以对接新的 Token 体系、可以做调用量按日统计、可以在网关层接入更细粒度的权限控制。希望这篇实战经验能帮你少走一些弯路,把精力留给真正想解决的问题。
