Docker 部署 SearXNG 并添加自定义 Token 认证网关实战指南

本文记录了如何在 Linux 服务器上使用 Docker 部署隐私搜索引擎 SearXNG,将其端口修改为 8003,并为了防止公网滥用,添加一个基于 Python Flask 的轻量级 Token 认证网关。同时详细记录了在部署过程中遇到的 “Internal Server Error”、“样式丢失 (404)” 以及 “Too Many Requests” 等问题的排查与解决。

一、环境准备与目录结构

首先创建必要的目录结构:

mkdir -p /opt/searxng/searxng
mkdir -p /opt/searxng/auth-gateway
cd /opt/searxng

二、核心配置

1. SearXNG 配置文件 (纯净版)

编辑 /opt/searxng/searxng/settings.yml。

⚠️ 注意: 配置文件对 YAML 格式缩进极为敏感。为了避免 Vim 粘贴时注释错乱导致的 Internal Server Error (500),这里提供一份无注释的纯净版配置。请直接覆盖使用。

use_default_settings: true

general:
  debug: false
  instance_name: "SearXNG-Private"
  privacypolicy_url: false
  donation_url: false
  contact_url: false
  enable_metrics: true

search:
  safe_search: 0
  autocomplete: "baidu"
  default_lang: "zh-CN"
  ban_time_on_fail: 5
  max_ban_time_on_fail: 120
  formats:
    - html
    - json

server:
  # 请修改这里为你自己的随机密钥
  secret_key: "change-this-to-a-random-secret-key"
  # 关键:必须关闭限制,否则会被网关触发 429 错误
  limiter: false
  image_proxy: true
  port: 8080
  bind_address: "0.0.0.0"
  public_instance: false

ui:
  static_use_hash: true
  default_locale: "zh-Hans-CN"
  query_in_title: true
  infinite_scroll: true
  center_alignment: false
  default_theme: "simple"
  theme_args:
    simple_style: "auto"

redis:
  url: "redis://redis:6379/0"

outgoing:
  request_timeout: 5.0
  max_request_timeout: 15.0
  useragent_suffix: ""
  pool_connections: 100
  pool_maxsize: 20
  enable_http2: true

engines:
  - name: google
    engine: google
    shortcut: g
    disabled: true
  - name: bing
    engine: bing
    shortcut: b
    disabled: false
  - name: baidu
    engine: baidu
    shortcut: bd
    disabled: false

2. 环境变量 (.env)

生成随机 Token 并写入 /opt/searxng/.env:

# 在终端执行
echo "SEARXNG_SECRET=$(openssl rand -hex 32)" > .env
echo "VALID_TOKENS=$(openssl rand -hex 32)" >> .env

# 查看生成的 Token
cat .env

三、构建 Token 认证网关

为了解决 Flask 默认拦截静态资源导致的页面样式丢失问题,我们需要编写一个特殊的 Python 网关。

1. 网关代码 (auth_gateway.py)

文件路径:/opt/searxng/auth-gateway/auth_gateway.py

#!/usr/bin/env python3
"""
SearXNG Token 认证网关 (修复静态资源 404 问题)
"""
import os
import logging
from flask import Flask, request, Response, jsonify
import requests

# ==================== 关键配置 ====================
# static_folder=None 是修复样式丢失的核心,禁用 Flask 默认静态文件处理
app = Flask(__name__, static_folder=None)

# 后端配置 (去除末尾斜杠)
SEARXNG_BACKEND = os.getenv('SEARXNG_BACKEND', 'http://searxng:8080').rstrip('/')
VALID_TOKENS = set(filter(None, os.getenv('VALID_TOKENS', '').split(',')))
WEB_PAGES = {'/', '/preferences', '/stats', '/config', '/search'}

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)


def forward(path):
    """通用转发逻辑"""
    target_path = path.lstrip('/')
    backend_url = f"{SEARXNG_BACKEND}/{target_path}"

    args = request.args.copy()
    args.pop('token', None)  # 移除 Token 参数

    try:
        headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'x-api-token', 'content-length']}
        resp = requests.request(
            method=request.method,
            url=backend_url,
            headers=headers,
            params=args,
            data=request.get_data(),
            cookies=request.cookies,
            allow_redirects=False,
            stream=True,  # 关键:流式传输,保证 CSS/JS/图片完整加载
            timeout=30
        )

        excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
        headers = [(k, v) for k, v in resp.raw.headers.items() if k.lower() not in excluded_headers]

        return Response(resp.iter_content(chunk_size=1024*8), resp.status_code, headers)

    except Exception as e:
        logger.error(f"转发异常: {str(e)}")
        return jsonify({'error': 'Gateway Error'}), 502


def render_login_page():
    html = """
    <!DOCTYPE html>
    <html>
    <head><meta charset="UTF-8"><title>SearXNG Login</title></head>
    <body style="display:flex;justify-content:center;align-items:center;height:100vh;background:#f0f2f5;">
        <form action="/" method="get" style="background:white;padding:2rem;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
            <h3>🔐 请输入访问令牌</h3>
            <input type="password" name="token" required style="width:100%;padding:10px;margin:10px 0;">
            <button type="submit" style="width:100%;padding:10px;background:#007bff;color:white;border:none;cursor:pointer;">进入</button>
        </form>
    </body>
    </html>
    """
    return Response(html, mimetype='text/html'), 401


# 万能路由入口
@app.route('/', defaults={'path': ''}, methods=['GET', 'POST'])
@app.route('/<path:path>', methods=['GET', 'POST'])
def handle_all_requests(path):
    # 1. 静态资源直接放行 (白名单机制)
    if path.startswith('static/') or path in ['favicon.ico', 'opensearch.xml', 'robots.txt']:
        return forward(path)

    # 2. 提取 Token
    token = request.args.get('token') or request.headers.get('X-API-Token') or request.cookies.get('searxng_token')

    # 3. 验证 Token
    if token and token in VALID_TOKENS:
        resp = forward(path)
        # 登录成功种 Cookie (7天有效)
        if resp.status_code == 200 and 'text/html' in resp.headers.get('Content-Type', ''):
            resp.set_cookie('searxng_token', token, max_age=604800, httponly=True)
        return resp

    # 4. 验证失败
    if path == '' or path in WEB_PAGES:
        return render_login_page()

    return jsonify({'error': 'Unauthorized'}), 401


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8003, threaded=True)

2. Dockerfile

文件路径:/opt/searxng/auth-gateway/Dockerfile

FROM python:3.11-slim

WORKDIR /app

RUN pip install --no-cache-dir Flask==3.0.0 requests==2.31.0 gunicorn==21.2.0

COPY auth_gateway.py .

CMD ["gunicorn", "--bind", "0.0.0.0:8003", "--workers", "4", "--threads", "4", "--timeout", "60", "auth_gateway:app"]

四、Docker Compose 编排

编辑 /opt/searxng/docker-compose.yml:

version: "3.9"

services:
  # 认证网关 (对外暴露 8003)
  auth-gateway:
    build: ./auth-gateway
    container_name: searxng-auth-gateway
    restart: unless-stopped
    ports:
      - "8003:8003"
    environment:
      - SEARXNG_BACKEND=http://searxng:8080
      - VALID_TOKENS=${VALID_TOKENS}
    networks:
      - searxng-net
    depends_on:
      - searxng

  # SearXNG (仅内部访问)
  searxng:
    image: searxng/searxng:latest
    container_name: searxng
    restart: unless-stopped
    depends_on:
      redis:
        condition: service_healthy
    expose:
      - "8080"
    volumes:
      - ./searxng:/etc/searxng:rw
    environment:
      - SEARXNG_BASE_URL=http://localhost:8003/
      - SEARXNG_SECRET=${SEARXNG_SECRET}
    networks:
      - searxng-net

  redis:
    image: redis:7-alpine
    container_name: searxng-redis
    command: redis-server --save 30 1 --loglevel warning
    restart: unless-stopped
    volumes:
      - redis-data:/data
    networks:
      - searxng-net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  redis-data:

networks:
  searxng-net:
    driver: bridge

五、启动与排错 (Troubleshooting)

1. 启动命令

cd /opt/searxng
# 必须加上 --build,否则 Python 代码修改不会生效
docker compose up -d --build

2. 常见错误解决方案

🔴 错误一:Internal Server Error (500)
ERROR: yaml.scanner.ScannerError: mapping values are not allowed here...

原因: settings.yml 文件格式错误。通常是因为直接复制了带注释的配置到 Vim 中,导致缩进错乱或混入了非法字符。

解决: 使用本文提供的纯净版配置(无注释),确保 YAML 缩进严格对齐。


🔴 错误二:Web 页面样式丢失 (404)
GET /static/themes/simple/sxng-ltr.min.css 404

原因: Flask 默认会拦截 /static 路由并在容器本地查找文件,导致请求未转发给 SearXNG 后端。

解决:

  1. 在 Python 代码中初始化 Flask 时添加参数:static_folder=None。
  2. 手动在代码中处理 path.startswith('static/') 的转发逻辑。
  3. 执行 docker compose up -d --build 重新构建镜像。

🔴 错误三:Too Many Requests (429)
HTTP/1.1 429 Too Many Requests

原因: SearXNG 默认开启了限流。所有请求经过网关转发后,对于 SearXNG 来说来源 IP 都是网关容器的 IP,瞬间触发单 IP 限流。

解决: 在 settings.yml 中设置 server.limiter: false。


💡 提示: 部署成功后,访问 http://IP:8003 即可看到 Token 登录页面。输入 .env 文件中的 Token 即可使用。

更多推荐