Hosting your own AI on a Linux VPS with Ollama and Open WebUI keeps your conversations on your own server and ends pay-per-request pricing. The catch: the Ollama API has no authentication at all, so an open port 11434 means free AI for anyone who finds it. This guide walks you through a setup where only Nginx, over HTTPS, can be reached from the outside.
Why an exposed Ollama instance is a problem
Ollama ships a handy REST API, but with no password and no key. Anyone who reaches the port can list your models, pull new ones until the disk is full, delete them, or start generations that keep your VPS CPU pinned. The maintainers consider authentication a job for a reverse proxy, so it is up to you to add it.
This is not a theoretical risk. In February 2026, the leak search engine LeakIX counted more than 12,000 Ollama instances reachable without authentication. The same year, a high-severity flaw (CVE-2026-7482, nicknamed “Bleeding Llama”) allowed attackers to read the memory of any reachable Ollama server without credentials. An instance that only listens locally is simply out of reach for this kind of attack.
The architecture we are building
- Ollama listens only on
127.0.0.1:11434. - Open WebUI runs in Docker and listens only on
127.0.0.1:3000. - Nginx is the only public service: it receives HTTPS traffic on port 443 and forwards it to Open WebUI.
- The firewall only allows SSH, HTTP (for the certificate) and HTTPS.
- Open WebUI handles accounts: one administrator, sign-ups closed.
This guide assumes a Linux VPS running Ubuntu 24.04 or Debian 12, sudo access, Docker already installed and a subdomain such as ai.your-domain.com pointing to the server's IP address.
Which VPS to pick: RAM and CPU sizing without a GPU
On a VPS without a graphics card, everything comes down to RAM and core count. The model must fit entirely in memory, with headroom for the conversation context, Open WebUI (plan for about 1 GB) and the operating system. The sizes below are those of the default (4-bit quantized) builds published in the Ollama library.
| VPS | Realistic models | Examples (download size) | Use case |
|---|---|---|---|
| 4 GB RAM, 2 vCPU | 1 to 3 billion parameters | gemma3:1b (806 MB), qwen3:1.7b (1.4 GB), llama3.2:3b (2.0 GB) | Testing, short summaries, a small assistant |
| 8 GB RAM, 3 vCPU | Up to 4B comfortably, 8B possible but tight | qwen3:4b (2.5 GB), gemma3:4b (3.4 GB), qwen3:8b (5.2 GB) | Daily personal use |
| 12 GB RAM, 4 vCPU | 8B comfortably, 12-14B possible but tight | qwen3:8b (5.2 GB), gemma3:12b (8.2 GB), qwen3:14b (9.3 GB) | Writing, coding, a small team |
| 32 GB RAM or more (dedicated server) | 27 to 32B | gemma3:27b (17 GB), qwen3:32b (20 GB) | Better quality, slow answers without a GPU |
At SoLinux, this maps to the VPS Linux 04G (4 GB, 2 cores, 45 GB NVMe), the VPS Linux 08G (8 GB, 3 cores, 50 GB NVMe) and the VPS Linux 12G (12 GB, 4 cores, 70 GB NVMe), with no commitment, starting at €7 per month for the 04G, available in France, Germany and New York. For regular use, the 08G is the best starting point; the 12G makes sense as soon as you want an 8B model with room to spare or several users.
Be realistic about speed: without a GPU, a 7-8B model writes its answer word by word, at a few words per second, and the CPU sits at 100% during generation. 3-4B models are often the best trade-off. NVMe storage mostly matters for the initial loading of the model into memory.
Step 1: prepare the server and the firewall
Start by updating the system, then close everything that is not strictly needed. With UFW:
sudo apt update && sudo apt upgrade -y
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
If you prefer nftables directly (not alongside UFW), here is the minimal equivalent for the input chain, to place in /etc/nftables.conf:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif "lo" accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
tcp dport { 22, 80, 443 } accept
}
}
sudo nft -c -f /etc/nftables.conf # check the syntax
sudo systemctl enable --now nftables
A classic trap: a port published by Docker with -p 3000:8080 is open on every interface and bypasses UFW rules, because Docker writes its own firewall rules. That is why this guide never publishes a Docker port.
Step 2: install Ollama and keep it local
Ollama installs on Linux with its official script, which creates an ollama user and a systemd service:
curl -fsSL https://ollama.com/install.sh | sh
Then check that the service is running:
ollama --version
sudo systemctl enable --now ollama
sudo systemctl status ollama
By default, Ollama listens on 127.0.0.1:11434. Many tutorials tell you to set OLLAMA_HOST to 0.0.0.0 so Docker can reach it: that is exactly what exposes the API. Instead, pin the local address explicitly with a systemd override that survives updates:
sudo systemctl edit ollama
Add these lines in the editable area:
[Service]
Environment="OLLAMA_HOST=127.0.0.1:11434"
Environment="OLLAMA_MAX_LOADED_MODELS=1"
Environment="OLLAMA_NUM_PARALLEL=1"
The last two variables limit Ollama to one loaded model and one request at a time, which keeps RAM under control on a CPU-only VPS. Reload and check what it listens on:
sudo systemctl daemon-reload
sudo systemctl restart ollama
sudo ss -tlnp | grep 11434
# expected: 127.0.0.1:11434, never 0.0.0.0:11434 or [::]:11434
curl http://127.0.0.1:11434
# expected: Ollama is running
Next, pull a model that fits your VPS and test it from the command line:
ollama pull qwen3:4b
ollama run qwen3:4b "Explain in one sentence what a reverse proxy is."
ollama list
Step 3: run Open WebUI with Docker, with no public port
The Open WebUI quick start command publishes port 3000 and reaches Ollama through host.docker.internal, which forces Ollama to listen on every interface. We do it differently: the container shares the host network stack (--network=host), reaches Ollama on 127.0.0.1, and itself listens only on 127.0.0.1:3000 thanks to the HOST and PORT variables.
# fixed secret key: without it, sessions reset on every restart
sudo mkdir -p /etc/open-webui
openssl rand -hex 32 | sudo tee /etc/open-webui/secret > /dev/null
sudo chmod 600 /etc/open-webui/secret
DOMAIN=ai.your-domain.com # your subdomain
sudo docker run -d --name open-webui \
--network=host \
-e HOST=127.0.0.1 \
-e PORT=3000 \
-e OLLAMA_BASE_URL=http://127.0.0.1:11434 \
-e WEBUI_SECRET_KEY="$(sudo cat /etc/open-webui/secret)" \
-e CORS_ALLOW_ORIGIN="https://$DOMAIN" \
-v open-webui:/app/backend/data \
--restart always \
ghcr.io/open-webui/open-webui:main
The open-webui volume stores accounts, conversations and settings. In production, you can replace the main tag with a specific version number so you decide when updates happen. Check the listening address after a minute (the first start takes a while):
sudo ss -tlnp | grep 3000
# expected: 127.0.0.1:3000
curl -I http://127.0.0.1:3000
Step 4: create the admin account before exposing anything
The first account created in Open WebUI becomes the administrator. Do not leave that seat to a stranger who finds your subdomain before you do: create it before configuring Nginx, through an SSH tunnel. From your computer:
ssh -L 3000:127.0.0.1:3000 user@VPS_IP
Then open http://localhost:3000 in your computer's browser and create the admin account with a long, unique password. Next, in the admin settings (users or sign-up section, depending on your version):
- turn off new sign-ups;
- if you must keep them open for a while, leave the default role for new users on pending, so that you approve every account yourself;
- create your team members' accounts yourself from user management.
Never disable authentication (WEBUI_AUTH=False) on an instance reachable from the Internet.
Step 5: Nginx over HTTPS in front of Open WebUI
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginx
Add a login rate limit in /etc/nginx/conf.d/limits.conf:
limit_req_zone $binary_remote_addr zone=owui_login:10m rate=5r/m;
Then create /etc/nginx/sites-available/open-webui. Open WebUI uses WebSockets and streams its answers, so you need HTTP/1.1, the Upgrade headers and buffering turned off; otherwise answers arrive in one block or get cut off.
server {
listen 80;
server_name ai.your-domain.com;
client_max_body_size 50M;
# throttle password attempts
location = /api/v1/auths/signin {
limit_req zone=owui_login burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1800s;
proxy_send_timeout 1800s;
}
}
Enable the site, test the configuration and request the certificate. Certbot adds the HTTPS block on port 443 and the HTTP redirect by itself:
sudo ln -s /etc/nginx/sites-available/open-webui /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d ai.your-domain.com --redirect
sudo certbot renew --dry-run
For a small team, you can add a second barrier with auth_basic in the location / block, or restrict access to a few IP addresses with allow and deny. These protections come on top of the Open WebUI account; they do not replace it.
Check that nothing is exposed
On the server, list listening ports: only 22, 80 and 443 should appear on a public address.
sudo ss -tlnp
From your computer, test the VPS IP address: ports 11434 and 3000 must refuse the connection or time out. Only your subdomain over HTTPS, opened in your browser, should show the login page.
nc -zv -w 3 VPS_IP 11434 # must fail
nc -zv -w 3 VPS_IP 3000 # must fail
Keep the setup up to date
- Ollama: run the official install script again. Your systemd override is kept; still check the listening address with
ss -tlnpafter each update. CVE-2026-7482 is fixed since version 0.17.1: check yours withollama --versionand do not leave an old version running. - Open WebUI:
docker pullthe image, thendocker stop,docker rmand run the samedocker runcommand again. The volume keeps your data. - Backups: back up the
open-webuivolume regularly (accounts and conversations); models can simply be downloaded again. - Logs:
journalctl -u ollama,docker logs open-webuiand/var/log/nginx/to spot repeated login attempts.
FAQ
Do I need a GPU to run Ollama on a VPS?
No. Ollama runs on the CPU alone. Answers are slower than with a graphics card, but a 3 to 8 billion parameter model is perfectly usable on a VPS with 8 to 12 GB of RAM.
How can I use the Ollama API from another machine without exposing it?
Use an SSH tunnel: ssh -L 11434:127.0.0.1:11434 user@VPS_IP, then query http://localhost:11434 from your computer. For a third-party application, Open WebUI can also issue per-user API keys, behind your HTTPS Nginx.
Open WebUI shows no models. What should I do?
Check that ollama list returns models, that curl http://127.0.0.1:11434 answers, and that the container was started with --network=host and OLLAMA_BASE_URL=http://127.0.0.1:11434. You can also check the connection in the admin settings, in the connections section.
How many people can use it at the same time?
Without a GPU, expect one generation at a time; other requests wait their turn. For two or three occasional users, a 12G VPS with a 4B or 8B model works well; beyond that, a dedicated server with more cores and RAM becomes necessary.