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

SymptomCauseFix
502 Bad GatewayOllama not runningsudo systemctl start ollama
504 Gateway TimeoutResponse took too longIncrease proxy_read_timeout
Streaming not workingProxy buffering enabledSet proxy_buffering off
413 Request Entity Too LargeBody size limitIncrease client_max_body_size
401 on every requestPassword file permissionschmod 640, owned by root:www-data
Connection refusedOllama bound to wrong interfaceCheck 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