Last updated: November 2026 — RAM floors are engineering estimates for Q4_K_M-class GGUF + short context; measure with ollama ps on your box.
For: Self-hosting a private chat UI (Open WebUI) backed by Ollama on CPU (no GPU), with a clear table of which model sizes fit which VPS RAM.
Do not use this guide when:
You have a GPU and want VRAM tables only (different optimization path)
You expect 70B-class models to feel fast on a 4–8 vCPU cloud VPS (they often crawl)
You need multi-user production SLAs (this is a personal/small-team stack)
Your host has < 8 GB RAM and you refuse tiny models (3B class)
Resource
Absolute floor
Comfortable CPU chat
Notes
RAM
8 GB (3B only)
16–32 GB (7B–8B)
OS + Docker + Open WebUI + KV cache
CPU
4 vCPU
8 vCPU
Tokens/s scale with cores + memory bandwidth
Disk
40 GB SSD
100 GB+ NVMe
Models are multi-GB each; leave free space
GPU
none
none
This guide is CPU-only
Ports
80/443 via Caddy
same
Do not expose Ollama :11434 publicly
Approximate weights-only memory plus a practical system RAM recommendation for one interactive user, ~4k–8k context, Ollama default Q4-style pulls:
Model class
Examples
Weights (Q4-ish)
Min system RAM
Comfortable system RAM
CPU feel (rough)
1B–3B
Llama 3.2 3B, Phi-class small
~2–3 GB
8 GB
16 GB
Usable
7B–8B
Llama 3.1 8B, Mistral 7B, Qwen 7B
~4–5 GB
16 GB
24–32 GB
Common sweet spot
13B–14B
older 13B / Qwen 14B
~8–9 GB
24 GB
32 GB
Slower CPU
30B–34B
32B distill class
~20 GB
48 GB
64 GB
Often too slow on cheap VPS
70B
Llama 70B Q4
~38–45 GB
64 GB
96–128 GB
Homelab/workstation, not $10 VPS
Add headroom for: Open WebUI (~0.5–1 GB), Docker, OS page cache, and KV cache (grows with context length). A “16 GB for 7B” box that also runs Immich will OOM — dedicate the box or size up.
Formula people use for ballpark weights: parameters(B) × bits/8 (Q4 → ~0.5 bytes/param) + 1–2 GB runtime/KV fudge. Always confirm with ollama ps after ollama run.
ollama listens on the Compose network (11434)
open-webui talks to http://ollama:11434
caddy publishes 80/443 only; no public 11434
~/llm/
docker-compose.yml
Caddyfile
.env
ollama-data/
open-webui-data/
caddy-data/
caddy-config/
DOMAIN=llm.example.com
ACME_EMAIL=you@example.com
# Optional: pre-pull hint for docs only — pull happens via CLI after up
OLLAMA_MODEL=llama3.2:3b
{$DOMAIN} {
encode gzip
reverse_proxy open-webui:8080
}
services:
ollama:
image: ollama/ollama:0.12.3
container_name: ollama
restart: unless-stopped
volumes:
- ./ollama-data:/root/.ollama
# CPU-only: do not add GPU deploy reservations
expose:
- "11434"
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
open-webui:
image: ghcr.io/open-webui/open-webui:0.6.34
container_name: open-webui
restart: unless-stopped
depends_on:
ollama:
condition: service_healthy
environment:
OLLAMA_BASE_URL: http://ollama:11434
WEBUI_AUTH: "true"
volumes:
- ./open-webui-data:/app/backend/data
expose:
- "8080"
caddy:
image: caddy:2.11.7
container_name: llm_caddy
restart: unless-stopped
depends_on:
- open-webui
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
DOMAIN: "${DOMAIN}"
EMAIL: "${ACME_EMAIL}"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
Pin tags deliberately. Upstream moves fast — bump after reading release notes.
Name
Meaning
Example
DOMAIN
Public hostname for Caddy
llm.example.com
ACME_EMAIL
Let's Encrypt contact
you@example.com
OLLAMA_BASE_URL
Open WebUI → Ollama
http://ollama:11434
WEBUI_AUTH
Force login on WebUI
true
OLLAMA_KEEP_ALIVE (optional)
How long models stay loaded
30m
OLLAMA_NUM_PARALLEL (optional)
Concurrent slots
1 on small VPS
mkdir -p ~/llm && cd ~/llm
# create files above
docker compose up -d
docker compose exec ollama ollama pull llama3.2:3b
docker compose exec ollama ollama run llama3.2:3b "Say OK"
docker compose exec ollama ollama ps
Then open https://llm.example.com, create the admin user, select the model.
Sizing path: start 3b on 8–16 GB → move to 8b only after free -h shows stable headroom under load.
Same as other guides: only Caddy on 80/443. Do not publish 11434:11434 on 0.0.0.0 — unauthenticated Ollama on the internet is a prompt-injection / abuse magnet.
Stop Open WebUI (optional but cleaner): docker compose stop open-webui
Archive ./ollama-data (models — large) and ./open-webui-data (users, chats)
Keep .env + Caddyfile off-box
Restore onto similar RAM class; ollama list should show pulled tags
Re-pull models if you skipped copying ollama-data (bandwidth tradeoff)
Symptom
Cause
Fix
OOM killer / container dies on pull or first prompt
Model > RAM headroom
Drop to smaller tag; add swap only as last resort; upgrade VPS
ollama run works in CLI, WebUI empty model list
Wrong OLLAMA_BASE_URL or not same network
Must be http://ollama:11434 on Compose network
Extremely slow tokens on 13B+
CPU-only + large model
Expect it; use 3B/8B or get a GPU host
Caddy 502
WebUI not up
docker compose logs open-webui
WebUI open to world without password
Auth disabled
WEBUI_AUTH=true; recreate
Disk full after several pulls
Multiple multi-GB models
ollama rm <tag>; keep one daily driver
bind: address already in use 80/443
Another proxy
Shared Caddy or stop host nginx
Healthcheck fails forever
Old tag / binary path
Adjust healthcheck; check docker compose logs ollama
[ ] curl -I https://llm.example.com → valid cert
[ ] Open WebUI login works; model appears
[ ] One short chat completes without OOM (dmesg \| grep -i oom clean)
[ ] ollama ps shows loaded model size consistent with the RAM table
[ ] Port 11434 not reachable from the public internet (nc -vz YOUR_IP 11434 fails from outside)
Your RAM
Pull this first
Avoid
8 GB
3B Q4
7B+ as daily driver
16 GB
7B–8B Q4
13B+ concurrent with other stacks
32 GB
8B comfortable / 13B possible
70B
64 GB+
30B experiments / 70B Q4 painful but possible
Expecting GPT-class latency on CPU
Image pins example: ollama/ollama:0.12.3, ghcr.io/open-webui/open-webui:0.6.34, caddy:2.11.7 — confirm current tags before production. RAM table is for planning; your tokenizer/context settings change residency.