
Hosting your own LLM endpoint keeps prompts and responses out of third-party logs. It can also reduce jitter because the model sits close to the apps calling it.
This walkthrough shows you how to install Ollama on a VPS, run it under systemd, and publish it through Nginx with HTTPS. You decide exactly what gets exposed.
The steps assume Ubuntu Server LTS on a typical VPS. The same setup also works on a dedicated server, especially if you later add more cores or a GPU.
What you’ll build (and what it is not)
- Ollama API bound to
127.0.0.1:11434(not exposed to the public internet). - Nginx as the only public entry point, terminating TLS and proxying requests to Ollama.
- Access controls so you can keep it private (VPN-only, IP allowlist, or HTTP auth).
- Basic verification via
curland Nginx logs.
This is not a “host a public ChatGPT replacement” guide. Public LLM endpoints get abused fast unless you add auth, rate limits, quotas, and monitoring.
Prerequisites and sizing (CPU/RAM reality check)
Before you touch the install, decide what “fast enough” means for your use case. On CPU-only VPS plans, small models can work well for internal tools and light chatbot traffic.
They just won’t feel instantaneous.
- OS: Ubuntu Server LTS (root or sudo access)
- Disk: 30–60 GB free (models are large; keep headroom for logs and updates)
- CPU-only starting point: 4 vCPU / 8–16 GB RAM for small quantized models
- Comfortable CPU-only: 8 vCPU / 16–32 GB RAM for better concurrency
As a rough guide, a quantized 7B–8B model often fits comfortably in RAM on a 16 GB VPS. Larger models often push you toward 32 GB+ and can still feel sluggish on CPU.
If you expect multiple concurrent users, prioritize more CPU cores, not only more RAM.
If you need predictable throughput, move to a larger instance sooner rather than later. A HostMyCode VPS is a solid baseline for private endpoints.
Dedicated servers are the next step once CPU stays saturated or you want GPU hardware.
Step 1: Prepare your VPS (updates, user, firewall)
SSH into the server and patch the base system:
sudo apt update
sudo apt -y upgrade
sudo reboot
Reconnect after the reboot.
If you haven’t set up a firewall yet, do it now. In most cases, you only want SSH and HTTPS reachable from the internet:
sudo apt -y install ufw
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
If you want a fuller walkthrough, see Set Up UFW Firewall on Ubuntu for a Web Server.
For this guide, the critical point is simple: do not open port 11434 to the internet.
Step 2: Install Ollama on Ubuntu
Use the official installer script. It’s the most direct approach on Ubuntu.
Run it via sudo:
curl -fsSL https://ollama.com/install.sh | sudo sh
Check that the service is up:
sudo systemctl status ollama --no-pager
If it isn’t running yet, enable and start it:
sudo systemctl enable --now ollama
Then confirm the local API responds:
curl -s http://127.0.0.1:11434/api/tags | head
You should see JSON (often an empty list until you pull a model).
Step 3: Bind Ollama to localhost (do not expose it)
You want Ollama listening only on 127.0.0.1. The cleanest method is a systemd override that sets OLLAMA_HOST.
Create an override:
sudo systemctl edit ollama
Add this:
[Service]
Environment="OLLAMA_HOST=127.0.0.1:11434"
Reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart ollama
Verify what address it’s listening on:
sudo ss -ltnp | grep 11434 || true
You should see 127.0.0.1:11434. If it’s bound to 0.0.0.0, stop here and fix that before you put Nginx in front.
Step 4: Pull a small model for testing
On a CPU-only VPS, start small. You want to validate the full path (Ollama → Nginx → TLS) without turning “testing” into a long wait.
Pull a model:
ollama pull llama3.2
Then run a quick local prompt:
ollama run llama3.2 "Write a two-sentence status update for a server maintenance window."
If it feels slow, that’s normal on smaller CPU plans. What matters is whether it works for your traffic.
You can tune models and quantizations later. First, get the deployment and exposure rules right.
Step 5: Install and configure Nginx as a reverse proxy
Install Nginx:
sudo apt -y install nginx
sudo systemctl enable --now nginx
Before requesting TLS, create a DNS record. You need a hostname (for example, ollama-api.example.com) pointing to your VPS public IP.
If you manage domains through HostMyCode, you can do this via domains DNS management.
Create an Nginx site config:
sudo nano /etc/nginx/sites-available/ollama-api
Paste this HTTP-only server block (TLS comes next). Replace ollama-api.example.com with your real hostname:
server {
listen 80;
server_name ollama-api.example.com;
access_log /var/log/nginx/ollama-api.access.log;
error_log /var/log/nginx/ollama-api.error.log;
# Basic hardening headers (safe defaults)
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy no-referrer always;
location / {
proxy_pass http://127.0.0.1:11434;
proxy_http_version 1.1;
# Keep connections sane for streaming responses
proxy_read_timeout 300s;
proxy_send_timeout 300s;
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;
# Ollama can stream tokens; avoid buffering
proxy_buffering off;
}
}
Enable the site and test the config:
sudo ln -s /etc/nginx/sites-available/ollama-api /etc/nginx/sites-enabled/ollama-api
sudo nginx -t
sudo systemctl reload nginx
Test from your laptop (or from the VPS itself, using the hostname):
curl -s http://ollama-api.example.com/api/tags | head
Step 6: Add TLS with Let’s Encrypt (Certbot)
Use HTTPS if you’ll ever send prompts, API keys, or internal code snippets. Let’s Encrypt + Certbot is the standard setup.
Install Certbot and the Nginx plugin:
sudo apt -y install certbot python3-certbot-nginx
Request a certificate:
sudo certbot --nginx -d ollama-api.example.com
Certbot will update your Nginx config and set up renewals. Confirm the timer exists:
systemctl list-timers | grep -E 'certbot|letsencrypt' || true
If you need a deeper troubleshooting flow (chain issues, renewal problems, mixed content), use HostMyCode’s guide: TLS setup guide tutorial (2026).
Step 7: Lock down access (pick one approach)
At this point, the endpoint is reachable over the internet. Don’t leave it wide open unless you’re intentionally offering a public API.
Pick one of the patterns below and apply it deliberately.
Option A (recommended): VPN-only access
This works best for admin teams and internal services. Put operators and calling services on a VPN, then restrict Nginx to the VPN subnet.
If you don’t have WireGuard set up yet, follow: Set Up WireGuard VPN on Ubuntu VPS for Admin Access.
Then add an allowlist to your Nginx server block (replace 10.8.0.0/24 with your VPN subnet):
# Inside the server { } block
allow 10.8.0.0/24;
deny all;
Reload Nginx:
sudo nginx -t && sudo systemctl reload nginx
Option B: IP allowlist (quick and effective)
If your office or app servers have stable IPs, allowlist them:
allow 203.0.113.10;
allow 203.0.113.11;
deny all;
Be realistic about the tradeoff. If your ISP changes your IP, you can lock yourself out until you update the config.
Option C: HTTP Basic Auth (works everywhere, not perfect)
Basic Auth blocks casual scanning and drive-by requests. It does not replace proper per-user auth or quotas.
Install tools and create a password file:
sudo apt -y install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd ollamauser
Add this to your Nginx location block:
location / {
auth_basic "Private LLM";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:11434;
proxy_http_version 1.1;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
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;
}
Reload Nginx and test with credentials:
sudo nginx -t && sudo systemctl reload nginx
curl -u ollamauser -s https://ollama-api.example.com/api/tags | head
Step 8: Verify the endpoint behaves exactly as intended
These checks catch the most common “accidentally public” mistakes.
- Ollama is not reachable on the public IP:
curl -s http://YOUR_SERVER_PUBLIC_IP:11434/ || trueThis should fail (timeout/connection refused) if you bound to localhost and didn’t open the port.
- HTTPS works and redirects from HTTP:
curl -I http://ollama-api.example.com curl -I https://ollama-api.example.com - Nginx is the only public entry point:
sudo tail -n 50 /var/log/nginx/ollama-api.access.log - Ollama logs look normal:
sudo journalctl -u ollama -n 100 --no-pager
Step 9: Try a real API call through Nginx
Ollama exposes an HTTP API. Here’s a minimal request sent through your Nginx/TLS endpoint using /api/generate:
curl -s https://ollama-api.example.com/api/generate \
-H 'Content-Type: application/json' \
-d '{
"model": "llama3.2",
"prompt": "Give me three bullet points to harden an Ubuntu VPS.",
"stream": false
}' | jq
If you enabled Basic Auth, add -u user. If you used VPN or IP allowlisting, run the command from an allowed network.
No jq installed? Install it:
sudo apt -y install jq
Operational notes: updates, capacity, and safe exposure
Once the endpoint is reachable, usage tends to creep up. Keep these operator basics in mind:
- Model storage grows fast. Track
/usr/share/ollama(or wherever your install stores models) and keep 20–30% disk free. - CPU saturation looks like “slow answers”. Check load and throttling:
uptime htop - Nginx timeouts matter. If generations get cut off, raise
proxy_read_timeoutfor long prompts. - Don’t skip monitoring. If this endpoint becomes a dependency, alert on CPU, RAM, disk, and Nginx 5xx errors. Use Netdata + uptime checks as a practical baseline.
If you later put Ollama behind an app (chat UI, RAG pipeline, or a coding assistant), keep the same rule. Bind Ollama to localhost.
Expose only Nginx (or your app) to the network.
Common pitfalls (and quick fixes)
- Certbot fails HTTP validation: make sure DNS points to the VPS, and ports 80/443 are open in UFW and any upstream firewall.
- 502 Bad Gateway from Nginx: confirm Ollama is running and listening on localhost:
sudo systemctl status ollama --no-pager curl -s http://127.0.0.1:11434/api/tags | head sudo ss -ltnp | grep 11434 - Streaming responses look “stuck”: ensure
proxy_buffering off;is set, and increaseproxy_read_timeout. - It works locally but not remotely: check allow/deny rules, Basic Auth, and that your test machine is on the VPN/allowlisted IP.
Summary: a private LLM endpoint you can actually operate
You’ve installed Ollama under systemd, kept it bound to localhost, and exposed it only through Nginx with HTTPS and explicit access controls.
It’s intentionally boring. That means fewer open ports, fewer surprises, and logs you can find quickly.
If you want a stronger baseline server posture, pair this with HostMyCode’s web server hardening checklist (the TLS and header sections map directly onto this stack).
If your current box bogs down under model load—or you want cleaner isolation for a private AI endpoint—move it to a properly sized VPS. Start with a HostMyCode VPS, or choose managed VPS hosting if you want help with OS updates, hardening, and day-to-day reliability.
FAQ
Should I open port 11434 to the internet?
No. Keep Ollama on 127.0.0.1 and expose it only through Nginx with TLS and access controls. Public 11434 invites scanning and abuse.
Can I run Ollama on a small VPS?
You can, but keep expectations realistic. A small CPU-only VPS may handle a lightweight model for personal use or low traffic. If you need concurrency, move up in vCPU and RAM.
What’s the safest way to make the API private?
VPN-only access is the simplest strong option. Put your team and services on WireGuard, then allow only the VPN subnet in Nginx.
How do I confirm Ollama isn’t exposed directly?
Check ss -ltnp shows 127.0.0.1:11434, and test curl http://PUBLIC_IP:11434 fails. Also confirm UFW does not allow 11434.
Where do I look when requests fail?
Start with /var/log/nginx/ollama-api.error.log and journalctl -u ollama. Most issues are 502s from a stopped service, wrong bind address, or an allowlist/auth rule blocking you.