> ## Documentation Index
> Fetch the complete documentation index at: https://www.towbar.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# SearXNG

> SearXNG combines results from multiple search engines in a self-hosted search interface.

export const UseCaseNavigation = () => {
  useLayoutEffect(() => {
    const storageKey = "towbar-use-case-open-groups";
    const groupSelector = "#sidebar li[data-title] > button[aria-expanded]";
    let restoring = false;
    let frame = 0;
    function storedGroups() {
      try {
        return new Set(JSON.parse(sessionStorage.getItem(storageKey) || "[]"));
      } catch {
        return new Set();
      }
    }
    function save(groups) {
      try {
        sessionStorage.setItem(storageKey, JSON.stringify([...groups]));
      } catch {}
    }
    function groupName(button) {
      return button.parentElement?.dataset.title;
    }
    function rememberOpenGroups() {
      const groups = new Set();
      for (const button of document.querySelectorAll(groupSelector)) {
        if (button.getAttribute("aria-expanded") === "true") {
          const name = groupName(button);
          if (name) groups.add(name);
        }
      }
      save(groups);
    }
    function restoreOpenGroups() {
      frame = 0;
      const groups = storedGroups();
      if (!groups.size) return;
      restoring = true;
      for (const button of document.querySelectorAll(groupSelector)) {
        if (groups.has(groupName(button)) && button.getAttribute("aria-expanded") === "false") {
          button.click();
        }
      }
      restoring = false;
    }
    function scheduleRestore() {
      if (!frame) frame = requestAnimationFrame(restoreOpenGroups);
    }
    function onClick(event) {
      if (restoring || !(event.target instanceof Element)) return;
      const button = event.target.closest(groupSelector);
      if (button) {
        const name = groupName(button);
        if (!name) return;
        const groups = storedGroups();
        if (button.getAttribute("aria-expanded") === "true") groups.delete(name); else groups.add(name);
        save(groups);
        return;
      }
      if (event.target.closest('a[href^="/docs/use-cases/"]')) {
        rememberOpenGroups();
      }
    }
    const observer = new MutationObserver(scheduleRestore);
    document.addEventListener("click", onClick, true);
    observer.observe(document.getElementById("sidebar") || document.body, {
      childList: true,
      subtree: true
    });
    scheduleRestore();
    return () => {
      document.removeEventListener("click", onClick, true);
      observer.disconnect();
      cancelAnimationFrame(frame);
    };
  }, []);
  return null;
};

<UseCaseNavigation />

<img className="towbar-doc-brand-logo" src="https://mintcdn.com/avgeek/Wq-9tmR7jDSzqtpH/assets/use-case-logos/searxng.svg?fit=max&auto=format&n=Wq-9tmR7jDSzqtpH&q=85&s=5934f0aa0159432665984f6b84e16350" alt="SearXNG logo" aria-hidden="true" width="24" height="24" data-path="assets/use-case-logos/searxng.svg" />

**Upstream source:** [Current installation guide](https://docs.searxng.org/admin/installation-docker.html).

SearXNG can load its bundled default settings and override the signing key and URL through environment variables, so a configuration file is not needed for this first instance. The cache volume keeps its local cache. Public bot protection needs Valkey and an explicitly configured limiter; do not treat this minimal setup as a public search service.

## Towbar manifest

```yaml title=".towbar/services/searxng.service.yml" theme={"system"}
id: searxng
name: SearXNG
buildServer: null
deployment:
  type: image
  image: searxng/searxng@sha256:5286edb35782454ab8a102c5eff6b54bff745853191b46aeead95f225aa6dfb6
container:
  port: 8080
  network: application
  volumes:
    - name: searxng
      mountPath: /var/cache/searxng
      initialData: image
environments:
  production:
    server: 192.0.2.10
rollout:
  type: recreate
  maintenanceMode: true
  reason: Persistent application data uses a single writer
secrets:
  runtime:
    - SEARXNG_SECRET
    - SEARXNG_BASE_URL
    - SEARXNG_LIMITER
```

## Configure

1. Prepare the example server, connect the repository, and map `production` to the branch containing these manifests. Replace the example server IP. Commit the manifests, then sync the repository and inspect the resolved configuration.
2. Set the Service's declared runtime values under **Service → Settings → Secrets** using the table below. Replace descriptions and placeholders with actual values; do not commit passwords or keys.
3. Keep the Service's named volumes attached across deployments. Towbar's Datastore backup policy does not back up Service volumes, so include them in your own recovery plan.
4. This manifest has no public domain. Use a trusted connection to the server for the first check. Add a public route only after configuring the product's authentication or an access gateway.
5. Deploy the Service, then perform the checks below before enabling auto-deploy.

## Runtime values

Save these values on the Service after the repository sync. The manifest declares required keys, not their values.

| Key                | Value to save                                                |
| ------------------ | ------------------------------------------------------------ |
| `SEARXNG_SECRET`   | Generate a long random signing key.                          |
| `SEARXNG_BASE_URL` | [https://searxng.example.com/](https://searxng.example.com/) |
| `SEARXNG_LIMITER`  | false                                                        |

## Verify

Run a search from a trusted connection. Before assigning a public domain, configure the upstream Valkey-backed limiter or an access gateway and verify that the base URL matches the route.

For field constraints, see [Service manifest](/docs/services/manifest).
