Background

After getting my somewhat self-hosted LLM up and running, one of the first things I missed was web search.

After some research and trying out different things, I landed on SearXNG running as a Docker container on the same server. It was easy to get up and running, but I immediately had some qualms.

My server IP was being sent to all these search services, which 1) is a privacy concern for me and 2) since SearXNG is a meta-search engine, I would probably start getting blocked. Using a VPN would get me around the privacy concern at least, and for getting blocked, still likely on a VPN - but at least I could change my IP in a pinch.

Luckily for me, I had done something like this before and I had documented it. I adapted my config from that and spun it up.

Bonus here: I now have my own meta-search engine behind a VPN that I can use for normal search-engine purposes outside of Open WebUI.

References

Agentic Search & URL Fetching / Open WebUI
Web Search / Open WebUI
SearXNG / Open WebUI

Gotchas

Using traditional RAG mode instead of Native mode

Initially I was trying to configure with the legacy options that I didn’t realize were legacy - apparently Open WebUI’s agentic search is the new default. Being somewhat naive and skimming the documentation, I thought “agentic” search was allowing the models to use built-into-the-model web search tools, not the search engine set up in Open WebUI.

I was just plain wrong on this. If I understand it correctly now, this allows the model itself to call certain functions when it deems it appropriate, instead of injecting search results as RAG. Something like that, it’s laid out here. But I was stuck on the SearXNG configuration page and not understanding why it wasn’t working. These two Github issues helped: issue 25585 and issue 25038

Configuring the correct search URL

In the web UI, the correct URL for me was this:

http://gluetun-searxng:8888/search?q=<query>
  • gluetun-searxng is the name of the Docker container exposing the port.
  • 8888 is the internal docker port that is exposed. It needs to be on the same Docker network as the Open WebUI container.

I ran into a couple issues here.

First, I wasn’t able to figure out what hostname to use - host.docker.internal wasn’t working, localhost or 127.0.0.1 wasn’t working, the SearXNG container name wasn’t working.

I was running it with localhost for a minute, and I thought it was working, because curl’ing inside the container returned a result. But I kept getting “incorrect mime-type text/html” errors - when I figured it out, I realized it’s because I was curl’ing the Open WebUI container, and returning the HTML of the web page, not the JSON result that SearXNG should have been returning. Eventually I figured out I needed to use the hostname.

Second was the port. I never gave much thought to the backend port my Docker containers use, because I never really interact with that port - only the one that is exposed to the localhost of the parent machine. I had both SearXNG and Open WebUI running on port 8080 internally, so I was getting a conflict with that port and needed to change SearXNG’s default internal port.

Configuration

Steps

  • Generate Mullvad Wireguard key and download config files
  • Create Docker compose file and .env file with Mullvad information
  • Create Nginx reverse proxy entry
  • Spin everything up
  • Edit Open WebUI settings to enable Web Search and Agentic mode

SearXNG

Compose stack

This uses an adapted compose file from SearXNG’s documentation, as well as my own Gluetun compose file that I used previously. Hopefully when SearXNG updates it doesn’t cause any breakage - I imagine I will need to adapt this when the default Compose template updates upstream.

name: searxng
 
services:
  gluetun:
    image: qmcgaw/gluetun
    container_name: searxng-gluetun
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - 127.0.0.1:${WEBUI_PORT}:${SEARXNG_PORT}
    volumes:
      - ${GLUETUN_CONFIG_DIR}:/gluetun
    networks:
      - npm_network
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - NET_ADMIN
 
  core:
    container_name: searxng-core
    image: docker.io/searxng/searxng:latest
    # image: docker.io/searxng/searxng:${SEARXNG_VERSION:-latest}
    restart: unless-stopped
    # restart: always
    # ports:
    #   - 127.0.0.1:${WEBUI_PORT}:8080
          # - ${SEARXNG_HOST:+${SEARXNG_HOST}:}${SEARXNG_PORT:-8080}:${SEARXNG_PORT:-8080}
    env_file:
      - .env
    volumes:
      - ${SEARXNG_CONFIG_DIR}:/etc/searxng/:Z
      - ${CACHE_DIR}:/var/cache/searxng/
      # - core-data:/var/cache/searxng/
    network_mode: service:gluetun
    depends_on:
      gluetun:
        condition: service_healthy
 
  valkey:
    container_name: searxng-valkey
    image: docker.io/valkey/valkey:9-alpine
    command: valkey-server --save 30 1 --loglevel warning
    restart: unless-stopped
    env_file:
      - .env
    # restart: always
    volumes:
      - ${VALKEY_DIR}:/data/
      # - valkey-data:/data/
    networks:
      - npm_network
 
# volumes:
#   core-data:
#   valkey-data:
 
networks:
  npm_network:
    external: true

.env file

WEBUI_PORT=8888
SEARXNG_PORT=8888
GLUETUN_CONFIG_DIR=./gluetun_config
SEARXNG_CONFIG_DIR=./config
CACHE_DIR=./cache
VALKEY_DIR=./valkey
VPN_SERVICE_PROVIDER=mullvad
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=<private-key>
WIREGUARD_ADDRESSES=<ip-address>
SERVER_COUNTRIES=<country>

Nginx reverse proxy

server {

  server_name searxng.domain.com;

  location / {
    proxy_pass http://127.0.0.1:8888;

  }

  listen 443 ssl;
  include snippets/domain-com.conf;
  include snippets/ssl-params.conf;

  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-Forwarded-Proto $scheme;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_http_version 1.1;
  proxy_read_timeout 900s;

}

Open WebUI

  • Admin Panel Settings Web Search
    • Enable globally
    • Select SearXNG as engine
    • Set SearXNG query URL to http://container-name:8888/search?q=<query>
    • Adjust other settings as desired here, such as result count and concurrency
  • Admin Panel Models - > specific model
    • Enable Web Search under Capabilities, Default Features, and Builtin Tools
    • Under Advanced Params, change Function Calling to Native

Conclusion

I’ve been using this for a few days now with OpenAI’s gpt-oss-120b, and it’s been impressive. I had to cough up $15 to HuggingFace, but I’ve only spent like 20 cents of it, and I was hitting it pretty hard there for a minute working on my next project.

I’m taking a bit of a step back from it for now I think, but I will be back. It’s cool to have a near-frontier model ready for me if a simple search isn’t enough.

The next logical step would be to spin up a dedicated pay-as-you-go instance with a service such as RunPod or Vast.ai, but I’d like to do some more research, model testing, playing around with advanced parameters, and figuring out all the different Open WebUI features before paying up for a model deployment. There’s plenty I can do with Inference Providers for the time being.

EOF