2026-07-15
HP Workstation Deployment (Part 2): Second Open WebUI Instance
Follow-up to llama.cpp + Open WebUI Setup. The workstation now runs two llama-server instances side by side, plus a second, fully isolated Open WebUI pointed at both ā and later, two more models added on top.
Architecture
āāāāāāāāāāā
ā You ā
ā(browser)ā
āāāāāā¬āāāāā
āāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāā
ā¼ ā¼
āāāāāāāāāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāā
ā open-webui :8080 ā ā open-webui-2 :8081 ā
āāāāāāāāāāāāā¬āāāāāāāāāāāāā āāāāāāāāāāāāā¬āāāāāāāāāāāāā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā HP Workstation (Quadro P4000, 8GB VRAM) ā
ā ā
ā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāā
ā ā phi-2 :8000ā ā tinyllama :8001 ā ā gemma-3-4b-it:8002ā ā qwen2.5-3b-instruct:8003ā
ā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāBoth Open WebUI instances can reach all four llama-server backends (via OPENAI_API_BASE_URLS) ā which model actually responds depends on which llama-inference*.service is running at the time, since VRAM only fits one or two of the four resident at once (see the VRAM footprint reference near the bottom of this page).
Current state (before adding more models)
ps -aux | grep llamajay 814 /home/jay/llama.cpp/build/bin/llama-server -m tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf --gpu-layers 15 --port 8001 --host 0.0.0.0
jay 815 /home/jay/llama.cpp/build/bin/llama-server -m phi-2.Q4_K_M.gguf --gpu-layers 30 --port 8000 --host 0.0.0.0| Service | Model | Port |
|---|---|---|
llama-inference.service | phi-2 (Q4_K_M) | 8000 |
llama-inference-2.service | TinyLlama 1.1B Chat (Q4_K_M) | 8001 |
open-webui (existing) | UI on host network, pointed only at 8000 | ā |
open-webui-2 (new) | UI pointed at both 8000 and 8001 | 8081 |
The existing open-webui container was left untouched (OLLAMA_BASE_URL=http://127.0.0.1:8000, host networking). Rather than reconfigure it, a second, isolated instance was added so the original setup stays stable.
Why a second container, not a reconfigured one
--network=hoston the original container means it can't be remapped to a different port without disrupting the running service.- A dedicated container/volume keeps chat history, users, and settings separate between the two UIs.
llama-serverspeaks the OpenAI-compatible API (/v1/chat/completions), not native Ollama ā so the new instance is wired up withOPENAI_API_BASE_URLSinstead ofOLLAMA_BASE_URL.
Docker run (manual)
sudo docker run -d \
--name open-webui-2 \
-p 8081:8080 \
--add-host=host.docker.internal:host-gateway \
-v open-webui-2:/app/backend/data \
-e OPENAI_API_BASE_URLS="http://host.docker.internal:8000/v1;http://host.docker.internal:8001/v1" \
-e OPENAI_API_KEYS="sk-not-needed;sk-not-needed" \
-e ENABLE_OLLAMA_API=false \
-e DEFAULT_LOCALE=en-US \
ghcr.io/open-webui/open-webui:mainKey flags:
- No
--network=hostā isolates it from the existing instance and lets it bind cleanly to8081. --add-host=host.docker.internal:host-gatewayā since it's not on host networking, the container needs an explicit route back to the host's127.0.0.1:8000/:8001.OPENAI_API_BASE_URLS/OPENAI_API_KEYSā semicolon-separated lists, one entry per backend.llama-serverdoesn't check the API key, so any placeholder value works.-v open-webui-2:/app/backend/dataā separate Docker volume from the originalopen-webuivolume, so chat history/users don't mix.DEFAULT_LOCALE=en-USā Open WebUI otherwise auto-detects UI language from the browser'sAccept-Languageheader; this pins it to English regardless of browser settings. (If it doesn't take effect for an already-created account, set it manually: Settings ā General ā Language ā English.)
systemd service
sudo vi /etc/systemd/system/open-webui-2.service[Unit]
Description=Open WebUI (secondary instance, port 8081)
After=network.target docker.service llama-inference.service llama-inference-2.service
Requires=docker.service
[Service]
Type=simple
User=jay
ExecStartPre=-/usr/bin/docker rm -f open-webui-2
ExecStart=/usr/bin/docker run \
--name open-webui-2 \
-p 8081:8080 \
--add-host=host.docker.internal:host-gateway \
-v open-webui-2:/app/backend/data \
-e OPENAI_API_BASE_URLS="http://host.docker.internal:8000/v1;http://host.docker.internal:8001/v1" \
-e OPENAI_API_KEYS="sk-not-needed;sk-not-needed" \
-e ENABLE_OLLAMA_API=false \
-e DEFAULT_LOCALE=en-US \
ghcr.io/open-webui/open-webui:main
ExecStop=/usr/bin/docker stop open-webui-2
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsudo docker rm -f open-webui-2
sudo systemctl daemon-reload
sudo systemctl enable open-webui-2.service
sudo systemctl start open-webui-2.service
sudo systemctl status open-webui-2.serviceVerifying
- Open
http://<workstation-ip>:8081ā should load in English. - Log in / sign up.
- Settings ā Connections should show both OpenAI-compatible endpoints (
:8000/v1,:8001/v1). - In a new chat, the model picker should list both phi-2 and TinyLlama 1.1B Chat.
Troubleshooting
- UI loads in the wrong language ā
DEFAULT_LOCALEonly affects new signups on some Open WebUI versions; set the language manually under Settings ā General for existing accounts. - Models don't appear in the picker ā confirm
host.docker.internalresolves inside the container (docker exec -it open-webui-2 getent hosts host.docker.internal); if it doesn't, replace it with the workstation's LAN IP and restart the service. - Port conflict on 8081 ā check nothing else is bound there first:
sudo ss -tlnp | grep 8081.
Mistakes made and lessons learned
1. Wrong flag: --c instead of -c
When bumping TinyLlama's context size, the systemd unit was first edited with --c 2048. llama-server doesn't recognize --c as a long option ā only the short form -c (or the long form --ctx-size) is valid. A flag typo like this can silently fail to apply rather than erroring loudly.
Lesson: verify flags landed by checking the running process args in systemctl status (the CGroup: line), not just that the service says "active."
2. "Context size exceeded" wasn't a GPU/VRAM problem
Open WebUI threw a "Context size has been exceeded" error, and it was initially unclear whether this meant the GPU had run out of memory. nvidia-smi showed plenty of free VRAM, which ruled that out immediately.
Lesson: "context" in an LLM setup almost always means the model's token context window (the -c / --ctx-size flag), not GPU memory. Check nvidia-smi first to rule out VRAM, then check whether -c was ever explicitly set.
3. Setting -c larger than a model's trained context causes silent quality loss
phi-2 was restarted with -c 4096, but phi-2's native trained context is only 2048 tokens. llama-server logs a warning (GENERATION QUALITY WILL BE DEGRADED!), but the service still starts and looks healthy.
Lesson: a "successfully running" service isn't the same as a correctly configured one. Read the startup log lines, not just the Active: running status.
4. ChatML tokens (<|im_start|> / <|im_end|>) leaking into responses
phi-2 is a base/instruct model, not a chat-tuned model ā it was never trained on ChatML formatting. llama-server was nonetheless applying a ChatML-style chat template, and because phi-2's tokenizer doesn't treat <|im_end|> as a real stop token, the model kept generating past its answer and hallucinated a fake next turn, printing the literal control tokens as visible text.
Lesson: the chat template used to format a model's prompt has to match how that model was actually trained. Fix by adding the missing stop sequence explicitly (--reverse-prompt "<|im_end|>" server-side, or a Stop Sequence in Open WebUI's per-model Advanced Params).
5. Picking models: chat-tuned beats base/completion models for a chat UI
Mistakes #3 and #4 trace back to the same root cause: phi-2 and TinyLlama are small/older models not designed for open-ended multi-turn chat. Moving to a properly instruct/chat-tuned current-generation model (e.g. Gemma 3 4B Instruct) avoids both problems by construction.
Lesson: when picking a model for a chat UI, prioritize "instruct/chat-tuned" variants over base/completion models of the same size.
6. Guessing model repo IDs (and trusting a fetched page) leads to broken/fake downloads
hf download google/gemma-3-4b-it-GGUF ... failed with "Repository not found" ā Google doesn't publish a bare -GGUF repo under that name. Separately, a fetched page for a guessed URL (google/gemma-4-E4B-it) came back describing a plausible-sounding model ā but the description was nearly identical to the real, different model Gemma 3n E4B, strongly suggesting a wrong URL or a hallucinated summary. There is no known "Gemma 4" family.
Lesson: never guess Hugging Face repo IDs and run hf download directly against the guess. Resolve the exact repo ID first, and treat any single fetched page describing a model as unverified until confirmed in a browser.
7. hf search doesn't exist ā the real command is hf models list
Guessed at hf search <query> based on convention from other CLIs; the actual hf CLI has no search subcommand. The correct tool is hf models list (alias hf models ls).
Lesson: check <command> --help before running an invented command ā CLI tools don't always follow the naming conventions you'd expect from a different tool.
How to search Hugging Face from the CLI
hf models list (alias hf models ls) is the real search command ā there is no hf search.
# Search by keyword
hf models list --search "gemma-3-4b-it gguf" --sort downloads --limit 10
# Search restricted to a specific publisher
hf models list --search "gemma-3-4b-it" --author google --sort downloads
# Search restricted to models runnable by llama.cpp specifically
hf models list --search "gemma-3-4b" --apps llama.cpp --sort downloads --limit 10
# Search by parameter count range instead of a name
hf models list --search "instruct gguf" --num-parameters min:2B,max:5B --sort downloads
# Qwen models specifically
hf models list --search "qwen2.5 instruct gguf" --sort downloads --limit 10
# List the actual files/quant sizes inside a specific repo before downloading
hf models list unsloth/gemma-3-4b-it-GGUF
hf models list Qwen/Qwen2.5-7B-Instruct-GGUFAlways run the last form (hf models list <repo_id>) before hf download, to confirm the repo exists and see the exact filename/quant available.
Checking file sizes before downloading
hf download <repo_id> <filename> only downloads the one file you name, not the whole repo ā but a GGUF repo typically contains every quantization level of the same model (often 20-30+ files), so it's important to list files first.
hf models list unsloth/gemma-3-4b-it-GGUF -h 7.8 GB gemma-3-4b-it-BF16.gguf
2.4 GB gemma-3-4b-it-IQ4_NL.gguf
2.3 GB gemma-3-4b-it-IQ4_XS.gguf
1.7 GB gemma-3-4b-it-Q2_K.gguf
1.7 GB gemma-3-4b-it-Q2_K_L.gguf
2.1 GB gemma-3-4b-it-Q3_K_M.gguf
1.9 GB gemma-3-4b-it-Q3_K_S.gguf
2.4 GB gemma-3-4b-it-Q4_0.gguf
2.6 GB gemma-3-4b-it-Q4_1.gguf
2.5 GB gemma-3-4b-it-Q4_K_M.gguf
2.4 GB gemma-3-4b-it-Q4_K_S.gguf
2.8 GB gemma-3-4b-it-Q5_K_M.gguf
2.8 GB gemma-3-4b-it-Q5_K_S.gguf
3.2 GB gemma-3-4b-it-Q6_K.gguf
4.1 GB gemma-3-4b-it-Q8_0.gguf
1.2 GB gemma-3-4b-it-UD-IQ1_M.gguf
1.2 GB gemma-3-4b-it-UD-IQ1_S.gguf
1.6 GB gemma-3-4b-it-UD-IQ2_M.gguf
1.3 GB gemma-3-4b-it-UD-IQ2_XXS.gguf
1.7 GB gemma-3-4b-it-UD-IQ3_XXS.gguf
1.8 GB gemma-3-4b-it-UD-Q2_K_XL.gguf
2.2 GB gemma-3-4b-it-UD-Q3_K_XL.gguf
2.5 GB gemma-3-4b-it-UD-Q4_K_XL.gguf
2.8 GB gemma-3-4b-it-UD-Q5_K_XL.gguf
3.6 GB gemma-3-4b-it-UD-Q6_K_XL.gguf
5.2 GB gemma-3-4b-it-UD-Q8_K_XL.gguf
851.3 MB mmproj-BF16.gguf
851.3 MB mmproj-F16.gguf
1.7 GB mmproj-F32.ggufhf models list Qwen/Qwen2.5-3B-Instruct-GGUF -h 4.0 GB qwen2.5-3b-instruct-fp16-00001-of-00002.gguf
2.8 GB qwen2.5-3b-instruct-fp16-00002-of-00002.gguf
1.4 GB qwen2.5-3b-instruct-q2_k.gguf
1.7 GB qwen2.5-3b-instruct-q3_k_m.gguf
2.0 GB qwen2.5-3b-instruct-q4_0.gguf
2.1 GB qwen2.5-3b-instruct-q4_k_m.gguf
2.4 GB qwen2.5-3b-instruct-q5_0.gguf
2.4 GB qwen2.5-3b-instruct-q5_k_m.gguf
2.8 GB qwen2.5-3b-instruct-q6_k.gguf
3.6 GB qwen2.5-3b-instruct-q8_0.ggufKey takeaway: the total size hf models list -h shows is for the entire repo, not a single download. hf download <repo> <one-filename> only fetches that one row. Don't run hf download <repo> without naming a file, or it pulls everything listed (in Gemma's case, ~68GB across every quant + BF16 + mmproj files).
What the quant suffixes mean, briefly:
- The number after
Qis roughly bits-per-weight (Q2ā 2-bit,Q8ā 8-bit) ā lower number = smaller file and faster, but more quality loss. _K_M/_K_S= "K-quant", a mixed-precision scheme (M = medium, S = small), generally better quality-per-byte than plainQ4_0/Q4_1.IQ*= "importance quantization", newer and even smaller for the same rough quality, at the cost of slightly slower inference.UD-*(Unsloth Dynamic) = per-layer adaptive quantization, tends to preserve quality better than a same-numbered standard quant.BF16/F16/F32/fp16= unquantized or near-unquantized full-precision weights ā much larger, not what you want for a VRAM-constrained box.mmproj-*.gguf= the separate vision projector file, only needed for image input (Gemma 3 is multimodal); not needed for text-only chat.
| Model | File | Size |
|---|---|---|
| Gemma 3 4B Instruct | gemma-3-4b-it-Q4_K_M.gguf | 2.5GB |
| Qwen2.5 3B Instruct | qwen2.5-3b-instruct-q4_k_m.gguf | 2.1GB |
Downloading both models
cd ~/llama.cpp/models
hf download unsloth/gemma-3-4b-it-GGUF gemma-3-4b-it-Q4_K_M.gguf --local-dir ~/llama.cpp/models
hf download Qwen/Qwen2.5-3B-Instruct-GGUF qwen2.5-3b-instruct-q4_k_m.gguf --local-dir ~/llama.cpp/modelsOutput:
gemma-3-4b-it-Q4_K_M.gguf: downloading bytes: 100% 2.49GB, 95.0MB/s
gemma-3-4b-it-Q4_K_M.gguf: reconstructing file: 100% 2.49GB / 2.49GB, 168MB/s
ā Downloaded
path: /home/jay/llama.cpp/models/gemma-3-4b-it-Q4_K_M.gguf
qwen2.5-3b-instruct-q4_k_m.gguf: downloading bytes: 100% 2.09GB, 89.8MB/s
qwen2.5-3b-instruct-q4_k_m.gguf: reconstructing file: 100% 2.10GB / 2.10GB, 146MB/s
ā Downloaded
path: /home/jay/llama.cpp/models/qwen2.5-3b-instruct-q4_k_m.ggufCorrection to an earlier assumption: both downloads succeeded without hf auth login ā unsloth/gemma-3-4b-it-GGUF turned out not to be gated (community re-uploads of Gemma weights are often ungated even though Google's own google/gemma-3-4b-it repo is), and Qwen was never gated. The "Warning: unauthenticated requests" line is just a rate-limit notice, not an auth failure.
Running both as new systemd services (ports 8002, 8003)
| Service | Model | Port |
|---|---|---|
llama-inference-3.service | Gemma 3 4B Instruct (Q4_K_M) | 8002 |
llama-inference-4.service | Qwen2.5 3B Instruct (Q4_K_M) | 8003 |
sudo vi /etc/systemd/system/llama-inference-3.service[Unit]
Description=Llama Inference Server (Gemma 3 4B Instruct)
After=network.target
[Service]
Type=simple
User=jay
ExecStart=/home/jay/llama.cpp/build/bin/llama-server \
-m /home/jay/llama.cpp/models/gemma-3-4b-it-Q4_K_M.gguf \
--alias gemma-3-4b-it \
--gpu-layers 99 \
-c 4096 \
--port 8002 \
--host 0.0.0.0
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo vi /etc/systemd/system/llama-inference-4.service[Unit]
Description=Llama Inference Server (Qwen2.5 3B Instruct)
After=network.target
[Service]
Type=simple
User=jay
ExecStart=/home/jay/llama.cpp/build/bin/llama-server \
-m /home/jay/llama.cpp/models/qwen2.5-3b-instruct-q4_k_m.gguf \
--alias qwen2.5-3b-instruct \
--gpu-layers 99 \
-c 4096 \
--port 8003 \
--host 0.0.0.0
Restart=on-failure
[Install]
WantedBy=multi-user.targetNotes on the flags:
--aliasā sets a short, friendly model name reported via/v1/models. Without it,llama-serverreports the full filesystem path (e.g./home/jay/llama.cpp/models/phi-2.Q4_K_M.gguf) as the model ID, which is what Open WebUI then shows in the picker ā ugly, but purely cosmetic, and fixed entirely server-side with this one flag.--gpu-layers 99requests full GPU offload (more than either model actually has, which is fine ā llama.cpp just offloads all of it). Watchnvidia-smiafter starting each one; if VRAM runs out, lower this to partially offload instead of failing outright.-c 4096is well within both models' native trained context (Gemma 3 4B: 128K, Qwen2.5 3B: 32K) ā no quality-degradation warning like the phi-2 mistake.- No
--chat-templateoverride ā both are properly instruct-tuned models with correct template metadata baked into the GGUF, so the ChatML-leak bug from phi-2 shouldn't recur here.
Starting on demand instead of always-on: VRAM is tight ā the workstation has an 8GB Quadro P4000, phi-2 + TinyLlama already use ~2.9GB, and Gemma 3 4B alone pushed usage to 6711MiB/8116MiB once loaded (its real footprint with KV cache is closer to 3.7GB, not just its 2.5GB file size). Qwen2.5 3B doesn't fit alongside everything else at once ā don't enable all four services at boot. Load and start only what you're actively testing:
sudo systemctl daemon-reload
# Start whichever one you want to try ā not both new ones at once
sudo systemctl start llama-inference-3.service # Gemma 3 4B
sudo systemctl status llama-inference-3.service
nvidia-smi
# Free VRAM before starting another, e.g. stop TinyLlama (the weakest model) to make room
sudo systemctl stop llama-inference-2.service
sudo systemctl start llama-inference-4.service # Qwen2.5 3BIf VRAM checks out fine with more than two running at once, sudo systemctl enable llama-inference-3.service (and/or -4) later to make it start at boot like the first pair.
Wiring new models into Open WebUI
Two ways to add the new :8002 / :8003 endpoints to open-webui-2:
Option A: quick, via the Admin UI (no restart needed)
- Open
http://<workstation-ip>:8081, log in as admin. - Profile icon (top-right) ā Admin Panel ā Settings ā Connections.
- Under OpenAI API, click + Add Connection and add one entry per new model: URL
http://host.docker.internal:8002/v1andhttp://host.docker.internal:8003/v1, Keysk-not-neededfor both. - Save, then start a new chat ā the model picker should list all connected models (only the currently-running
llama-serverwill actually respond).
This is stored in the open-webui-2 Docker volume, so it survives container restarts ā just not a full volume wipe.
Option B: persistent, baked into the systemd unit
Update the two -e lines in open-webui-2.service to list all four ports, semicolon-separated, then recreate the container. This is more failure-prone to hand-edit (see the mistake below) ā Option A is the safer default.
Mistake: editing the systemd unit broke the container with "invalid reference format"
Editing ExecStart to add the two extra endpoints resulted in:
docker: invalid reference format
Run 'docker run --help' for more information
open-webui-2.service: Main process exited, code=exited, status=125/n/a"invalid reference format" means the image name argument to docker run got mangled ā almost certainly a missing or stray backslash line-continuation left over from a manual edit, so systemd fed docker run a broken argument list where the image reference merged with a neighboring flag.
Lesson: when a multi-line ExecStart with backslash continuations breaks, don't patch individual lines ā replace the whole block from a known-good copy. Every continued line must end with \ and nothing after it (no trailing spaces), which is easy to break with in-place `vi` edits. Fix was to paste the complete file back in cleanly rather than hunting for the exact broken character.
VRAM footprint reference
| State | VRAM used |
|---|---|
| phi-2 + TinyLlama only | 2969MiB / 8116MiB |
+ Gemma 3 4B (Q4_K_M, -c 4096) | 6711MiB / 8116MiB |
Loading a 2.5GB weight file added ~3.7GB of actual VRAM usage once KV cache for a 4096-token context was included ā a useful reminder for planning capacity that the GGUF file size on disk is not the same as the runtime VRAM footprint.