# Web Search Configuration (https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/web_search)

The `webSearch` configuration allows you to customize the web search functionality within LibreChat, including search providers, content scrapers, and result rerankers.

## Overview

The web search feature consists of three main components:

1. **Search Providers**: Services that perform the initial web search
2. **Scrapers**: Services that extract content from web pages
3. **Rerankers**: Services that reorder search results for better relevance

## Example

```yaml filename="webSearch"
webSearch:
  # Search Provider Configuration
  serperApiKey: "${SERPER_API_KEY}"
  searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}"
  searxngApiKey: "${SEARXNG_API_KEY}"
  searchProvider: "serper" # Options: "serper", "searxng", "tavily", "keenable"

  # Optional: query options sent to a SearXNG instance
  searxngSearchOptions:
    engines: "google,bing,startpage" # Default: "google,bing,duckduckgo"
    language: "en" # Default: "all"

  # Tavily Configuration (search and/or scraper)
  tavilyApiKey: "${TAVILY_API_KEY}"
  # Optional: custom Tavily-compatible endpoints
  tavilySearchUrl: "${TAVILY_SEARCH_URL}"
  tavilyExtractUrl: "${TAVILY_EXTRACT_URL}"

  # Keenable Configuration (search and/or scraper; all credentials optional)
  keenableApiKey: "${KEENABLE_API_KEY}"
  keenableApiUrl: "${KEENABLE_API_URL}"
  keenableSearchOptions:
    maxResults: 8
    site: "example.com"
    attributionTitle: "LibreChat"
    timeout: 15000
  keenableScraperOptions:
    attributionTitle: "LibreChat"
    timeout: 15000

  # Scraper Configuration
  firecrawlApiKey: "${FIRECRAWL_API_KEY}"
  firecrawlApiUrl: "${FIRECRAWL_API_URL}"
  firecrawlVersion: "${FIRECRAWL_VERSION}"
  scraperProvider: "firecrawl" # Options: "firecrawl", "serper", "tavily", "keenable"

  # Reranker Configuration
  jinaApiKey: "${JINA_API_KEY}"
  jinaApiUrl: "${JINA_API_URL}"
  cohereApiKey: "${COHERE_API_KEY}"
  rerankerType: "jina" # Options: "jina", "cohere", "none"

  # General Settings
  scraperTimeout: 7500 # Timeout in milliseconds for scraper requests (default: 7500)
  safeSearch: 1 # Options: 0 (OFF), 1 (MODERATE - default), 2 (STRICT)
  # Required for deliberately private self-hosted provider endpoints
  allowedAddresses:
    - "searxng:8080"
```

## SSRF Protection and Private Providers

LibreChat validates each connection it opens for web search, scraping, and reranking. Private, loopback, link-local, and cloud-metadata destinations are blocked by default, including redirect hops and hostnames that resolve to a private address at connect time.

Use `webSearch.allowedAddresses` when a configured SearXNG, Firecrawl, Jina, Tavily-compatible, Keenable-compatible, or related provider endpoint is deliberately private:

```yaml filename="webSearch / allowedAddresses"
webSearch:
  searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}"
  allowedAddresses:
    - "searxng:8080"
    - "10.0.0.5:3002"
    - "[fd00::5]:8080"
```

This field is an SSRF exemption list, not a strict allowlist. Public destinations remain available. Entries must be exact `host:port`, `private.ip:port`, or `[ipv6]:port` pairs; URLs, paths, CIDR ranges, bare hosts/IPs, and public IP literals are rejected. An exempted hostname trusts whichever private IP it resolves to on that port, so prefer a private IP literal and list only names whose DNS you control.

LibreChat automatically exempts a configured HTTP(S) forward-proxy endpoint so it can be reached. When the proxy carries a request, it resolves the destination and must enforce destination egress itself. `NO_PROXY` can make a request direct, where LibreChat's connection guard still applies. The guard covers connections LibreChat opens to provider APIs; it cannot constrain a page URL that a third-party scraper receives as request data and fetches on its own infrastructure.

## Search Providers

### searchProvider

<OptionTable
  options={[
    ['searchProvider', 'String', 'Specifies which search provider to use.', 'Options: "serper", "searxng", "tavily", "keenable"'],
  ]}
/>

### serperApiKey

<OptionTable
  options={[
    ['serperApiKey', 'String', 'Environment variable name for the Serper API key. If not set in .env, users will be prompted to provide it via UI.', '${SERPER_API_KEY}'],
  ]}
/>

**Note:** Get your API key from [Serper.dev](https://serper.dev/api-keys)


### searxngInstanceUrl

<OptionTable
  options={[
    ['searxngInstanceUrl', 'String', 'Environment variable name for the SearXNG instance URL. If not set in .env, users will be prompted to provide it via UI.', '${SEARXNG_INSTANCE_URL}'],
  ]}
/>

### searxngApiKey

<OptionTable
  options={[
    ['searxngApiKey', 'String', 'Environment variable name for the SearXNG API key. If not set in .env, users will be prompted to provide it via UI.', '${SEARXNG_API_KEY}'],
  ]}
/>

**Note:** This is optional and only needed if your SearXNG instance requires authentication.

### searxngSearchOptions

<OptionTable
  options={[
    ['searxngSearchOptions', 'Object', 'Query options sent to your SearXNG instance on every search. Every subkey is optional, and the block is only read when searchProvider is "searxng".', ''],
  ]}
/>

**Subkeys:**

<OptionTable
  options={[
    ['engines', 'String or Array of Strings', 'Engines your instance should query, sent to SearXNG as the engines parameter. Accepts a comma-separated string or a YAML list; both are normalized to the comma-separated form, with surrounding whitespace and empty entries dropped. A value that is empty once trimmed is treated as unset.', 'Default: "google,bing,duckduckgo"'],
    ['language', 'String', 'Result language code, sent to SearXNG as the language parameter. Use a code your instance accepts, such as "en", "de", or "fr".', 'Default: "all"'],
    ['timeRange', 'String', 'Restricts results by publish date, sent to SearXNG as time_range. Left out of the request entirely when unset.', 'Options: "day", "month", "year"'],
    ['timeout', 'Number', 'HTTP request timeout in milliseconds for calls to your instance. Must be a positive integer no greater than 120000; 0 is rejected because it would disable the timeout.', 'Default: 10000'],
  ]}
/>

<Callout type="info" title="Availability">
`searxngSearchOptions` reaches SearXNG through the search tool in `@librechat/agents`, and needs `@librechat/agents` v3.6.9 or later. Releases built against an earlier version ignore the block rather than failing to start, so an instance that keeps returning default results may simply be running an older build.
</Callout>

**Example:**

```yaml filename="webSearch"
webSearch:
  searchProvider: "searxng"
  searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}"
  searxngApiKey: "${SEARXNG_API_KEY}" # Optional
  searxngSearchOptions:
    engines:
      - google
      - bing
      - startpage
      - qwant
    language: "en"
    timeRange: "month"
    timeout: 10000
```

`engines` also accepts the comma-separated form SearXNG itself uses, so this is equivalent to the list above:

```yaml filename="webSearch"
webSearch:
  searxngSearchOptions:
    engines: "google, bing, startpage, qwant"
```

#### Choosing engines

Engine names must match engines that are enabled on your own instance, and two things follow from that:

- SearXNG ignores an engine it does not recognize instead of reporting an error. A typo, or an engine that is disabled on your instance, shows up as fewer results rather than as a failure.
- The valid names are instance-specific. Check the enabled list at `https://your-instance/config`, which returns JSON including every enabled engine, or open the **Engines** tab of your instance preferences page.

The default is `google,bing,duckduckgo`. SearXNG aggregates whatever the selected engines return, so one blocked engine costs you its share of the results rather than the whole response, and a search only comes back empty when every selected engine fails or returns nothing.

DuckDuckGo is the engine in that default set most likely to be blocked, since it serves CAPTCHAs to most self-hosted instances. With only three engines in the set, losing it at the same time as a rate-limited Google or Bing is a common way to end up with no results at all. Setting `engines` explicitly, leaving DuckDuckGo out, and listing more than three engines all reduce that risk.

Engines that answer quickly and tolerate self-hosted traffic make the best starting set, for example:

```yaml filename="webSearch"
webSearch:
  searchProvider: "searxng"
  searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}"
  searxngSearchOptions:
    engines:
      - google
      - bing
      - brave
      - startpage
      - qwant
```

Adding more engines widens coverage but also slows every search down, since SearXNG waits on the slowest engine in the set before responding. Keep `timeout` in mind when you grow the list.

#### What these options do not control

- **Safe search** is not part of this block. It comes from the top-level [`safeSearch`](#safesearch) key and is forwarded to SearXNG as `safesearch`.
- **Result categories** are chosen per query by LibreChat, which maps the search type to `general`, `images`, `videos`, or `news`. They cannot be overridden here.
- **Page number and result format** are fixed. LibreChat always requests page 1 in JSON format, which is why your instance must have `json` enabled under `formats`.

### tavilyApiKey

<OptionTable
  options={[
    ['tavilyApiKey', 'String', 'Environment variable name for the Tavily API key. Used for both search and scraper. If not set in .env, users will be prompted to provide it via UI.', '${TAVILY_API_KEY}'],
  ]}
/>

**Note:** Get your API key from [Tavily](https://app.tavily.com/home)

### tavilySearchUrl

<OptionTable
  options={[
    ['tavilySearchUrl', 'String', 'Environment variable name for a custom Tavily Search API URL. Optional; defaults to Tavily hosted search when unset.', '${TAVILY_SEARCH_URL}'],
  ]}
/>

### tavilyExtractUrl

<OptionTable
  options={[
    ['tavilyExtractUrl', 'String', 'Environment variable name for a custom Tavily Extract API URL. Optional; defaults to Tavily hosted extract when unset.', '${TAVILY_EXTRACT_URL}'],
  ]}
/>

### tavilySearchOptions

<OptionTable
  options={[
    ['tavilySearchOptions', 'Object', 'Configuration options for Tavily search.', ''],
  ]}
/>

**Subkeys:**

<OptionTable
  options={[
    ['searchDepth', 'String', 'Controls relevance vs latency tradeoff. "basic" returns one NLP summary per URL. "advanced" returns multiple semantically relevant snippets per URL (2 API credits). "fast" balances speed and relevance with snippets. "ultra-fast" minimizes latency with one NLP summary.', 'Options: "basic", "advanced", "fast", "ultra-fast". Default: "basic"'],
    ['maxResults', 'Number', 'The maximum number of search results to return.', 'Range: 1-20. Default: 5'],
    ['topic', 'String', 'The category of the search. "news" is useful for real-time updates. "finance" for financial data.', 'Options: "general", "news", "finance". Default: "general"'],
    ['includeImages', 'Boolean', 'Include images in the response. Returns both top-level query images and per-result images.', 'Default: false'],
    ['includeAnswer', 'Boolean or String', 'Include an LLM-generated answer. "basic" or true for a quick answer, "advanced" for a detailed answer.', 'Default: false'],
    ['includeRawContent', 'Boolean or String', 'Include cleaned and parsed HTML content. "markdown" or true for markdown format, "text" for plain text.', 'Default: false'],
    ['includeDomains', 'Array of Strings', 'Restrict search to specific domains. Maximum 300 domains.', ''],
    ['excludeDomains', 'Array of Strings', 'Exclude specific domains from results. Maximum 150 domains.', ''],
    ['timeRange', 'String', 'Time range filter based on publish or last updated date.', 'Options: "day", "week", "month", "year"'],
    ['includeImageDescriptions', 'Boolean', 'When includeImages is true, also add a descriptive text for each image.', 'Default: false'],
    ['includeFavicon', 'Boolean', 'Include the favicon URL for each search result.', 'Default: false'],
    ['chunksPerSource', 'Number', 'Maximum number of relevant content chunks per source. Only available when searchDepth is "advanced".', 'Range: 1-3. Default: 3'],
    ['safeSearch', 'Boolean', 'Optional Tavily safe_search override for Tavily Search requests. Omitted by default; true may require Tavily Enterprise.', 'Default: omitted'],
    ['timeout', 'Number', 'Client-side HTTP request timeout in milliseconds. Controls how long to wait for the Tavily API to respond before giving up.', 'Default: 15000'],
  ]}
/>

### keenableApiKey

<OptionTable
  options={[
    ['keenableApiKey', 'String', 'Environment variable name for the optional Keenable API key. Keenable works without a key; configuring one raises public rate limits.', '${KEENABLE_API_KEY}'],
  ]}
/>

### keenableApiUrl

<OptionTable
  options={[
    ['keenableApiUrl', 'String', 'Environment variable name for a custom Keenable search API URL. This does not change the page-fetch endpoint.', '${KEENABLE_API_URL}'],
  ]}
/>

The page-fetch endpoint is configured separately with the environment-only `KEENABLE_FETCH_URL` variable.

### keenableSearchOptions

<OptionTable
  options={[
    ['keenableSearchOptions', 'Object', 'Configuration options for Keenable search.', ''],
  ]}
/>

<OptionTable
  options={[
    ['maxResults', 'Number', 'Maximum number of search results.', 'Range: 1-20. Default: 8'],
    ['site', 'String', 'Restricts results to one domain.', ''],
    ['attributionTitle', 'String', 'Value sent in the X-Keenable-Title attribution header.', ''],
    ['timeout', 'Number', 'Client-side HTTP request timeout in milliseconds.', 'Range: 0-120000. Default: 15000'],
  ]}
/>

## Scrapers

### firecrawlApiKey

<OptionTable
  options={[
    ['firecrawlApiKey', 'String', 'Environment variable name for the Firecrawl API key. If not set in .env, users will be prompted to provide it via UI.', '${FIRECRAWL_API_KEY}'],
  ]}
/>

**Note:** Get your API key from [Firecrawl.dev](https://docs.firecrawl.dev/introduction#api-key)

### firecrawlApiUrl

<OptionTable
  options={[
    ['firecrawlApiUrl', 'String', 'Environment variable name for the Firecrawl API URL. If not set in .env, users will be prompted to provide it via UI.', '${FIRECRAWL_API_URL}'],
  ]}
/>

**Note:** This is optional and only needed if you're using a custom Firecrawl instance.

### firecrawlVersion

<OptionTable
  options={[
    ['firecrawlVersion', 'String', 'Environment variable name for the Firecrawl API version (v0 or v1).', '${FIRECRAWL_VERSION}'],
  ]}
/>

### scraperProvider

<OptionTable
  options={[
    ['scraperProvider', 'String', 'Specifies which scraper service to use.', 'Options: "firecrawl", "serper", "tavily", "keenable"'],
  ]}
/>

### firecrawlOptions

<OptionTable
  options={[
    ['firecrawlOptions', 'Object', 'Advanced configuration options for Firecrawl scraper.', ''],
  ]}
/>

**Subkeys:**

#### formats

<OptionTable
  options={[
    ['formats', 'Array of Strings', 'Formats to include in the output.', ''],
  ]}
/>

#### includeTags

<OptionTable
  options={[
    ['includeTags', 'Array of Strings', 'Tags to include in the output.', ''],
  ]}
/>

#### excludeTags

<OptionTable
  options={[
    ['excludeTags', 'Array of Strings', 'Tags to exclude from the output.', ''],
  ]}
/>

#### headers

<OptionTable
  options={[
    ['headers', 'Object', 'Headers to send with the request. Can be used to send cookies, user-agent, etc.', ''],
  ]}
/>

#### waitFor

<OptionTable
  options={[
    ['waitFor', 'Number', 'Specify a delay in milliseconds before fetching the content, allowing the page sufficient time to load.', ''],
  ]}
/>

#### timeout

<OptionTable
  options={[
    ['timeout', 'Integer', 'Timeout in milliseconds for the scraping request. Must be a non-negative integer.', 'Default: 7500'],
  ]}
/>

#### maxAge

<OptionTable
  options={[
    ['maxAge', 'Number', 'Returns a cached version of the page if it is younger than this age in milliseconds. If a cached version of the page is older than this value, the page will be scraped.', ''],
  ]}
/>

**Note:** If you do not need extremely fresh data, enabling this can speed up your scrapes by 500%.

#### mobile

<OptionTable
  options={[
    ['mobile', 'Boolean', 'Emulate scraping from a mobile device.', ''],
  ]}
/>

#### skipTlsVerification

<OptionTable
  options={[
    ['skipTlsVerification', 'Boolean', 'Skip TLS certificate verification when making requests.', ''],
  ]}
/>

#### blockAds

<OptionTable
  options={[
    ['blockAds', 'Boolean', 'Enables ad-blocking and cookie popup blocking.', ''],
  ]}
/>

#### removeBase64Images

<OptionTable
  options={[
    ['removeBase64Images', 'Boolean', 'Removes all base 64 images from the output, which may be overwhelmingly long. The image\'s alt text remains in the output, but the URL is replaced with a placeholder.', ''],
  ]}
/>

#### parsePDF

<OptionTable
  options={[
    ['parsePDF', 'Boolean', 'Controls how PDF files are processed during scraping.', ''],
  ]}
/>

#### storeInCache

<OptionTable
  options={[
    ['storeInCache', 'Boolean', 'If true, the page will be stored in the Firecrawl index and cache. Setting this to false is useful if your scraping activity may have data protection concerns. Using some parameters associated with sensitive scraping (headers) will force this parameter to be false.', ''],
  ]}
/>

#### zeroDataRetention

<OptionTable
  options={[
    ['zeroDataRetention', 'Boolean', 'If true, this will enable zero data retention for this scrape (requires prior setup on Firecrawl).', ''],
  ]}
/>

#### location

<OptionTable
  options={[
    ['location', 'Object', 'Geographic location and language settings for scraping.', ''],
  ]}
/>

#### onlyMainContent

<OptionTable
  options={[
    ['onlyMainContent', 'Boolean', 'Only return the main content of the page excluding headers, navs, footers, etc.', ''],
  ]}
/>

#### changeTrackingOptions

<OptionTable
  options={[
    ['changeTrackingOptions', 'Object', 'Configuration for tracking changes in scraped content.', ''],
  ]}
/>

**Example:**
```yaml filename="webSearch"
webSearch:
  firecrawlApiKey: "${FIRECRAWL_API_KEY}"
  firecrawlOptions:
    formats: ["markdown", "rawHtml"]
    includeTags: ["main", "article", ".content"]
    excludeTags: ["nav", "footer", ".ads"]
    waitFor: 2000
    timeout: 10000
    mobile: false
    blockAds: true
    onlyMainContent: true
    location:
      country: "US"
      languages: ["en"]
```

**Note:** For detailed information about Firecrawl scraper options and defaults, see the [Firecrawl API Documentation](https://docs.firecrawl.dev/api-reference/endpoint/scrape).

### tavilyScraperOptions

<OptionTable
  options={[
    ['tavilyScraperOptions', 'Object', 'Configuration options for Tavily Extract (scraper).', ''],
  ]}
/>

**Subkeys:**

<OptionTable
  options={[
    ['extractDepth', 'String', 'The depth of the extraction process. "advanced" retrieves more data including tables and embedded content with higher success but may increase latency. "basic" costs 1 credit per 5 successful URLs, "advanced" costs 2 credits per 5 successful URLs.', 'Options: "basic", "advanced". Default: "basic"'],
    ['includeImages', 'Boolean', 'Include a list of images extracted from the URLs in the response.', 'Default: false'],
    ['includeFavicon', 'Boolean', 'Include the favicon URL for each extracted result.', 'Default: false'],
    ['format', 'String', 'The format of the extracted web page content. "markdown" returns content in markdown format. "text" returns plain text and may increase latency.', 'Options: "markdown", "text". Default: "markdown"'],
    ['timeout', 'Number', 'Timeout in milliseconds. Controls the client-side HTTP timeout. When set, it also sends Tavily a server-side extraction timeout converted to seconds and clamped to 1-60s.', 'Default: 15000 for basic, 30000 for advanced'],
  ]}
/>

**Example:**
```yaml filename="webSearch"
webSearch:
  searchProvider: tavily
  scraperProvider: tavily
  tavilyApiKey: "${TAVILY_API_KEY}"
  # Optional: custom Tavily-compatible endpoints
  # tavilySearchUrl: "${TAVILY_SEARCH_URL}"
  # tavilyExtractUrl: "${TAVILY_EXTRACT_URL}"
  tavilySearchOptions:
    searchDepth: basic
    maxResults: 5
    topic: general
  tavilyScraperOptions:
    extractDepth: basic
```

**Note:** For detailed information about Tavily API options, see the [Tavily API Documentation](https://docs.tavily.com).

### keenableScraperOptions

<OptionTable
  options={[
    ['keenableScraperOptions', 'Object', 'Configuration options for Keenable page fetch.', ''],
  ]}
/>

<OptionTable
  options={[
    ['attributionTitle', 'String', 'Value sent in the X-Keenable-Title attribution header.', ''],
    ['timeout', 'Number', 'Client-side HTTP request timeout in milliseconds.', 'Range: 0-120000. Default: 15000'],
  ]}
/>

Keenable can provide search, page fetch, or both. Its public endpoints work without a key; the optional `keenableApiKey` raises rate limits. Set `rerankerType: "none"` to avoid requiring a reranker key:

```yaml filename="webSearch"
webSearch:
  searchProvider: keenable
  scraperProvider: keenable
  rerankerType: none
  # keenableApiKey: "${KEENABLE_API_KEY}" # Optional
  # keenableApiUrl: "${KEENABLE_API_URL}" # Optional search endpoint
  keenableSearchOptions:
    maxResults: 8
    # site: example.com
  keenableScraperOptions:
    timeout: 15000
```

## Rerankers

### jinaApiKey

<OptionTable
  options={[
    ['jinaApiKey', 'String', 'Environment variable name for the Jina API key. If not set in .env, users will be prompted to provide it via UI.', '${JINA_API_KEY}'],
  ]}
/>

**Note:** Get your API key from [Jina.ai](https://jina.ai/api-dashboard/)

### jinaApiUrl

<OptionTable
  options={[
    ['jinaApiUrl', 'String', 'Environment variable name for the Jina API URL. If not set in .env, users will be prompted to provide it via UI.', '${JINA_API_URL}'],
  ]}
/>

**Note:** This is optional and only needed if you're using a custom Jina instance.

### cohereApiKey

<OptionTable
  options={[
    ['cohereApiKey', 'String', 'Environment variable name for the Cohere API key. If not set in .env, users will be prompted to provide it via UI.', '${COHERE_API_KEY}'],
  ]}
/>

**Note:** Get your API key from [Cohere Dashboard](https://dashboard.cohere.com/welcome/login)

### rerankerType

<OptionTable
  options={[
    ['rerankerType', 'String', 'Specifies which reranker service to use. Set to "none" to skip reranking.', 'Options: "jina", "cohere", "none"'],
  ]}
/>

## General Settings

### scraperTimeout

<OptionTable
  options={[
    ['scraperTimeout', 'Integer', 'Timeout in milliseconds for scraper requests. Must be a non-negative integer.', 'Default: 7500'],
  ]}
/>

### safeSearch

<OptionTable
  options={[
    ['safeSearch', 'Number', 'Safe search filtering level. 0 = OFF (no filtering), 1 = MODERATE (default), 2 = STRICT (maximum filtering).', 'Default: 1 (MODERATE)'],
  ]}
/>

**Note:** Safe search levels align with standard search API conventions. MODERATE filtering is enabled by default to provide reasonable content filtering while maintaining search effectiveness. Tavily does not inherit this global setting by default; use `tavilySearchOptions.safeSearch` only if your Tavily account supports `safe_search`.

## Notes

- API keys can be configured in two ways:
  1. Set the environment variables specified in the YAML configuration
  2. If environment variables are not set, users will be prompted to provide the API keys via the UI
- The configuration supports multiple services for each component (providers, scrapers, rerankers)
- If a specific service type is not specified, the system will try all available services in that category
- Safe search provides three levels of content filtering: OFF (0), MODERATE (1), and STRICT (2)
- Tavily does not inherit the global safe search setting by default; set `tavilySearchOptions.safeSearch` explicitly only when your Tavily account supports `safe_search`
- Keenable supports keyless search and page fetch; its optional key raises public rate limits, and its fetch URL override is environment-only
- SearXNG query behavior (engines, language, time range, request timeout) is configured under `searxngSearchOptions`; see [Choosing engines](#choosing-engines) if searches come back empty
- Never put actual API keys in the YAML configuration - only use environment variable names 

## Setting Up SearXNG

SearXNG is a privacy-focused meta search engine that you can self-host. 
For more information, see the [official SearXNG documentation](https://docs.searxng.org/). 

Here are the steps to set up your own SearXNG instance for use with LibreChat:

### Using Docker Desktop

1. **Search for the official SearXNG image**
   - Open Docker Desktop
   - Search for `searxng/searxng` in the **Images** tab
   - Click **Run** on the official image to automatically pull down and run a container of the image

2. **Running the container**
   - Expand the **Optional Settings** dropdown in the proceeding panel that appears once the download completes
   - Set your desired configuration details (port number, container name, etc.)
   - Click **Run** to start the container

3. **Configure SearXNG for LibreChat**
   - Navigate to the `Files` tab in Docker Desktop
   - Go to `/etc/searxng/settings.yaml`
   - Open the file editor
   - Navigate to the `formats` section
   - Add `json` as an acceptable format so that LibreChat can communicate with your instance
   - Save the file

4. **Restart the container**
   - Restart the container for the changes to take effect

#### Video Guide:

Here's a video to guide you through the process from start to finish in about a minute:

     <Video src="/videos/min_searxng_docker_setup_hq.mp4" title="SearXNG Docker setup walkthrough" />

**Note:** In this example, the instance URL is http://localhost:55011 (port number found under the container name in the top left at the end of the video)

### Configuring LibreChat to use SearXNG

You can configure SearXNG in LibreChat within the UI or through `librechat.yaml`.

#### UI Configuration

1. **Open the tools dropdown in the chat input bar**
![Tools configuration button](/images/web-search/tools_highlight.png)

2. **Click on the gear icon next to Web Search**
![Tools configuration section](/images/web-search/tools_config_highlight.png)

3. **Select SearXNG from the Search Provider dropdown**
![SearXNG dropdown selection](/images/web-search/searxng_dropdown_highlight.png)

4. **Enter your configuration details (e.g. instance URL, scraper type, etc.) and click save**
![Save web search configuration](/images/web-search/web_search_save.png)

5. **Click on the Web Search option in the tools dropdown**
![Web search badge in chat interface](/images/web-search/websearch_highlight.png)

6. **The Web Search badge should now be enabled, meaning your queries can now utilize the web search functionality**
![Web search badge confirmation](/images/web-search/search_badge_confirm.png)

#### YAML Configuration

To configure SearXNG for everyone on the instance instead of per user, set the same values in `librechat.yaml`:

```yaml filename="librechat.yaml"
webSearch:
  searchProvider: "searxng"
  searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}"
  # searxngApiKey: "${SEARXNG_API_KEY}" # Only if your instance requires authentication
  # Required: a self-hosted instance is a private destination, so exempt it from the connection guard
  allowedAddresses:
    - "localhost:55011"
  searxngSearchOptions:
    engines:
      - google
      - bing
      - startpage
      - qwant
    language: "en"
    timeout: 10000
```

The URL and the key are still read from the environment, so pair the config above with:

```bash filename=".env"
SEARXNG_INSTANCE_URL=http://localhost:55011
# SEARXNG_API_KEY=your_api_key
```

<Callout type="warning" title="Private instances need an allowedAddresses entry">
`localhost:55011` is a loopback address, which LibreChat's [SSRF protection](#ssrf-protection-and-private-providers) blocks at connect time. Without the matching `allowedAddresses` entry the search fails even when SearXNG is healthy. The entry must be the exact `host:port` pair from your instance URL.

When LibreChat itself runs in Docker, `localhost` is the LibreChat container, not the SearXNG one. Use a hostname the container can reach, such as the compose service name, and exempt that instead:

```yaml filename="librechat.yaml"
webSearch:
  searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}" # SEARXNG_INSTANCE_URL=http://searxng:8080
  allowedAddresses:
    - "searxng:8080"
```
</Callout>

See [`searxngSearchOptions`](#searxngsearchoptions) for every subkey this block accepts.

#### Troubleshooting empty results

A SearXNG search that returns nothing is almost always the request never reaching a working instance rather than LibreChat dropping results. Work through these in order:

1. **The instance is a private address with no `allowedAddresses` entry.** LibreChat blocks loopback and private destinations at connect time, so a self-hosted instance needs its exact `host:port` listed under [`allowedAddresses`](#ssrf-protection-and-private-providers). This blocks the request outright rather than returning an empty result set.
2. **`json` is missing from `formats`.** LibreChat requests results as JSON. If the `formats` section of your instance settings file does not list `json`, the instance answers with an error page and every search comes back empty. This is step 3 of the Docker setup above.
3. **Every selected engine failed.** SearXNG merges the engines that did answer, so an empty response means none of them returned anything. DuckDuckGo is in the default set (`google,bing,duckduckgo`) and serves CAPTCHAs to most self-hosted instances, which leaves only two engines to carry the search. Set `searxngSearchOptions.engines` explicitly, leave DuckDuckGo out, and list a few more engines.
4. **An engine name does not exist on your instance.** SearXNG silently skips engines it does not recognize, so a typo just means fewer results. Compare your list against `https://your-instance/config`.
5. **The instance is slower than the timeout.** Raise `searxngSearchOptions.timeout` above the default `10000` if your instance queries many engines or sits behind a slow network path.

You can reproduce what LibreChat sends by calling the instance directly:

```bash
curl -s "http://localhost:55011/search?q=librechat&format=json&engines=google,bing,startpage" | head -c 500
```

This runs from your own machine, so it bypasses LibreChat's connection guard entirely. An empty `results` array here confirms the problem is on the SearXNG side; results here while LibreChat still finds nothing points back at `allowedAddresses`.
