A private ChatGPT on a VPS is possible, but a small server gives you a small, slow model. Ollama plus Open WebUI installs in minutes. Choosing the right RAM, and knowing how slow CPU-only generation is before you pay for a year of hosting, is what saves you from a disappointing weekend.
This guide does the sizing math, gives you a table you can reproduce, and shows a Docker Compose stack that doesn't expose the model port. I computed every number below with a script I ran on 2026-10-08 and checked install steps against the Ollama and Open WebUI docs. I did not run a model on a VPS in this session, so I have no measured tokens-per-second to show you. I give a ceiling calculated from memory bandwidth instead, and a command so you can measure your own server in two minutes.
For connecting a local model to an assistant, see OpenClaw with a local LLM through Ollama. For which open models exist right now, see open-weight models in 2026.
How much RAM does a local LLM need?
Weights first. The rule: gigabytes of weights = parameters in billions x bits per weight / 8. Quantization shrinks bits per weight. llama.cpp's quantize README lists Q4_K_M at 4.8944 bits per weight and Q8_0 at 8.5008 for Llama 3.1 8B. That 4.89 matches Ollama's own file sizes: its library page lists llama3.1:8b at 4.9 GB and llama3.1:70b at 43 GB, which work out to 4.88 and 4.87 bits per weight if you take the models to have about 8.03 and 70.6 billion parameters (parameter counts from memory of the model cards, not re-checked today).
Here is the table from my script:
| Parameters | FP16 | Q8_0 (8.5 bits) | Q4_K_M (4.89 bits) |
|---|---|---|---|
| 3B | 6.0 GB | 3.2 GB | 1.8 GB |
| 8B | 16.0 GB | 8.5 GB | 4.9 GB |
| 14B | 28.0 GB | 14.9 GB | 8.6 GB |
| 32B | 64.0 GB | 34.0 GB | 19.6 GB |
| 70B | 140.0 GB | 74.4 GB | 42.8 GB |
Weights aren't the whole bill. Every token of context adds to the KV cache: 2 (keys and values) x layers x KV heads x head dimension x bytes per value. I pulled the architecture from the Llama 3.1 config.json files: the 8B model has 32 layers, 8 KV heads and a hidden size of 4096 across 32 attention heads (head dimension 128), and the 70B has 80 layers with the same 8 KV heads and 128 dimension. In 16-bit that is:
| Context | Llama 3.1 8B KV cache | Llama 3.1 70B KV cache |
|---|---|---|
| 4,096 | 0.50 GiB | 1.25 GiB |
| 8,192 | 1.00 GiB | 2.50 GiB |
| 32,768 | 4.00 GiB | 10.00 GiB |
| 131,072 | 16.00 GiB | 40.00 GiB |
That's 128 KiB per token for the 8B model. Ollama's FAQ says the default context is 4096 tokens, and that memory use scales with parallel requests times context length. So a long document pasted into chat costs RAM as well as time. Ollama can also quantize the cache (OLLAMA_KV_CACHE_TYPE, default f16, with q8_0 and q4_0 listed as smaller options, which needs Flash Attention enabled). I haven't tested the quality cost.
Which VPS size fits which model?
My fit rule: weights + KV cache at 8K context + 1 GB runtime overhead must fit in total RAM minus 2 GB for the OS, Docker and Open WebUI. The overhead and the 2 GB are my assumptions, not measured figures. For the 14B and 32B rows I reused the 8B model's KV cache as a stand-in, so treat those as rough.
| Plan (hostinger.com, 2026-10-09) | RAM | Model I'd try | Verdict |
|---|---|---|---|
| KVM 1, 1 vCPU | 4 GB | 3B Q4 | Runs. A toy for summaries and drafts |
| KVM 2, 2 vCPU | 8 GB | 3B Q4 comfortably; 8B Q4 | 8B needs about 6.9 GB against 6 available by my rule. Only with a short context and nothing else running |
| KVM 4, 4 vCPU | 16 GB | 8B Q4 or 14B Q4 | Fits, with room for a 16K context on 8B |
| KVM 8, 8 vCPU | 32 GB | Up to 32B Q4 | Fits (about 21.6 GB), but slow, see below |
Plan specs are from Hostinger's VPS page: KVM 1 is 1 vCPU and 4 GB, KVM 2 is 2 vCPU and 8 GB, KVM 4 is 4 vCPU and 16 GB, KVM 8 is 8 vCPU and 32 GB. Prices on 2026-10-09 were $6.49, $8.99, $12.99 and $25.99 a month, with renewal prices of $11.99, $14.99, $28.99 and $49.99 (shown "for 2 years"; I don't know the initial term, so read the checkout page). Prices change, so check before you buy.
If your plan is the 16 GB tier, the one I'd pick for this job is Hostinger KVM 4. Some links on this page are affiliate links; they cost you nothing and I only link tools I'd mention anyway. It isn't the only choice. Hetzner, DigitalOcean and Linode sell comparable CPU servers, and a GPU box from a GPU cloud is a different class of machine entirely.
What does CPU-only inference really feel like?
Slow in two different ways, and people usually notice only one.
Generation is memory-bound. To produce each token, the CPU reads roughly all the weights from RAM once. So the ceiling on tokens per second is memory bandwidth divided by model size. I don't know what bandwidth a given VPS gets (it's shared, and the provider doesn't publish it), so here is the ceiling at four assumed bandwidths:
| Model | 10 GB/s | 20 GB/s | 40 GB/s | 80 GB/s |
|---|---|---|---|---|
| 8B Q4 (4.9 GB) | 2.0 | 4.1 | 8.2 | 16.3 |
| 14B Q4 (~8.6 GB) | 1.2 | 2.3 | 4.7 | 9.3 |
| 32B Q4 (~19.6 GB) | 0.5 | 1.0 | 2.0 | 4.1 |
| 70B Q4 (43 GB) | 0.2 | 0.5 | 0.9 | 1.9 |
These are tokens per second, upper bounds, with assumed inputs. My own rule of thumb, not a measurement: around 6 tokens per second is readable as it streams, and below 3 it starts to feel broken. A third-party benchmarking guide I found while researching put CPU-only desktop 7 to 8B Q4 at 3 to 8 tokens per second; I couldn't verify that source, so treat it only as a sanity check on the table.
Prompt processing is compute-bound. Before the first word appears, the server must chew through your whole prompt, at about 2 x parameters floating-point operations per token. For an 8B model and a 2,000 token prompt (a pasted page plus instructions), my script gives 320, 160 or 80 seconds at an assumed 100, 200 or 400 GFLOP/s sustained. Those throughputs are guesses for a shared 4 vCPU slice. The point: a long document can mean minutes of silence before any output. Chat with short messages is fine. "Summarize this PDF" is where a CPU VPS hurts.
Measure yours instead of trusting me. After installing, run this and read the eval rate (generation) and prompt eval rate lines it prints at the end:
ollama run llama3.1:8b --verbose "Explain what a reverse proxy does in three sentences."
I haven't run it on a VPS here, so I can't show you the output. A calculation I did like: if an 8B model ran at 5 tokens per second around the clock for a 30-day month, it would produce 2,592,000 s x 5 = about 13 million tokens. That is the upper limit on what a fully busy server can make.
What does the Docker Compose stack look like?
You can run Ollama directly on the host, or in Docker next to Open WebUI. I prefer one Compose file so nothing listens on a public port except the proxy. Ollama's docs give the CPU-only command as docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama. Note the -p 11434:11434: published like that, it listens on every interface, and Ollama has no authentication. Skip the -p.
Files (.env, Caddyfile, compose.yaml). The .env:
CHAT_DOMAIN=chat.yourdomain.com
OLLAMA_VERSION=<pin from Docker Hub>
OPEN_WEBUI_VERSION=<pin a vX.Y.Z tag from the Open WebUI releases page>
WEBUI_SECRET_KEY=<openssl rand -hex 32>
Open WebUI's docs say :main and :latest are rolling builds, :vX.Y.Z is fixed, and that the secret key should stay constant or recreating the container logs everyone out. The Caddyfile:
{$CHAT_DOMAIN} {
reverse_proxy open-webui:8080
}
The compose.yaml:
services:
ollama:
image: ollama/ollama:${OLLAMA_VERSION:?set OLLAMA_VERSION in .env}
restart: unless-stopped
environment:
OLLAMA_KEEP_ALIVE: 30m
OLLAMA_NUM_PARALLEL: 1
OLLAMA_MAX_LOADED_MODELS: 1
OLLAMA_CONTEXT_LENGTH: 4096
volumes:
- ollama:/root/.ollama
# No "ports:" on purpose. Open WebUI reaches it at http://ollama:11434.
open-webui:
image: ghcr.io/open-webui/open-webui:${OPEN_WEBUI_VERSION:?set OPEN_WEBUI_VERSION in .env}
restart: unless-stopped
depends_on:
- ollama
environment:
OLLAMA_BASE_URL: http://ollama:11434
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:?set WEBUI_SECRET_KEY in .env}
ENABLE_SIGNUP: "False"
volumes:
- open-webui:/app/backend/data
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
- open-webui
ports:
- "80:80"
- "443:443"
environment:
CHAT_DOMAIN: ${CHAT_DOMAIN:?set CHAT_DOMAIN in .env}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
ollama:
open-webui:
caddy_data:
caddy_config:
I validated this with docker compose config --quiet and a dummy .env; it parsed with no errors. I did not start the containers. The Ollama settings are all from Ollama's FAQ: OLLAMA_KEEP_ALIVE (default 5 minutes before unload), OLLAMA_NUM_PARALLEL (default 1), OLLAMA_MAX_LOADED_MODELS and OLLAMA_CONTEXT_LENGTH (default 4096). Keeping the model loaded for 30 minutes avoids a reload on every chat after a pause, but holds the RAM.
One caveat on ENABLE_SIGNUP. Open WebUI's environment reference lists it as a persistent setting (default True), meaning the value saved after first launch can win over the variable on later starts. Its quick-start page says the first account you create is the administrator and that sign-up turns itself off once that exists, so create your admin account straight away, then check Settings > Admin > Authentication.
Then, unrun:
docker compose up -d
docker compose exec ollama ollama pull llama3.1:8b
docker compose exec ollama ollama list
Open https://chat.yourdomain.com, create the admin account, and pick the model. Alternatively Open WebUI publishes an :ollama image tag that bundles Ollama for CPU in one container; the two-container layout lets you update them separately.
When is an API the better choice?
More often than people admit. Three honest comparisons:
- Speed. Hosted models run on GPUs with far more memory bandwidth than a VPS slice, so they generate faster and don't make you wait minutes on a long prompt. Compare by timing the same prompt on both.
- Quality. The models you can fit in 16 GB are much weaker than current hosted ones. Whatever "private ChatGPT" you build, your users will notice.
- Cost. Break-even is just division. A 16 GB plan at $12.99 a month equals this many tokens at three hypothetical API prices (blended price per million tokens, not any vendor's real rate): $0.15 gives 86.6 million, $1 gives 13.0 million, $3 gives 4.3 million. Remember the earlier ceiling of about 13 million tokens a month for a server that never idles at 5 tokens per second. At $1 per million, a fully busy server only just matches the API, and a lightly used one loses badly. Check real rates on AI model pricing in 2026.
So the case for self-hosting is rarely cost. It's one of: the text must not leave your machine (see what not to paste into ChatGPT), you want an offline or fallback model, you want a flat bill, or you're learning. For the privacy case, a 14B or 8B model for summaries and rewriting of short text is a sensible fit. For heavy reasoning or long documents, call an API, or rent a GPU.
A middle path: Open WebUI can also talk to hosted APIs. Keep the local model for sensitive material and route everything else to an API model.
What do you lock down before real use?
- Only Caddy publishes ports. After
docker compose up, rundocker compose psand confirm 11434 and 8080 are not listed as published. - Sign-ups off, strong admin password, and the secret key stored outside the server.
- A firewall on the host, understanding that Docker-published ports bypass ufw. The hardening checklist covers this in detail.
- Backups of the
open-webuivolume (users, chats, settings). Theollamavolume only holds downloaded models you can pull again.
Should you buy the VPS?
Run this check first. Decide the model you'd actually use, find its size in the table above, add 1 GiB per 8K tokens of context for an 8B-class model, and add 3 GB for everything else. If the sum is under the plan's RAM and you accept a few tokens per second, go ahead. If you want fast answers on long documents, you're shopping for a GPU or an API key, not a bigger CPU plan.



