← Back to Blog

Self-Host Open WebUI: Your Own ChatGPT with Ollama, RAG, and Web Search

The document your team wants to ask about is the one that is not allowed to leave the network. Contracts, postmortems, customer records, that internal spec nobody has read since it was written. So you end up with two bad options: a chat tool nobody can use for real work, or a copy-paste ritual that moves the data out anyway.

Running your own frontend solves it properly. Open WebUI is a chat interface you host yourself. It talks to Ollama, OpenAI, Anthropic, or any OpenAI-compatible endpoint, keeps every conversation in a local database, and adds the parts a bare model endpoint does not have: accounts, document retrieval, web search, and a UI your teammates can open from a phone. Version v0.11.3 is the current stable release.

This guide goes from a single docker run to a working knowledge base and web search that does not hand your queries to a third party. The compose file below is validated, and the version tags are ones I checked against the registry.

Prerequisites

  • Docker Engine with the Compose v2 plugin. docker compose version should print a version number.
  • Ollama reachable from your host, installed natively or in a container.
  • A machine that stays on. This is a service, not a one-off script.
  • Disk and memory headroom. The UI container is light; the models are what eat resources. Plan for the weights you actually intend to run.

Step 1: one command to a working UI

docker pull ghcr.io/open-webui/open-webui:v0.11.3
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui ghcr.io/open-webui/open-webui:v0.11.3

Open http://localhost:3000. Two flags carry the weight. -v open-webui:/app/backend/data is where chats, accounts, and uploaded files live, so dropping it means losing everything on the next recreate. -p 3000:8080 maps the container's internal 8080 to port 3000 on your host.

The first account you register becomes the administrator. Every signup after that sits in Pending until an admin approves it.

Pin the version, and not because of paranoia. :main and :latest are rolling tags rebuilt on every merge to the main branch, so an update can land overnight and change behavior while you sleep. :v0.11.3 never moves. The docs also publish :ollama (Ollama bundled in) and :cuda variants if you want either.

Step 2: fix the two things that bite later

Generate a secret key first.

openssl rand -hex 32

Pass the output as WEBUI_SECRET_KEY. Without a stable key, every container recreate signs your users out and invalidates their sessions. It is the most common "why am I logged out again" complaint, and the fix is one environment variable.

Then decide how login works before you hand out the URL:

  • Multi-user: keep auth on, and set ENABLE_SIGNUP=false once the team is registered so nobody can self-register on an instance that happens to be reachable.
  • Single user: WEBUI_AUTH=False skips the login screen entirely.

One warning from the docs that is easy to miss: you cannot switch between single-user mode and multi-account mode after that change. Pick the shape you actually want on day one.

Step 3: a compose stack worth keeping

A single container is fine for a test drive. For something you leave running, put it in compose with the pieces you will want next: a pinned image, a secret from .env, and SearXNG on the same network so web search stays in-house.

services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:v0.11.3
    container_name: open-webui
    ports:
      - "3000:8080"
    volumes:
      - open-webui:/app/backend/data
    environment:
      - OLLAMA_BASE_URL=http://host.docker.internal:11434
      - WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
      - ENABLE_SIGNUP=false
      - ENABLE_WEB_SEARCH=true
      - WEB_SEARCH_ENGINE=searxng
      - SEARXNG_QUERY_URL=http://searxng:8080/search?q=<query>
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - searxng
    restart: unless-stopped

  searxng:
    image: searxng/searxng:latest
    container_name: searxng
    volumes:
      - ./searxng:/etc/searxng:rw
    environment:
      - SEARXNG_BASE_URL=http://localhost:8080/
    cap_drop:
      - ALL
    restart: unless-stopped

volumes:
  open-webui:

A few notes on why it looks like this. The extra_hosts line is what makes host.docker.internal resolve to your host on Linux; Docker Desktop provides that name automatically on macOS and Windows. Both containers share the default network, so Open WebUI reaches SearXNG at http://searxng:8080 without publishing SearXNG to the outside. cap_drop: ALL on the search container follows the docs' hardening advice. You can check the file yourself with docker compose config -q, which stays silent when the syntax and variable interpolation are valid.

Run it:

echo "WEBUI_SECRET_KEY=$(openssl rand -hex 32)" > .env
docker compose up -d

Step 4: point the UI at a model

If Ollama runs on the host, OLLAMA_BASE_URL=http://host.docker.internal:11434 already does it. Running it on another box? Use that address instead. If the model picker comes up empty, go to Admin > Connections, add the URL there, and pull a model from the Model Selector.

The "Ollama" connection type means the Ollama HTTP API on port 11434. A backend that only speaks the OpenAI standard (vLLM, LocalAI, Docker Model Runner) belongs under OpenAI-compatible connections instead, where you get the full feature set for that protocol. You can also register several Ollama instances, and Open WebUI spreads requests across them with random selection, but the model IDs have to match exactly or you get duplicate entries in the picker.

Step 5: query your own documents

There are two ways to use retrieval, and they are not interchangeable. Drag a file into a chat for a one-off question, and it gets chunked and embedded for that conversation only. For anything you will ask about repeatedly, create a knowledge base under Workspace > Knowledge, then attach it to a chat with the # shortcut or bind it to a model in Workspace > Models so every conversation has it.

The stock defaults are fine for one person with a handful of PDFs. Before you put a team on it, change these:

Setting Default Recommended Why
Text splitter character token consistent chunk sizes across document types
Chunk size / overlap 1000 / 100 2000 / 200 more surrounding context per hit, less chance of cutting a sentence in half
Top K 3 15 wider candidate pool for the model to choose from

Three infrastructure settings matter once you pass roughly a hundred documents or ten concurrent users, and all three are documented in the Open WebUI docs:

  • Embeddings. The default all-MiniLM-L6-v2 model runs locally on CPU and consumes about 500 MB of RAM per worker. Set RAG_EMBEDDING_ENGINE=ollama with nomic-embed-text, or point it at an embeddings API, to get that memory back.
  • Content extraction. The default pypdf extractor leaks memory during heavy ingestion. Switch to Tika or Docling via CONTENT_EXTRACTION_ENGINE.
  • Vector database. The default ChromaDB client is SQLite-backed and does not survive multiple workers. PGVector is the only vector database the Open WebUI team maintains.

One more switch worth flipping: ENABLE_KB_EXEC=True gives the model a filesystem-style interface over your knowledge base, with ls, tree, grep, and cat style access. Capable models chain those more reliably than individual search calls. It does nothing for models set to the legacy tool-calling mode, since the built-in tools do not exist there.

Step 6: web search without an API key

SearXNG is a metasearch engine you host, so queries never leave your infrastructure. The Open WebUI docs treat this setup as a community tutorial rather than a supported path, but the steps are short.

git clone https://github.com/searxng/searxng-docker.git
cd searxng-docker
sed -i 's/127.0.0.1:8080/0.0.0.0:8080/' docker-compose.yaml

Create searxng/limiter.toml with the bot-detection limits relaxed, since Open WebUI calls the API directly:

[botdetection.ip_limit]
link_token = false

[botdetection.ip_lists]
block_ip = []
pass_ip = []

Then delete searxng/settings.yml, start the container briefly so it regenerates a fresh one, and stop it again:

rm searxng/settings.yml
docker compose up -d ; sleep 10 ; docker compose down
sed -i 's/- html/- html\n    - json/' searxng/settings.yml
docker compose up -d

That last line is the one people skip. SearXNG ships with HTML output only, and Open WebUI reads JSON. Without the format line, search returns nothing and you get no error explaining why.

Then confirm the Open WebUI side has WEB_SEARCH_ENGINE=searxng and the query URL pointing at the container: http://searxng:8080/search?q=<query>. Because native function calling is the default mode, the model decides on its own when a question needs the live web. The toggle in the prompt field is only there for when you want to force a fresh lookup.

Step 7: two settings that make it feel fast

Titles, tags, follow-up questions, and prompt autocomplete all run on your main chat model until you say otherwise. On a local 30B that means a sluggish text box and wasted compute on a two-word task. Set a small dedicated model under Admin > Interface and that overhead disappears.

The second one only matters if you use hosted models. Tool definitions sit next to the system prompt in the cached prefix, so switching tool categories or toggling web search mid-conversation invalidates the prompt cache from that point on. Keep the tool list stable while you work.

The ConfigVar trap

If you come from plain environment variables, this one costs an afternoon. Many settings, web search included, are marked ConfigVar in the docs. On first launch Open WebUI reads them from your environment and then persists the values internally. Restart with a changed env var and nothing happens, because the stored value wins. The docs are explicit that this is by design.

You have two ways out. Edit those settings in the admin UI, which is where they are meant to live, or set ENABLE_PERSISTENT_CONFIG=False so environment variables always take precedence. Both are legitimate; the second one means admin UI edits stop saving, so pick deliberately.

When to use this vs alternatives

Honest comparison, because the wrong choice here costs more than a container.

Open WebUI LibreChat AnythingLLM Ollama CLI
Focus full platform: chat, knowledge, tools, team features multi-provider chat interface document Q&A workspaces single-machine model runner
License Open WebUI License (branding preservation) MIT MIT MIT
Best when you need knowledge bases, channels, and team roles you want focused chat with strong side-by-side model comparison you mostly want to talk to your documents, desktop first you just want an answer right now

Two of those licenses matter more than people expect. LibreChat and AnythingLLM are MIT, so you can fork and rebrand freely. Open WebUI's current license carries a branding preservation requirement, while earlier contributions keep their original terms. Read the license before planning to white-label it.

When Open WebUI is the wrong answer:

  • You need multiple replicas or more than one Uvicorn worker. The default ChromaDB client is not fork-safe and concurrent writes crash workers, so you must move the vector database out first. That is documented, not a bug you can tune around.
  • Your team does not want to operate anything. A hosted product is cheaper than the hours you will spend on upgrades.
  • One model on one laptop, nobody else using it. The CLI is faster to the answer.

Troubleshooting the usual suspects

Blank page or dead buttons behind a reverse proxy: Open WebUI needs WebSocket support, and proxies that do not forward the upgrade headers will serve you a broken shell. The docs ship working Nginx, Caddy, and HAProxy configs.

Port 8080 conflict: the container listens on 8080 internally and SearXNG defaults to 8080 on the host. Keep the host-side mappings distinct. In the stack above, SearXNG is not published to the host at all, which sidesteps the issue until you need its own UI.

Slow or stuck model picker: a saved endpoint that has gone unreachable drags the model list request to its full timeout. AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST=3 shortens the wait.

Where to go next

You now have a self-hosted chat platform with retrieval and web search, running from a compose file you can read in one sitting. The next moves, roughly in order of payoff: put it behind HTTPS with Caddy or Nginx, back up the open-webui volume, then move to PostgreSQL, Redis, and PGVector if the user count grows past a handful.

References

Need Help Implementing This?

I help teams design and build scalable cloud infrastructure, DevOps pipelines, and production-grade systems.

Book a Free Consultation