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
221 lines
7.1 KiB
Markdown
221 lines
7.1 KiB
Markdown
---
|
|
type: concept
|
|
title: VPN Routing for SearXNG
|
|
description: Send only SearXNG outbound requests through a VPN while leaving all other local traffic alone.
|
|
tags:
|
|
- vpn
|
|
- searxng
|
|
- networking
|
|
- proxy
|
|
- unverified
|
|
verified: 2026-10-21
|
|
timestamp: 2026-08-19
|
|
resource: 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](#fleet-instance-on-dankclaw), read 2026-10-21.
|
|
> The proxy pattern (1) works from any host.
|
|
|
|
## The Goal
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```yaml
|
|
outgoing:
|
|
proxies:
|
|
all://:
|
|
- socks5://vpn-proxy:1080
|
|
extra_proxy_timeout: 10.0
|
|
```
|
|
|
|
Apply the proxy to one engine only:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
outgoing:
|
|
source_ips:
|
|
- 10.8.0.2
|
|
|
|
engines:
|
|
- name: my-search
|
|
engine: duckduckgo
|
|
network: ipv4
|
|
```
|
|
|
|
## Pattern 2: Gluetun (Recommended)
|
|
|
|
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.
|
|
|
|
```yaml
|
|
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.
|
|
|
|
```yaml
|
|
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
|
|
|
|
```yaml
|
|
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](#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](#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.
|
|
|
|
```bash
|
|
# 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
|
|
|
|
- SearXNG outgoing settings: <https://docs.searxng.org/admin/settings/settings_outgoing.html>
|
|
- SearXNG engine settings: <https://docs.searxng.org/admin/settings/settings_engines.html>
|
|
- Gluetun: <https://github.com/qdm12/gluetun>
|
|
- Connect a container to Gluetun: <https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md>
|
|
- Docker Compose networking: <https://docs.docker.com/compose/networking/>
|
|
|
|
## See Also
|
|
|
|
- [SearXNG Tuning](searxng-tuning.md) — The instance these patterns protect.
|
|
- [Agentic Search Best Practices](agentic-search-best-practices.md) — Proxy rotation and retries.
|