Ollama Behind Nginx Reverse Proxy: SSL and Multi-User Setup
TL;DR
# Install Nginx and Certbot
sudo apt install nginx certbot python3-certbot-nginx
# Get SSL certificate
sudo certbot --nginx -d ollama.example.com
# Test the proxy
curl -s https://ollama.example.com/api/tags \
-u admin:password | jq .
By default, Ollama listens on localhost:11434 with no authentication, no encryption, and no rate limiting. This is fine for single-user local development but inadequate for team use or any network-exposed deployment. Nginx solves all three problems as a reverse proxy layer in front of Ollama.
Why You Need a Reverse Proxy
Ollama’s built-in HTTP server is intentionally minimal. It has no concept of users, authentication, or TLS. If you need any of the following, a reverse proxy is required:
- SSL/TLS encryption for API traffic over a network
- Authentication to restrict who can access the API
- Rate limiting to prevent a single user from monopolizing GPU resources
- Access logging for auditing who used the API and when
- Multiple services (Ollama + Open WebUI) behind one domain
- WebSocket support for streaming responses through a proxy
Prerequisites
- A Linux server running Ollama (bound to localhost)
- A domain name pointing to the server (for SSL)
- Nginx installed
- Root or sudo access
Confirm Ollama is running and bound to localhost:
curl -s http://localhost:11434/api/tags | jq .
If Ollama is not running, start it:
sudo systemctl start ollama
Basic Nginx Reverse Proxy
Step 1: Create the Nginx Configuration
sudo nano /etc/nginx/sites-available/ollama
Minimal proxy configuration:
server {
listen 80;
server_name ollama.example.com;
location / {
proxy_pass http://127.0.0.1:11434;
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;
# Required for streaming responses
proxy_buffering off;
proxy_cache off;
# LLM responses can take a while
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
Step 2: Enable the Site
sudo ln -s /etc/nginx/sites-available/ollama /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Step 3: Test
curl -s http://ollama.example.com/api/tags | jq .
Adding SSL with Let’s Encrypt
Obtain a Certificate
sudo certbot --nginx -d ollama.example.com
Certbot modifies the Nginx configuration automatically to add:
- HTTPS listener on port 443
- SSL certificate and key paths
- HTTP-to-HTTPS redirect
Verify it worked:
curl -s https://ollama.example.com/api/tags | jq .
Certificate Renewal
Certbot installs a systemd timer for automatic renewal. Verify it is active:
sudo systemctl status certbot.timer
Test renewal:
sudo certbot renew --dry-run
Adding Basic Authentication
Create a Password File
sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.ollama-auth admin
For additional users:
sudo htpasswd /etc/nginx/.ollama-auth alice
sudo htpasswd /etc/nginx/.ollama-auth bob
Set permissions:
sudo chown root:www-data /etc/nginx/.ollama-auth
sudo chmod 640 /etc/nginx/.ollama-auth
Update Nginx Configuration
server {
listen 443 ssl;
server_name ollama.example.com;
ssl_certificate /etc/letsencrypt/live/ollama.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ollama.example.com/privkey.pem;
# Basic auth
auth_basic "Ollama API";
auth_basic_user_file /etc/nginx/.ollama-auth;
location / {
proxy_pass http://127.0.0.1:11434;
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_set_header Authorization "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
server {
listen 80;
server_name ollama.example.com;
return 301 https://$host$request_uri;
}
Note the proxy_set_header Authorization "" line. This strips the basic auth header before forwarding to Ollama, which does not understand HTTP authentication and would reject requests with an Authorization header it cannot parse.
Reload and test:
sudo nginx -t && sudo systemctl reload nginx
# Without auth (should get 401)
curl -s -o /dev/null -w "%{http_code}" https://ollama.example.com/api/tags
# With auth
curl -s -u admin:yourpassword https://ollama.example.com/api/tags | jq .
Rate Limiting
Rate limiting prevents a single user from saturating the GPU with concurrent requests. Nginx rate limiting works at the connection level.
Define Rate Limit Zones
Add these directives in the http block of /etc/nginx/nginx.conf (outside the server block):
# Rate limit by authenticated user (or IP if no auth)
limit_req_zone $remote_user zone=ollama_user:10m rate=10r/m;
# Connection limit per IP
limit_conn_zone $binary_remote_addr zone=ollama_conn:10m;
Apply Rate Limits
In the server block:
location / {
# Max 10 requests per minute per user
limit_req zone=ollama_user burst=5 nodelay;
# Max 3 concurrent connections per IP
limit_conn ollama_conn 3;
# Return 429 instead of 503 when rate limited
limit_req_status 429;
limit_conn_status 429;
proxy_pass http://127.0.0.1:11434;
# ... rest of proxy settings
}
The burst=5 parameter allows short bursts of up to 5 additional requests that are processed immediately (nodelay), then enforces the 10 requests/minute limit.
Differentiated Rate Limits
For different rate limits per endpoint (e.g., allowing more requests to the lightweight /api/tags endpoint):
# Chat completions -- strict rate limiting
location /api/chat {
limit_req zone=ollama_user burst=3 nodelay;
limit_conn ollama_conn 2;
proxy_pass http://127.0.0.1:11434;
# ... proxy settings
}
# Generate -- strict rate limiting
location /api/generate {
limit_req zone=ollama_user burst=3 nodelay;
limit_conn ollama_conn 2;
proxy_pass http://127.0.0.1:11434;
# ... proxy settings
}
# Model management -- more relaxed
location /api/tags {
limit_req zone=ollama_user burst=20 nodelay;
proxy_pass http://127.0.0.1:11434;
# ... proxy settings
}
WebSocket Support for Streaming
Ollama’s /api/chat and /api/generate endpoints support streaming responses via chunked HTTP transfer encoding, not WebSockets. However, some clients (including certain Open WebUI configurations) may upgrade to WebSocket connections. Add WebSocket support to be safe:
location / {
proxy_pass http://127.0.0.1:11434;
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 600s;
proxy_send_timeout 600s;
}
The key additions are proxy_http_version 1.1, Upgrade, and Connection "upgrade".
Complete Configuration: Ollama + Open WebUI
A common deployment runs both Ollama and Open WebUI behind the same Nginx instance. Open WebUI provides a chat interface and connects to Ollama’s API internally.
# Rate limit zones (in http block)
limit_req_zone $remote_user zone=ollama_user:10m rate=10r/m;
limit_conn_zone $binary_remote_addr zone=ollama_conn:10m;
# Ollama API
server {
listen 443 ssl;
server_name ollama.example.com;
ssl_certificate /etc/letsencrypt/live/ollama.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ollama.example.com/privkey.pem;
auth_basic "Ollama API";
auth_basic_user_file /etc/nginx/.ollama-auth;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
limit_req zone=ollama_user burst=5 nodelay;
limit_conn ollama_conn 3;
proxy_pass http://127.0.0.1:11434;
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_set_header Authorization "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
# Deny access to model management endpoints for non-admins
location ~ ^/api/(pull|push|delete|create|copy) {
auth_basic "Admin Only";
auth_basic_user_file /etc/nginx/.ollama-admin;
proxy_pass http://127.0.0.1:11434;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization "";
}
access_log /var/log/nginx/ollama-access.log;
error_log /var/log/nginx/ollama-error.log;
}
# Open WebUI
server {
listen 443 ssl;
server_name chat.example.com;
ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
client_max_body_size 50M;
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_read_timeout 600s;
}
access_log /var/log/nginx/openwebui-access.log;
error_log /var/log/nginx/openwebui-error.log;
}
# HTTP to HTTPS redirects
server {
listen 80;
server_name ollama.example.com chat.example.com;
return 301 https://$host$request_uri;
}
In this configuration, Open WebUI connects to Ollama directly at http://127.0.0.1:11434 (not through the proxy), so it does not need authentication credentials. External API users authenticate through the Nginx proxy.
Open WebUI Docker Configuration
When running Open WebUI in Docker, point it at the host’s Ollama instance:
docker run -d \
--name open-webui \
--network host \
-v open-webui-data:/app/backend/data \
-e OLLAMA_BASE_URL=http://127.0.0.1:11434 \
ghcr.io/open-webui/open-webui:main
Using --network host lets the container access localhost directly. Alternatively, use Docker’s host gateway:
docker run -d \
--name open-webui \
-p 3000:8080 \
--add-host=host.docker.internal:host-gateway \
-v open-webui-data:/app/backend/data \
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
ghcr.io/open-webui/open-webui:main
Access Logging and Monitoring
Structured Logging
Add a custom log format for API analytics:
log_format ollama_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_user_agent" $request_time';
access_log /var/log/nginx/ollama-access.log ollama_log;
Monitoring Usage
Parse logs to see per-user request counts:
# Requests per user today
grep "$(date '+%d/%b/%Y')" /var/log/nginx/ollama-access.log | \
awk '{print $3}' | sort | uniq -c | sort -rn
# Slow requests (over 30 seconds)
awk '$NF > 30.0 {print}' /var/log/nginx/ollama-access.log
# Requests per endpoint
grep "$(date '+%d/%b/%Y')" /var/log/nginx/ollama-access.log | \
awk '{print $7}' | sort | uniq -c | sort -rn
Security Considerations
Bind Ollama to localhost only. By default, Ollama listens on 127.0.0.1:11434. Do not change OLLAMA_HOST to 0.0.0.0 – let Nginx handle external access. If Ollama is bound to all interfaces, anyone on the network can bypass the proxy.
Verify:
ss -tlnp | grep 11434
Expected output should show 127.0.0.1:11434, not 0.0.0.0:11434.
Firewall rules. Even with Nginx in front, ensure port 11434 is not open in your firewall:
# UFW
sudo ufw deny 11434
# iptables
sudo iptables -A INPUT -p tcp --dport 11434 -j DROP -m comment --comment "Block direct Ollama access"
Separate admin credentials. The complete configuration above uses two password files: .ollama-auth for general API access and .ollama-admin for model management endpoints (pull, push, delete, create). This prevents regular users from modifying the model library.
TLS configuration. Certbot’s defaults are reasonable, but verify you are not using outdated TLS versions:
# Test TLS configuration
curl -sI https://ollama.example.com | grep -i strict
nmap --script ssl-enum-ciphers -p 443 ollama.example.com
Request size limits. Ollama’s generate endpoint accepts arbitrarily large prompts. Set a request body limit in Nginx to prevent abuse:
client_max_body_size 10M;
This caps individual requests at 10 MB, which is more than sufficient for text prompts but prevents uploading large binary payloads.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Ollama not running | sudo systemctl start ollama |
| 504 Gateway Timeout | Response took too long | Increase proxy_read_timeout |
| Streaming not working | Proxy buffering enabled | Set proxy_buffering off |
| 413 Request Entity Too Large | Body size limit | Increase client_max_body_size |
| 401 on every request | Password file permissions | chmod 640, owned by root:www-data |
| Connection refused | Ollama bound to wrong interface | Check OLLAMA_HOST is 127.0.0.1 |
Test the full chain:
# Direct to Ollama (should work)
curl -s http://localhost:11434/api/tags | jq .
# Through proxy with auth (should work)
curl -s -u admin:password https://ollama.example.com/api/tags | jq .
# Through proxy without auth (should get 401)
curl -s -o /dev/null -w "%{http_code}" https://ollama.example.com/api/tags
