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 versionshould 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=falseonce the team is registered so nobody can self-register on an instance that happens to be reachable. - Single user:
WEBUI_AUTH=Falseskips 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-v2model runs locally on CPU and consumes about 500 MB of RAM per worker. SetRAG_EMBEDDING_ENGINE=ollamawithnomic-embed-text, or point it at an embeddings API, to get that memory back. - Content extraction. The default
pypdfextractor leaks memory during heavy ingestion. Switch to Tika or Docling viaCONTENT_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.