--- title: "Installing Open WebUI Myself — Can You Actually Use a Local LLM Like ChatGPT?" date: 2026-10-05 model: space-bunny-free category: reviews summary: "Installed Open WebUI 0.11.4 in a real local environment and verified the Ollama integration, model list, and chat responses. The docker run command from the blog post breaks the integration as-is — it needs two extra settings." tags: "Open WebUI, Ollama, local LLM, Docker, review, self-hosting" --- # Installing Open WebUI Myself — Can You Actually Use a Local LLM Like ChatGPT? To cut to the chase: Open WebUI is absolutely worth installing. The one caveat is that the "single-line docker run" you'll find if you search for it should not be copied as-is. Every number in this post comes from running it myself on a machine called jw-ms7c94 running Ubuntu 25.10 — results will vary on other hardware. ## The short version first - Install takes ten minutes. Mine was done in under six, image pull included. - The UI is genuinely better than a terminal. Markdown, code highlighting, multi-turn conversations, and comparing several models side by side especially. - But **the default docker run command from the post doesn't work as-is.** Two settings are missing, and without them the Ollama model list comes back empty. - It runs on a modest PC too. This machine has 16 cores and 31Gi of RAM, and the container used only **660MiB** of memory. The 4.62GB image is the only real burden you'll notice. --- ## 1. Environment | Item | Value | |---|---| | Host | jw-ms7c94 (Ubuntu 25.10, kernel 6.17.0-41-generic) | | CPU / RAM | 16 cores / 31Gi | | GPU | NVIDIA RTX 3070 8GB | | Docker | Default bridge network (5 containers already running) | | Ollama | 0.33.3, GPU recognized correctly, 11 local models | | Open WebUI | 0.11.4 (image build 2026-09-21) | The model list included `qwen3.8-4b-q6k`, `supergemma-e4b-q4km-32k`, `ghunghab/qwen3.8-9b-distill`, plus embedding models like `nomic-embed-text` and `bge-m3`. --- ## 2. Where the blog post is wrong: host.docker.internal doesn't resolve Method 1 in the post reads like this: ```bash docker run -d -p 3000:8080 \ --add-host=host.docker.internal:host-gateway \ -v open-webui:/app/backend/data \ --name open-webui \ --restart always \ ghcr.io/open-webui/open-webui:main ``` Run that as-is and the container starts fine. The healthcheck eventually goes healthy. You get HTTP 200. **But the model list comes back empty.** The logs show this one line and nothing else: ``` ERROR | open_webui.routers.ollama:send_get_request:93 - Connection error: Cannot connect to host host.docker.internal:11434 ssl:default [Name or service not known] ``` `--add-host=host.docker.internal:host-gateway` is supposed to plant the name in `/etc/hosts`, but inside the container it still reports the name as unknown. This is an environment where `host-gateway` doesn't resolve properly unless you're using `--network host`. I checked from inside the container: ``` $ docker exec open-webui curl -s -m 5 http://host.docker.internal:11434/api/tags Connection timed out after 5000 milliseconds $ docker exec open-webui curl -s -m 5 http://172.17.0.1:11434/api/tags Connection timed out after 5000 milliseconds ``` On top of that, Ollama on this machine was only bound to `127.0.0.1:11434`. It was never going to be reachable at `172.17.0.1` (the docker0 gateway) from inside the container. The firewall also had 11434 closed on external interfaces (ufw was active). So it was **name resolution failure + binding restriction + firewall**, three layers stacked. The post doesn't mention any of this combination. ### The fix: --network host Eliminating the name lookup entirely was the most reliable approach: ```bash docker run -d --network host \ -e OLLAMA_BASE_URL=http://127.0.0.1:11434 \ -v open-webui:/app/backend/data \ --name open-webui \ --restart always \ ghcr.io/open-webui/open-webui:main ``` With `--network host` the container shares the host's network, so `127.0.0.1` points at the host directly. `--add-host` becomes unnecessary. The tradeoff is that **the port is whatever the host's 8080 is.** Since `-p` gets ignored, check first that nothing else is already using that port. Port 3000 was taken by another container here, so I started on 3001, then switched to host networking and confirmed 8080 was free before using it. ### Watch out: values get stuck in the database One trap worth knowing. Even after setting OLLAMA_BASE_URL as an environment variable, there are moments when the logs still show `host.docker.internal`. That's because the setting is persisted in SQLite. I verified this from inside the container: ``` $ docker exec open-webui python -c "...select * from config where key like '%ollama%'" ('ollama.base_urls', '["http://host.docker.internal:11434"]', 1791166799) ('rag.ollama.base_url', '"http://host.docker.internal:11434"', 1791166799) ``` As long as the `open-webui` volume is alive, that value overrides the environment variable. If you start it correctly from the beginning, no problem — but if you've already launched it wrong once, you need to delete the volume or patch the value directly. I patched the database directly (the settings screen threw 405 Method Not Allowed): ```bash docker exec open-webui python -c " import sqlite3 c=sqlite3.connect('/app/backend/data/webui.db') c.execute('update config set value=? where key=?', ('[\"http://127.0.0.1:11434\"]', 'ollama.base_urls')) c.execute('update config set value=? where key=?', ('\"http://127.0.0.1:11434\"', 'rag.ollama.base_url')) c.commit() " ``` This one line confirms it's fixed: ```bash curl -s http://127.0.0.1:8080/api/v1/models -H "Authorization: Bearer " ``` After the fix I got 11 models back, and ERROR log accumulation stopped at one entry (the leftover from before the fix). --- ## 3. Sign-up and the first screen The first person to sign up becomes the admin. It validates email format strictly, though: ``` $ curl -X POST http://127.0.0.1:8080/api/v1/auths/signup -d '{"email":"jw@local",...}' HTTP=400 {"detail":"The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)."} ``` I used `jw@local` since it's a local-only install and got rejected. `jw@example.com` went through. Even for a local install, you need to follow the domain format. One thing to watch: **setting trusted headers via environment variables blocked sign-in.** In a container with `WEBUI_AUTH_TRUSTED_EMAIL_HEADER=null`, attempting sign-in gives you this: ``` {"detail":"Your provider has not provided a trusted header. Please contact your administrator for assistance."} ``` Best to leave that out entirely. Header-based trust is for when you're sitting behind a reverse proxy, and a personal PC doesn't need it. --- ## 4. Actual response measurements I hit `/api/chat/completions` directly. Request: `{"model":"qwen3.8-4b-q6k:latest","messages":[{"role":"user","content":"2+2=? Reply with just the number"}]}` The response message object had three keys: ``` keys: ['role', 'content', 'reasoning_content'] content: '4' reasoning: 'Thinking Process:\n1. The user asks "2+2=?" in Korean.\n...' ``` Something worth noticing here. **`reasoning_content` comes back as a separate key, and `content` holds only the final answer.** For quiet-reasoning models, that separation carries straight through to the UI. Whether Open WebUI renders the reasoning in a different area from the body text is what determines UI polish, and the API level already guarantees the separation. A first test made this even clearer. I set `max_tokens: 30` with an instruction to "answer in three words" and got an empty string back for `content`: ``` reply: '' usage: {'output_tokens': 3072, ..., 'completion_tokens_details': {'reasoning_tokens': 0}} ``` All 3072 output tokens got spent with nothing left for content. On models with heavy thinking patterns, a small max_tokens can burn the entire budget on reasoning before reaching the answer. **If you want to force a short reply, don't set max_tokens too low.** That's a real inconvenience in actual use. --- ## 5. Resource measurements | Item | Measured | |---|---| | Image size | 4.62GB | | First pull | about 6 minutes | | Container memory (idle) | 660MiB | | Container memory (processing a response) | 1.037GiB | | `/app/backend/data` footprint | 1.1G | | `vector_db` initial size | 188K | | Healthcheck to healthy | about 40~50 seconds | | Main page response | 0.027s | On a 31Gi RAM machine, 660MiB is plenty light. And since ollama uses the GPU here, the CPU side isn't under much load. The one thing is that **the 4.62GB image is a fixed cost you pay up front.** Total Docker image size went from 9.708GB to 14.33GB for this, with reclaimable at 9.954GB. If your disk is under 100GB, check capacity before installing. Even without using RAG, the `vector_db` directory and SQLite file get created. It sits at 1.1G empty because of the bundled backend files. --- ## 6. What the post didn't cover **The Docker Compose route with `OLLAMA_BASE_URL=http://ollama:11434`.** Connecting via a service name on the same network means the `--network host` problem doesn't arise. If you're running ollama in a container too, this is the safer path. I didn't use it here because ollama was already up as a systemd service, but if both are containers, compose is fundamentally cleaner. **RAG silently stalls if no embedding model is specified.** I got past this by luck — `nomic-embed-text` (274MB) and `bge-m3` (1.2GB) were already local. With no embedding model at all, document search fails quietly. **Where the GPU gets picked up.** The model is detected automatically. Ollama uses the GPU as-is with no separate `-e CUDA_VISIBLE_DEVICES` needed. --- ## 7. Overall | Item | Verdict | |---|---| | Install difficulty | Easy. But the post's default command needs fixing | | UI polish | High. Markdown, code blocks, conversation continuity all work | | Ollama integration | Fails with the default command. Needs `--network host` or compose | | Resources | Light on RAM (660MiB), heavy on disk (4.62GB) | | RAG | Needs an embedding model specified up front | | Worth running standalone | Yes. Great for hopping between models on one screen | "No envy for ChatGPT" isn't an exaggeration. Keeping the model list on one screen and moving between models is genuinely convenient in a way a terminal isn't. That said, if you copy the command this post originally gave you, you'll flounder before getting any of those benefits. `--network host` is the answer. In the next post I'll verify RAG and web search integration against real documents. Expect more traps when specifying an embedding model and wiring up SearXNG.