How to Self-Host SearXNG with Docker
SearXNG aggregates results from several search services without passing each user’s browser cookies or home address to those services, which makes it a popular thing to run on a small VPS. One thing to settle up front: keep your instance private and authenticated. Our Acceptable Use Policy does not allow public Tor, open proxy, or anonymous proxy services, and in practice an open search endpoint attracts abuse traffic fast. The setup below puts Caddy in front, so every search requires a username and password.
The stack follows the official core plus Valkey architecture and pins the official SearXNG image tag 2026.7.13-9e25585ae. Caddy adds TLS and bcrypt-backed Basic authentication. Only Caddy publishes host ports; SearXNG port 8080 and Valkey port 6379 stay inside a fixed Docker subnet.
The limiter needs accurate client addresses and Valkey state, but it only shapes request behavior after authentication; deciding who may use the service stays Caddy’s job.
Prepare the VPS and DNS
You need a Debian or Ubuntu VPS with root or sudo access, an A record for search.example.com, and an AAAA record only when IPv6 works end to end. TCP 80 and 443 must reach the VPS for Caddy. Keep the administrative SSH port open, and leave 8080 and 6379 closed in UFW, security groups, and the provider firewall; only Caddy needs to be reachable from outside.
Install Docker Engine and the Compose plugin with Docker’s convenience script (inspect it first if that worries you; on a tightly managed host, use your distribution’s packages instead), then confirm the domain resolves:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo bash get-docker.sh
sudo docker compose version
getent ahosts search.example.com
The sudo docker compose version check above is the only privileged docker call in this guide; the later docker commands run unprivileged, so add your user to the docker group (sudo usermod -aG docker $USER, then log out and back in) or run them with sudo yourself.
If UFW is active, retain its working SSH rule and add only the public web ports:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
The old searxng/searxng-docker repository is archived, so skip it for a new deployment. As of July 18, 2026, the maintained container template lives in the SearXNG core repository and defines core plus valkey.
Generate the configuration and secret
Create a private project directory, an application secret, and a Caddy password hash. SearXNG’s entrypoint generates a random secret when it creates a missing settings.yml; because this setup supplies its own settings file, it sets SEARXNG_SECRET explicitly. The Caddy password lives only in a transient shell variable, and .env stores the bcrypt hash rather than the plaintext.
sudo install -d -m 750 -o "$USER" -g "$USER" /opt/searxng
cd /opt/searxng
umask 077
install -d -m 700 core-config
read -rp 'Private SearXNG username: ' SEARCH_USER
case "$SEARCH_USER" in
''|*[!A-Za-z0-9._-]*)
echo 'Use only letters, digits, dot, underscore, or hyphen' >&2
exit 1
;;
esac
read -rsp 'Private SearXNG password: ' SEARCH_PASSWORD
printf '\n'
SEARCH_PASSWORD_HASH=$(docker run --rm \
caddy:2-alpine \
caddy hash-password --algorithm bcrypt --plaintext "$SEARCH_PASSWORD")
unset SEARCH_PASSWORD
{
printf 'SEARXNG_SECRET=%s\n' "$(openssl rand -hex 32)"
printf 'SEARCH_USER=%s\n' "$SEARCH_USER"
printf "SEARCH_PASSWORD_HASH='%s'\n" "$SEARCH_PASSWORD_HASH"
printf '%s\n' \
'SEARXNG_BASE_URL=https://search.example.com/' \
'SEARXNG_BIND_ADDRESS=0.0.0.0' \
'SEARXNG_PORT=8080' \
'SEARXNG_LIMITER=true' \
'SEARXNG_PUBLIC_INSTANCE=false' \
'SEARXNG_IMAGE_PROXY=true' \
'SEARXNG_VALKEY_URL=valkey://valkey:6379/0'
} > .env
chmod 600 .env
unset SEARCH_USER SEARCH_PASSWORD_HASH
Keep the secret and bcrypt hash unchanged across ordinary restarts and restores, and keep .env out of tickets, shell transcripts, and repositories. SearXNG’s server settings reference documents the server overrides. SEARXNG_PUBLIC_INSTANCE=false matches this authenticated deployment; access control comes from Caddy, not that flag.
Create /opt/searxng/core-config/settings.yml:
use_default_settings: true
Create /opt/searxng/core-config/limiter.toml:
[botdetection]
ipv4_prefix = 32
ipv6_prefix = 48
trusted_proxies = [
"172.30.0.2/32"
]
[botdetection.ip_limit]
filter_link_local = false
link_token = true
[botdetection.ip_lists]
block_ip = []
pass_ip = []
pass_searxng_org = true
The limiter documentation explains how SearXNG walks forwarded addresses past trusted proxies. The /32 trusts only Caddy’s fixed address inside the private 172.30.0.0/24 proxy subnet. Trusting every RFC1918 range would allow another container or an accidentally exposed proxy to supply a forged client address. Add a pass_ip only for a monitored source you control; passed addresses also skip other bot checks.
Create the core, Valkey, and Caddy services
Create /opt/searxng/Caddyfile:
search.example.com {
encode zstd gzip
basic_auth {
{$SEARCH_USER} {$SEARCH_PASSWORD_HASH}
}
reverse_proxy core:8080
}
Caddy is not part of the official SearXNG Compose template; this integration relies on its documented automatic HTTPS and reverse proxy behavior. Caddy sets the forwarded host, scheme, and client chain and ignores spoofed incoming forwarded values by default.
Create /opt/searxng/compose.yml:
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
environment:
SEARCH_USER: ${SEARCH_USER}
SEARCH_PASSWORD_HASH: ${SEARCH_PASSWORD_HASH}
networks:
proxy:
ipv4_address: 172.30.0.2
core:
image: searxng/searxng:2026.7.13-9e25585ae
env_file:
- ./.env
volumes:
- ./core-config:/etc/searxng
- core-data:/var/cache/searxng
networks:
proxy:
ipv4_address: 172.30.0.3
depends_on:
valkey:
condition: service_healthy
restart: unless-stopped
valkey:
image: valkey/valkey:9-alpine
command: valkey-server --save 30 1 --loglevel warning
volumes:
- valkey-data:/data
networks:
proxy:
ipv4_address: 172.30.0.4
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 6
restart: unless-stopped
networks:
proxy:
ipam:
config:
- subnet: 172.30.0.0/24
volumes:
core-data:
valkey-data:
caddy-data:
caddy-config:
The SearXNG service has no ports entry. Valkey has neither ports nor a password because only containers on this project network can reach it; publishing 6379 would break that boundary. The core service loads the whole mode-600 .env file through env_file, which is where it gets SEARXNG_SECRET. Caddy deliberately does not: its environment block passes only the username and bcrypt hash, which Compose interpolates from the same .env in the project directory, so the application secret never enters the Caddy container. Basic credentials are safe in transit only over TLS, so do not publish an HTTP-only route.
Start the deployment and inspect all three services:
cd /opt/searxng
docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --tail=100 core valkey caddy
Test Valkey, TLS, search, and the limiter
Read the credentials, then check the state service and the authenticated HTTPS path:
cd /opt/searxng
SEARCH_USER=$(sed -n 's/^SEARCH_USER=//p' .env)
read -rsp 'Private SearXNG password: ' SEARCH_PASSWORD
printf '\n'
docker compose exec valkey valkey-cli ping
curl -fsSI -u "$SEARCH_USER:$SEARCH_PASSWORD" https://search.example.com/
Valkey must return PONG and the authenticated request must succeed. Next, prove that an unauthenticated request is refused and that Docker has not opened the private ports on the host:
curl -sS -o /dev/null -w '%{http_code}\n' https://search.example.com/
sudo ss -lntp | grep -E ':(8080|6379)\b'
The first command must print 401; the second must print nothing.
The default output format is HTML, so test with a real search rather than a JSON endpoint. Send browser-like headers and require at least one rendered result:
curl -fsS \
-u "$SEARCH_USER:$SEARCH_PASSWORD" \
-A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/126 Safari/537.36' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-US,en;q=0.8' \
-H 'Accept-Encoding: gzip, deflate, br' \
--compressed \
--data-urlencode 'q=SearXNG metasearch' \
https://search.example.com/search \
-o /tmp/searxng-search.html
grep -q '<article class="result ' /tmp/searxng-search.html
rm /tmp/searxng-search.html
Repeat the search in a real browser and open one result. If the page fills with engine errors, the search did not really succeed; the error messages and logs will tell you why.
Now send an intentionally bot-like burst. The limiter checks more than request counts, including request headers and user-agent behavior, so exact rejection timing depends on the current limiter.toml:
for request in $(seq 1 20); do
curl -sS -o /dev/null -w '%{http_code}\n' \
-u "$SEARCH_USER:$SEARCH_PASSWORD" \
-A 'curl/limiter-test' \
'https://search.example.com/search?q=limiter-test'
done | sort | uniq -c
docker compose logs --since=10m core valkey caddy | \
grep -Ei 'limiter|bot|429|403|valkey|error' || true
unset SEARCH_PASSWORD
The burst should produce blocked responses, commonly 403 or 429, while the browser-like search stays usable. If every request appears to come from 172.30.0.2, stop and fix trusted_proxies; raising limits would only hide the proxy error. Check the logs for Valkey connection failures. The limiter is not access control: it can throttle an accepted request, but only Caddy’s authentication decides whether a request reaches SearXNG.
Understand the privacy boundary
SearXNG’s own-instance documentation explains that it does not forward a user’s cookies to search engines and presents generated request profiles. The selected upstream engines still receive the search terms because they need the query to return results. They normally see the VPS exit IP instead of the user’s IP.
A private instance used by one person can still be linkable through its stable exit address, timing, and query content. Multiple authenticated users send more queries from one data-center IP and may trigger upstream rate limits, CAPTCHA challenges, or an IP block; SearXNG cannot promise that every engine stays available. And to repeat the house rule from the top: please keep Tor, relay, and open-proxy functionality off this deployment, as our AUP does not allow them.
Back up configuration and both data volumes
SearXNG has no application-level backup tool. This procedure follows its documented persistent paths and Docker’s volume backup guidance. It stops the whole project so the core cache and the Valkey snapshot belong to one recovery point:
cd /opt/searxng
umask 077
install -d -m 700 backups
core_volume=$(docker inspect -f \
'{{range .Mounts}}{{if eq .Destination "/var/cache/searxng"}}{{.Name}}{{end}}{{end}}' \
"$(docker compose ps -q core)")
valkey_volume=$(docker inspect -f \
'{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}' \
"$(docker compose ps -q valkey)")
stamp=$(date +%F-%H%M%S)
docker compose stop
sudo tar -czf "backups/searxng-config-$stamp.tar.gz" \
compose.yml Caddyfile .env core-config
docker run --rm -v "$core_volume:/source:ro" -v "$PWD/backups:/backup" \
alpine:3.22 \
tar -C /source -czf "/backup/searxng-core-$stamp.tar.gz" .
docker run --rm -v "$valkey_volume:/source:ro" -v "$PWD/backups:/backup" \
alpine:3.22 \
tar -C /source -czf "/backup/searxng-valkey-$stamp.tar.gz" .
docker compose start
sudo chown "$USER":"$USER" backups/*"$stamp"*
chmod 600 backups/*"$stamp"*
sha256sum backups/*"$stamp".tar.gz > "backups/searxng-$stamp.sha256"
The configuration archive contains SEARXNG_SECRET, so encrypt all three archives and copy them off the VPS. Back up Caddy’s caddy-data volume too if you want to preserve its certificate state, and steer clear of docker compose down -v around these volumes; the -v flag deletes them.
Rehearse an isolated same-version restore
Use a disposable VPS when possible. The same-host drill below uses timestamped names for its directory, network, containers, and volumes, never joins the production Caddy, and starts the same image versions that produced the backup. Keep the commands and the later cleanup in one shell.
cd /opt/searxng
config_archive=$(ls -1 /opt/searxng/backups/searxng-config-*.tar.gz | sort | tail -n 1)
backup_stamp=$(basename "$config_archive" .tar.gz)
backup_stamp=${backup_stamp#searxng-config-}
sha256sum -c "backups/searxng-$backup_stamp.sha256"
restore_id="$(date +%Y%m%d%H%M%S)"
restore_dir="/opt/searxng-restore-$restore_id"
sudo install -d -m 700 -o "$USER" -g "$USER" "$restore_dir"
cd "$restore_dir"
tar -xzf "$config_archive"
docker network create "searxng-restore-$restore_id"
docker volume create "searxng-restore-core-$restore_id"
docker volume create "searxng-restore-valkey-$restore_id"
docker run --rm -v "searxng-restore-core-$restore_id:/restore" \
-v /opt/searxng/backups:/backup:ro \
alpine:3.22 \
tar -C /restore -xzf "/backup/searxng-core-$backup_stamp.tar.gz"
docker run --rm -v "searxng-restore-valkey-$restore_id:/restore" \
-v /opt/searxng/backups:/backup:ro \
alpine:3.22 \
tar -C /restore -xzf "/backup/searxng-valkey-$backup_stamp.tar.gz"
docker run -d --name "searxng-restore-valkey-$restore_id" \
--network "searxng-restore-$restore_id" --network-alias valkey \
-v "searxng-restore-valkey-$restore_id:/data" \
valkey/valkey:9-alpine \
valkey-server --save 30 1 --loglevel warning
docker run -d --name "searxng-restore-core-$restore_id" \
--network "searxng-restore-$restore_id" \
--env-file .env -e SEARXNG_BASE_URL=http://127.0.0.1:18080/ \
-p 127.0.0.1:18080:8080 \
-v "$PWD/core-config:/etc/searxng" \
-v "searxng-restore-core-$restore_id:/var/cache/searxng" \
searxng/searxng:2026.7.13-9e25585ae
docker logs --tail=50 "searxng-restore-core-$restore_id"
Tunnel port 18080, open the restored instance, and run a real search. Verify customized engines and cached data. The drill omits Caddy on purpose, so it proves data and basic function recovery only; it cannot validate Basic authentication, the trusted-proxy chain, or per-client limiter behavior. Remove the test resources when finished:
docker rm -f "searxng-restore-core-$restore_id" "searxng-restore-valkey-$restore_id"
docker volume rm "searxng-restore-core-$restore_id" "searxng-restore-valkey-$restore_id"
docker network rm "searxng-restore-$restore_id"
sudo rm -r "$restore_dir"
Update images and templates separately
For an image update, back up and restore-test first, review the upstream commit, and bump the SearXNG tag. Change Valkey or Caddy only when you mean to update them. Then redeploy:
cd /opt/searxng
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose logs --tail=100 core valkey caddy
Repeat the Valkey, TLS, browser-like search, and limiter checks. Keep the previous image set and backup together; a safe rollback restores both rather than assuming newer data is backward compatible.
Template maintenance is a separate review. SearXNG’s official instructions may change service names, environment defaults, mounts, or Valkey options without changing your pinned image. Compare the current container/docker-compose.yml, .env.example, settings.template.yml, and searx/limiter.toml in the core repository with your local files. Avoid overwriting .env, settings.yml, or limiter.toml blindly, because that can replace the secret or discard your local proxy controls.
Choosing a Riven Cloud VPS for SearXNG
For a personal or small-team instance, our Premium plan (2 vCPU, 4 GB RAM, 40 GB NVMe, 1 TB monthly transfer) runs this whole stack comfortably. All plans come with a 1 Gbps port and full root access; our VPS are unmanaged, so you run the stack yourself and keep full control over it. If you enable many engines, serve several authenticated users, or proxy a lot of images, Ultra (2 vCPU, 8 GB RAM, 80 GB NVMe, 2 TB) gives the cache and transfer more headroom.
We offer Tokyo and Singapore. Pick the location that reaches both you and your upstream engines well. If you are on a mainland China network, test the Tokyo Looking Glass and Singapore Looking Glass from your own carrier at the hours you actually search; the two cities can behave quite differently. Current specs and prices are on the Riven Cloud VPS plans page.
Every plan includes daily provider-side backups, and they make a good safety net. They can catch Valkey and the other volumes at different write points, though, so keep the stopped archives, the protected secret, and the encrypted off-server copies above as your primary recovery path.
Security limits and non-fit cases
SearXNG has no built-in login of its own, which is why this deployment leans on Caddy Basic authentication. Test the logged-out denial as well as authenticated assets and form posts, keep the limiter enabled, patch all three images, watch logs and transfer, and keep Valkey off the public Internet. And please leave the Basic auth in place: an unauthenticated instance is open to the whole Internet, draws abuse traffic quickly, and runs against our AUP. The limiter slows bots down, but it is not a substitute for authentication.
Self-hosting is not the right fit for every case, and that is worth being honest about. If you need contractual availability, abuse handling, stable engine coverage, or a guaranteed clean exit reputation, a managed search product will serve you better. The same goes when upstream CAPTCHAs and IP blocks are unacceptable, when users need strong anonymity against traffic correlation, or when nobody has time to keep the limiter rules and restore drills current. SearXNG improves what upstream engines learn about you; it does not make searches invisible to them.
Share