Operating systems
Self-hosting SearXNG on OpenBSD over a Tor onion service
This guide shows how to self-host SearXNG on OpenBSD and expose it through a Tor onion service while keeping privileges separated and permissions minimal.
TL;DR
- Create a dedicated
_searxnguser and tune system limits before installing dependencies. - Install and run SearXNG inside a Python virtual environment,
store a separate
settings.ymlin/home/searxng, and optionally force all outbound traffic through Tor. - Bind Gunicorn to localhost and publish the service via a Tor v3 onion service to avoid exposing SearXNG directly on clearnet.
Context
This guide builds on an older tumfatig.net guide, with updates and OpenBSD-specific adjustments. The official SearXNG installation documentation targets Linux. The procedure below was tested on OpenBSD 7.9.
Preliminary steps
Set up a dedicated user
$ nano /etc/login.conf.d/searxng
searxng:\
:openfiles=4096:\
:tc=daemon:
$ useradd -g =uid -c "SearXNG metasearch engine" \
-L searxng -s /sbin/nologin -d /home/searxng \
-m -r 2000..2500 _searxng
Raise system limits
$ nano /etc/sysctl.conf
kern.seminfo.semmni=1024
kern.seminfo.semmns=4096
kern.seminfo.semmnu=1024
kern.seminfo.semmsl=1024
kern.seminfo.semopm=1024
$ egrep -v "^#" /etc/sysctl.conf | xargs sysctl
Installation
Install minimum system dependencies
$ pkg_add git python libxslt
Install SearXNG sources and prepare the environment
$ doas -u _searxng /bin/ksh -c 'cd /home/searxng && exec /bin/ksh -l'
$ echo "umask 077" >> ~/.profile
$ git clone https://github.com/searxng/searxng ~/src
$ python3 -m venv ~/pyenv
$ echo ". ~/pyenv/bin/activate" >> ~/.profile
$ ^D
Install required Python modules
If you want, use torsocks with each git
clone/git pull and pip command to
fetch packages through Tor.
$ doas -u _searxng /bin/ksh -c 'cd /home/searxng && exec /bin/ksh -l'
$ command -v python && python --version
/home/searxng/pyenv/bin/python
Python 3.13.13
$ pip install -U pip setuptools wheel pyyaml msgspec typing-extensions pybind11
If you want SOCKS support:
$ pip install "httpx[socks]" httpx-socks pysocks
$ cd ~/src
$ pip install --use-pep517 --no-build-isolation -e .
(...)
Successfully built searxng
Installing collected packages: pytz, whitenoise, valkey, sniffio, six, shellingham, python-socks, pygments, mdurl, markupsafe, lxml, itsdangerous, isodate, idna, hyperframe, hpack, h11, click, certifi, blinker, babel, async-timeout, annotated-doc, werkzeug, python-dateutil, markdown-it-py, jinja2, httpcore, h2, anyio, rich, httpx, flask, typer, httpx-socks, flask-babel, searxng
(...)
Successfully installed annotated-doc-0.0.4 anyio-4.13.0 async-timeout-5.0.1 babel-2.18.0 blinker-1.9.0 certifi-2026.5.20 click-8.4.1 flask-3.1.3 flask-babel-4.0.0 h11-0.16.0 h2-4.3.0 hpack-4.1.0 httpcore-1.0.9 httpx-0.28.1 httpx-socks-0.10.0 hyperframe-6.1.0 idna-3.18 isodate-0.7.2 itsdangerous-2.2.0 jinja2-3.1.6 lxml-6.1.1 markdown-it-py-4.2.0 markupsafe-3.0.3 mdurl-0.1.2 pygments-2.20.0 python-dateutil-2.9.0.post0 python-socks-2.8.1 pytz-2026.2 rich-15.0.0 searxng-2026.6.7+9d49a9f34 shellingham-1.5.4 six-1.17.0 sniffio-1.3.1 typer-0.26.7 valkey-6.1.1 werkzeug-3.1.8 whitenoise-6.12.0
Configuration
This setup keeps the configuration file outside the source tree so source updates do not overwrite it.
$ doas -u _searxng /bin/ksh -c 'cd /home/searxng && exec /bin/ksh -l'
$ sed -i -e "s/ultrasecretkey/$(openssl rand -hex 16)/g" ~/src/searx/settings.yml > /home/searxng/settings.yml
$ echo 'export SEARXNG_SETTINGS_PATH=/home/searxng/settings.yml' >> ~/.profile
Tor proxy defaults in shell profile
If you want every command for the _searxng account to
use Tor by default, add the following to ~/.profile. After
that, you no longer need torsocks for the
_searxng user.
export ALL_PROXY='socks5h://127.0.0.1:9050'
export HTTP_PROXY="$ALL_PROXY" HTTPS_PROXY="$ALL_PROXY"
Also configure this in ~/settings.yml, inside the
output section:
output:
request_timeout: 10.0
max_request_timeout: 30.0
pool_connections: 200
retries: 2
extra_proxy_timeout: 30
using_tor_proxy: true
proxies:
all://:
- socks5h://127.0.0.1:9050
$ . ~/.profile
$ nano ~/settings.yml
Check that everything starts correctly (use torsocks if
you did not configure ~/.profile as shown above):
$ python ~/src/searx/webapp.py
2026-06-08 03:40:07,287 ERROR:searx: Error while getting the version: fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
(...)
2026-06-08 03:40:07,338 ERROR:searx: Error while getting the git URL & branch: fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
(...)
* Serving Flask app 'webapp'
* Debug mode: off
^C
Tor onion service configuration
To expose SearXNG through an onion address, add this to
/etc/tor/torrc:
$ nano /etc/tor/torrc:
HiddenServiceDir /var/tor/searx_hidden_service/
HiddenServiceVersion 3
HiddenServicePort 80 127.0.0.1:8888
Then read the generated onion hostname:
cat /var/tor/searx_hidden_service/hostname
output: example.onion
Open the resulting .onion address in Tor Browser.
Daemonize with Gunicorn
Keep the service setup simple.
$ doas -u _searxng /bin/ksh -c 'cd /home/searxng && exec /bin/ksh -l'
$ pip install gunicorn
(...)
Successfully installed gunicorn-26.0.0
$ nano searxng.conf.py
# Server Socket ========================================================
bind = "127.0.0.1:8888"
backlog = 128
# Worker Processes =====================================================
workers = 1
threads = 2
keepalive = 5
worker_class = "gthread"
threads = 4
timeout = 120
graceful_timeout = 30
# Process Naming =======================================================
proc_name = "searxng"
# Config File ==========================================================
wsgi_app = "searx.webapp:application"
# Server Mechanics =====================================================
chdir = "/home/searxng/src"
# Logging ==============================================================
accesslog = '-'
errorlog = '-'
syslog = True
syslog_addr = 'unix:///dev/log#dgram'
syslog_facility = 'daemon'
syslog_prefix = 'searxng'
There is no startup file for Gunicorn by default, so create one:
$ nano /etc/rc.d/searxng
#!/bin/ksh
#
# PROVIDE: searxng
# REQUIRE: NETWORKING
name="searxng"
rcvar="${name}_enable"
: ${searxng_enable:="NO"}
: ${searxng_user:="_searxng"}
: ${searxng_home:="/home/searxng"}
: ${searxng_pyenv:="${searxng_home}/pyenv"}
: ${searxng_bind:="127.0.0.1:8888"}
: ${searxng_pid:="${searxng_home}/searxng.pid"}
: ${searxng_log:="${searxng_home}/searxng.log"}
: ${searxng_settings:="${searxng_home}/settings.yml"}
: ${searxng_conf:="${searxng_home}/searxng.conf.py"}
daemon="${searxng_pyenv}/bin/gunicorn"
daemon_flags="-b ${searxng_bind} -c ${searxng_conf} searx.webapp:application --pid ${searxng_pid}"
daemon_user="${searxng_user}"
. /etc/rc.d/rc.subr
rc_bg=NO
rc_reload=NO
rc_start() {
touch "${searxng_log}"
chown "${searxng_user}" "${searxng_log}" 2>/dev/null || :
rc_exec "env PYTHONPATH='${searxng_home}/src' SEARXNG_SETTINGS_PATH='${searxng_settings}' ${daemon} ${daemon_flags} >> '${searxng_log}' 2>&1 &"
}
rc_check() {
if [ -f "${searxng_pid}" ]; then
kill -0 "$(cat ${searxng_pid})" >/dev/null 2>&1 && return 0
fi
pgrep -u "${searxng_user}" -f "${daemon}" >/dev/null 2>&1
}
rc_stop() {
if [ -f "${searxng_pid}" ]; then
kill -TERM "$(cat ${searxng_pid})" 2>/dev/null || :
# wait up to 30s for process to exit
for i in 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
sleep 2
kill -0 "$(cat ${searxng_pid})" >/dev/null 2>&1 || break
done
# force kill if still alive
kill -KILL "$(cat ${searxng_pid})" 2>/dev/null || :
rm -f "${searxng_pid}" 2>/dev/null || :
else
pkill -u "${searxng_user}" -f "${daemon}" || true
fi
}
rc_cmd "$1"
$ chmod 0555 /etc/rc.d/searxng
$ rcctl enable searxng
$ rcctl start searxng
searxng(ok)
Updating SearXNG
SearXNG follows a rolling release model where the latest commit on the master branch is treated as stable. Because the update sequence is repetitive, this guide uses a helper script:
$ doas -u _searxng nano ~_searxng/update.sh
cd ~/src
git fetch origin "HEAD"
git reset --hard "origin/HEAD"
pip install -U pip
pip install -U setuptools
pip install -U wheel
pip install -U pyyaml
pip install -U msgspec
pip install -U typing-extensions
pip install -U pybind11
pip install -U pysocks
# Do not uncomment this line if this error appears when trying to update httpx-socks:
# searxng 202x.x.x+xxxxxxxxx requires httpx-socks[asyncio]==0.10.0",
# but you have httpx-socks x.xx.x which is incompatible.
#pip install -U httpx-socks
# Use this instead until searxng update its dependence version:
pip install --force-reinstall "httpx-socks[asyncio]==0.10.0"
pip install -U "httpx[socks]"
pip install --use-pep517 --no-build-isolation -U -e .
$ chmod 0750 ~_searxng/update.sh
To upgrade:
$ doas -u _searxng ksh -l ~_searxng/update
$ doas -u _searxng ksh -l ~_searxng/update_img
$ diff -U2 ./src/searx/settings.yml settings.yml
$ rcctl restart searxng
searxng(ok)
searxng(ok)
Conclusion
With this setup, you can avoid configuring httpd,
acme-client, and relayd for a reverse-proxy
HTTPS deployment, because onion services already provide end-to-end
integrity and encryption. That gives you a smaller attack surface and a
simpler long-term maintenance path.
If you choose to expose SearXNG on clearnet instead, you should still configure HTTPS correctly.
El Psy Kongroo!