Files
Frank fb36a1b0fd docs(web-research): the search egress re-read from the live boxes
Two pages described Dankclaw's search stack from memory, and one of them invented a port.

`searxng-tuning.md` claimed "`:8081` (UI) / `:8082` (JSON API)". Re-probed 2026-10-21: JSON is
served on **8081** (`/search?q=…&format=json` returns results) and **8082 is not SearXNG at
all** — it publishes container port 8085, which is Marreta's nginx, moved off 8080 because
SearXNG owns that port inside the shared namespace. Four containers print the same port list in
`podman ps` because they share gluetun's namespace, which is exactly the detail that gets
mistaken for "the JSON API is on another port". The policy-drift flag now carries today's
reading: `/config` still reports 262 engines and zero disabled, unchanged since 2026-10-19, so
the tuned engine policy is still not live — still an operator decision, still not silently
re-applied.

`vpn-routing.md` gave a mullvad-shaped gluetun example and no fleet shape at all, while the
bundle's own work log says the manual-WireGuard law lives "on concept pages". It lives here now,
read from the running container: `VPN_SERVICE_PROVIDER=custom` + `VPN_TYPE=wireguard` + the
single-peer `WIREGUARD_*` names (`protonvpn` and the plural `WIREGUARD_PEERS_*` family are both
wrong for this image), `FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on` — whose name
is the warning that disabling it leaks egress while every check keeps passing — and
`tun0` present. The mullvad block stays as the generic illustration, now labelled as one.

Refs #64
2026-10-21 02:19:23 -07:00

7.1 KiB

type, title, description, tags, verified, timestamp, resource
type title description tags verified timestamp resource
concept VPN Routing for SearXNG Send only SearXNG outbound requests through a VPN while leaving all other local traffic alone.
vpn
searxng
networking
proxy
unverified
2026-10-21 2026-08-19 https://docs.searxng.org/admin/settings/settings_outgoing.html

VPN Routing for SearXNG

SearXNG talks to many upstream engines. Some of them rate-limit or block the source IP. You can send only that outbound path through a VPN and leave every other process on the host untouched.

This page gives four working patterns. Choose the one that matches how you run SearXNG.

Fleet instance: SearXNG runs on dankclaw (192.168.0.143:8081, HTML and JSON alike) inside gluetun's network namespace — Pattern 2 below, in Quadlet rather than Compose. The live shape is in Fleet instance on dankclaw, read 2026-10-21. The proxy pattern (1) works from any host.

The Goal

graph LR
    A[Local agent] --> B[SearXNG]
    B -->|through VPN| C[Upstream engines]
    D[Other local traffic] --> E[Plain internet]

The VPN carries only the SearXNG outbound path. Nothing else changes.

Pattern 1: SearXNG Proxy Setting

SearXNG supports HTTP and SOCKS proxies natively. Point the outbound settings at a local proxy that exits through the VPN.

outgoing:
  proxies:
    all://:
      - socks5://vpn-proxy:1080
  extra_proxy_timeout: 10.0

Apply the proxy to one engine only:

engines:
  - name: google
    engine: google
    proxies:
      https:
        - socks5://vpn-proxy:1080

To force the VPN source IP on a multi-homed host, add source_ips and pin the engine to IPv4:

outgoing:
  source_ips:
    - 10.8.0.2

engines:
  - name: my-search
    engine: duckduckgo
    network: ipv4

Gluetun is a VPN container (WireGuard or OpenVPN). Attach SearXNG to its network namespace. Only SearXNG shares the tunnel. Other containers stay on the default network.

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    cap_add: [NET_ADMIN]
    devices: [/dev/net/tun:/dev/net/tun]
    ports:
      - "8081:8080"
    environment:
      VPN_SERVICE_PROVIDER: mullvad
      VPN_TYPE: wireguard
      WIREGUARD_PRIVATE_KEY: <key>
    restart: always

  searxng:
    image: searxng/searxng:latest
    network_mode: service:gluetun
    depends_on:
      - gluetun
    volumes:
      - ./core-config:/etc/searxng
    restart: unless-stopped

Put the port mapping on the gluetun service. The searxng container does not get its own ports.

Pattern 3: Dedicated Internal Network

Create a network that only SearXNG shares with a lighter VPN client.

services:
  searxng:
    image: searxng/searxng:latest
    networks:
      - searxng-vpn
    volumes:
      - ./core-config:/etc/searxng

  vpn-client:
    image: dperson/openvpn-client:latest
    networks:
      - searxng-vpn
    cap_add:
      - NET_ADMIN

networks:
  searxng-vpn:
    internal: true

An OpenVPN client container is simpler than Gluetun when your provider uses OpenVPN.

Pattern 4: Run SearXNG Inside the VPN Container

services:
  searxng:
    image: searxng/searxng:latest
    network_mode: "container:vpn-client"
    depends_on:
      - vpn-client
    volumes:
      - ./core-config:/etc/searxng

  vpn-client:
    image: dperson/openvpn-client:latest
    cap_add: [NET_ADMIN]
    environment:
      VPN: server.address;username;password
    restart: always

This pattern places the whole SearXNG process inside the tunnel namespace. The host sees the tunnel as the only route for SearXNG.

Compare the Patterns

Pattern Complexity Isolation Per-engine control
Proxy setting Low Medium Yes
Gluetun Medium High No
Dedicated network Medium High No
Inside VPN container Medium Highest No

Fleet instance on dankclaw

Read from the running container 2026-10-21 (podman inspect gluetun, podman ps):

  • VPN_SERVICE_PROVIDER=custom with VPN_TYPE=wireguard. The provider is Proton VPN, configured as manual WireGuard: the single-peer WIREGUARD_* names (WIREGUARD_PRIVATE_KEY, WIREGUARD_PEER_PUBLIC_KEY, WIREGUARD_ENDPOINT_IP) are what gluetun reads. VPN_SERVICE_PROVIDER=protonvpn is wrong here, and so is the plural WIREGUARD_PEERS_* family — that shape belongs to a different gluetun major.
  • One namespace, four names. searxng, searxng-valkey, marreta and marreta_browser join gluetun's network namespace, so podman ps prints the same published-port list against all of them: 8081→8080 is SearXNG, 8082→8085 is Marreta's nginx, which moved off 8080 because SearXNG owns it inside that namespace.
  • FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on. The name is the warning: turning it off sends the container's egress out the plain internet while every "VPN is configured" check keeps passing — results then quietly come from the host IP.
  • Health: tunnel presence is podman exec gluetun sh -c 'test -e /sys/class/net/tun0' (true on this box). gluetun ships no curl, so an egress-IP check needs a companion container on its network — see Verify the Route.

Decisions That Matter

  • A SOCKS proxy keeps the per-engine control. Use Pattern 1.
  • Gluetun gives the cleanest full-tunnel setup. Use Pattern 2.
  • Copying an upstream example? Read the provider and env shape from Fleet instance on dankclaw first: the mullvad-shaped block in Pattern 2 is an illustration, not this fleet's config.
  • An OpenVPN-only provider works with Pattern 3 or 4.

Verify the Route

  1. Run the instance with the VPN active.
  2. Query an IP-echo page through SearXNG.
  3. Confirm that the returned IP differs from your host IP.
  4. Confirm that unrelated host traffic keeps the host IP.
# Gluetun ships no curl; use its own endpoints or a throwaway container.
podman exec gluetun sh -c 'test -e /sys/class/net/tun0 && echo tunnel-up'
podman exec gluetun sh -c 'wget -qO- 127.0.0.1:9999/health'   # "OK" when up
# NOTE: :9999 is Gluetun's HTTP health server (200 "OK" / 500); :8000 is the
# CONTROL server (/v1/* API), which 404s on everything else. Do not health-
# probe :8000. Egress-IP check needs a real binary — run a companion
# container on gluetun's network: docker run --rm --net container:gluetun
# curlimages/curl -s ifconfig.me

Citations

See Also