Ollama Nginx Reverse Proxy: TLS, Authentication, Rate Limiting, and Streaming for Production

TL;DR — Ollama Nginx reverse proxy: TLS, auth, rate limiting, streaming. GetPageSpeed: "NGINX defaults silently break streaming — buffering delays tokens, HTTP/1.0 blocks chunked transfer, 60s timeout kills completions. Fix: proxy_buffering off, proxy_http_version 1.1, proxy_read_timeout 600s. Production needs TLS, auth, rate limits, block management endpoints." LocalAIOps: "Ollama: no auth, no TLS, no rate limiting. Nginx solves all three: SSL/TLS, authentication, rate limiting, access logging, WebSocket support." LinuxProfessional: "Ollama ships with zero security. Nginx: SSL, auth, rate limiting, streaming, Docker Compose stack." LocalAIMaster: "Bind to localhost, nginx + Basic Auth, certbot TLS, block port 11434, block management endpoints, limit_req per user." Glukhov: "Port 11434 is internal high-cost API. Reverse proxy: TLS, auth, timeouts, rate limits, logs. proxy_buffering off for NDJSON." Learn more with VPS setup, Docker production, REST API, and Ollama tutorial.

LinuxProfessional states the problem: "Ollama ships with exactly zero security features. No authentication, no TLS, no rate limiting — it binds to a port and serves requests to anyone who can reach it. The moment you expose Ollama to a network, you are handing every user unrestricted access to a GPU-backed inference service that can saturate your hardware in seconds."

GetPageSpeed identifies the streaming trap: "NGINX's default settings silently break LLM token streaming — buffering delays tokens, HTTP/1.0 blocks chunked transfer, and a 60-second timeout kills long completions mid-generation."

Nginx Reverse Proxy Architecture

flowchart TD subgraph Client["Client"] Browser["Browser / App
HTTPS request
https://llm.example.com"] end subgraph Nginx["Nginx Reverse Proxy"] TLS["TLS Termination
port 443
Let's Encrypt cert
TLSv1.2 + TLSv1.3
HSTS header"] Auth["Authentication
Basic Auth
htpasswd file
over TLS only
X-Ollama-User header"] Rate["Rate Limiting
limit_req_zone
30r/m per user
burst=10 nodelay
limit_conn 3"] Proxy["Proxy Layer
proxy_pass localhost:11434
proxy_buffering off
proxy_http_version 1.1
proxy_read_timeout 600s"] Block["Endpoint Blocking
/api/pull blocked
/api/delete blocked
/api/create blocked
ops only"] Log["Access Logging
who used API
when, what model
audit trail"] end subgraph Ollama["Ollama Server"] Server["localhost:11434
OLLAMA_HOST=127.0.0.1
never exposed directly
firewall blocks 11434"] end subgraph Streaming["Streaming Config"] Buffer["proxy_buffering off
NDJSON streaming
tokens flow immediately"] HTTP11["proxy_http_version 1.1
chunked transfer
HTTP/1.0 breaks streaming"] Timeout["proxy_read_timeout 600s
LLM generation takes minutes
default 60s too short"] WS["WebSocket Support
Upgrade $http_upgrade
Connection upgrade
for UI clients"] end Browser --> TLS TLS --> Auth Auth --> Rate Rate --> Proxy Proxy --> Block Block --> Log Log --> Server Proxy --> Buffer Proxy --> HTTP11 Proxy --> Timeout Proxy --> WS style Nginx fill:#4169E1,color:#fff style Ollama fill:#39FF14,color:#000 style Streaming fill:#FF6B6B,color:#fff

Configuration Comparison

Config TLS Auth Rate Limit Streaming Best For
Basic proxy No No No Off (broken) Dev only
TLS only Yes No No Off (broken) Internal network
TLS + Auth Yes Basic No Configured Small team
Full production Yes Basic Yes Configured Production
Full + blocked mgmt Yes Basic Yes Configured Enterprise

Implementation

from dataclasses import dataclass
from typing import Optional
from enum import Enum

class ProxyFeature(Enum):
    TLS = "tls"
    AUTH = "auth"
    RATE_LIMIT = "rate_limit"
    STREAMING = "streaming"
    BLOCK_MGMT = "block_management"

@dataclass
class OllamaNginxReverseProxyGuide:
    """Ollama Nginx reverse proxy implementation guide."""

    def get_basic_proxy(self) -> str:
        """Basic Nginx reverse proxy config."""
        return (
            "# /etc/nginx/sites-available/\n"
            "#   ollama\n"
            "upstream ollama_backend {\n"
            "    server 127.0.0.1:11434;\n"
            "    keepalive 32;\n"
            "}\n"
            "\n"
            "server {\n"
            "    listen 80;\n"
            "    server_name llm.\n"
            "        example.com;\n"
            "    return 301 https://\n"
            "        $host$request_uri;\n"
            "}\n"
            "\n"
            "server {\n"
            "    listen 443 ssl http2;\n"
            "    server_name llm.\n"
            "        example.com;\n"
            "\n"
            "    ssl_certificate /etc/\n"
            "        letsencrypt/live/\n"
            "        llm.example.com/\n"
            "        fullchain.pem;\n"
            "    ssl_certificate_key \\\n"
            "        /etc/letsencrypt/\n"
            "        live/llm.example.com/\n"
            "        privkey.pem;\n"
            "    ssl_protocols TLSv1.2\n"
            "        TLSv1.3;\n"
            "\n"
            "    location / {\n"
            "        proxy_pass \n"
            "            http://ollama_backend;\n"
            "        proxy_http_version 1.1;\n"
            "        proxy_set_header Host\n"
            "            localhost:11434;\n"
            "        proxy_set_header \n"
            "            Connection \"\";\n"
            "        proxy_buffering off;\n"
            "        proxy_cache off;\n"
            "        proxy_read_timeout 600s;\n"
            "        proxy_send_timeout 600s;\n"
            "    }\n"
            "}"
        )

    def get_full_production(self) -> str:
        """Full production config with auth, rate limit, blocked endpoints."""
        return (
            "# /etc/nginx/nginx.conf (http block)\n"
            "limit_req_zone $remote_user\n"
            "    zone=ollama_user:10m\n"
            "    rate=30r/m;\n"
            "limit_conn_zone $remote_user\n"
            "    zone=ollama_conn:10m;\n"
            "\n"
            "# WebSocket upgrade map\n"
            "map $http_upgrade\n"
            "    $connection_upgrade {\n"
            "    default upgrade;\n"
            "    '' close;\n"
            "}\n"
            "\n"
            "# /etc/nginx/sites-available/\n"
            "#   ollama\n"
            "upstream ollama_backend {\n"
            "    server 127.0.0.1:11434;\n"
            "    keepalive 32;\n"
            "}\n"
            "\n"
            "server {\n"
            "    listen 80;\n"
            "    server_name llm.\n"
            "        example.com;\n"
            "    return 301 https://\n"
            "        $host$request_uri;\n"
            "}\n"
            "\n"
            "server {\n"
            "    listen 443 ssl http2;\n"
            "    server_name llm.\n"
            "        example.com;\n"
            "\n"
            "    # TLS\n"
            "    ssl_certificate /etc/\n"
            "        letsencrypt/live/\n"
            "        llm.example.com/\n"
            "        fullchain.pem;\n"
            "    ssl_certificate_key \\\n"
            "        /etc/letsencrypt/\n"
            "        live/llm.example.com/\n"
            "        privkey.pem;\n"
            "    ssl_protocols TLSv1.2\n"
            "        TLSv1.3;\n"
            "    ssl_ciphers \n"
            "        ECDHE-ECDSA-\n"
            "        AES256-GCM-SHA384:\n"
            "        ECDHE-RSA-\n"
            "        AES256-GCM-SHA384;\n"
            "    ssl_prefer_server_\n"
            "        ciphers off;\n"
            "    ssl_session_cache \n"
            "        shared:SSL:10m;\n"
            "    add_header Strict-Transport-\n"
            "        Security\n"
            "        \"max-age=63072000;\n"
            "        includeSubDomains\"\n"
            "        always;\n"
            "\n"
            "    # Auth\n"
            "    auth_basic \"Ollama API\";\n"
            "    auth_basic_user_file \n"
            "        /etc/nginx/.ollama-users;\n"
            "\n"
            "    # Block management\n"
            "    #   endpoints (ops only)\n"
            "    location ~ ^/api/\n"
            "        (pull|push|create|\n"
            "         delete) {\n"
            "        allow 10.0.0.5;\n"
            "        # ops bastion IP\n"
            "        deny all;\n"
            "        proxy_pass \n"
            "            http://ollama_backend;\n"
            "        proxy_set_header Host\n"
            "            localhost:11434;\n"
            "    }\n"
            "\n"
            "    # Main proxy\n"
            "    location / {\n"
            "        # Rate limit\n"
            "        limit_req zone=\n"
            "            ollama_user\n"
            "            burst=10 nodelay;\n"
            "        limit_conn ollama_conn 3;\n"
            "\n"
            "        proxy_pass \n"
            "            http://ollama_backend;\n"
            "        proxy_http_version 1.1;\n"
            "        proxy_set_header Host\n"
            "            localhost:11434;\n"
            "        proxy_set_header \n"
            "            Upgrade\n"
            "            $http_upgrade;\n"
            "        proxy_set_header \n"
            "            Connection\n"
            "            $connection_upgrade;\n"
            "        proxy_set_header \n"
            "            X-Real-IP\n"
            "            $remote_addr;\n"
            "        proxy_set_header \n"
            "            X-Forwarded-For\n"
            "            $proxy_add_x_\n"
            "            forwarded_for;\n"
            "        proxy_set_header \n"
            "            X-Forwarded-Proto\n"
            "            $scheme;\n"
            "        proxy_set_header \n"
            "            X-Ollama-User\n"
            "            $remote_user;\n"
            "\n"
            "        # Streaming critical\n"
            "        proxy_buffering off;\n"
            "        proxy_cache off;\n"
            "\n"
            "        # Timeouts for LLM\n"
            "        proxy_connect_timeout 10s;\n"
            "        proxy_read_timeout 600s;\n"
            "        proxy_send_timeout 600s;\n"
            "\n"
            "        # Limit prompt size\n"
            "        client_max_body_size 2m;\n"
            "    }\n"
            "}"
        )

    def get_auth_setup(self) -> str:
        """Authentication setup commands."""
        return (
            "# === INSTALL TOOLS ===\n"
            "$ apt install nginx\n"
            "$ apt install apache2-utils\n"
            "\n"
            "# === CREATE AUTH FILE ===\n"
            "$ htpasswd -c /etc/nginx/\n"
            "    .ollama-users alice\n"
            "# Enter password for alice\n"
            "\n"
            "# === ADD MORE USERS ===\n"
            "$ htpasswd /etc/nginx/\n"
            "    .ollama-users bob\n"
            "$ htpasswd /etc/nginx/\n"
            "    .ollama-users charlie\n"
            "\n"
            "# === VERIFY ===\n"
            "$ cat /etc/nginx/\n"
            "    .ollama-users\n"
            "# alice:$apr1$...\n"
            "# bob:$apr1$...\n"
            "\n"
            "# === TEST AUTH ===\n"
            "$ curl -u alice:password \\\n"
            "    https://llm.example.com/\n"
            "    api/chat -d '{\n"
            "    \"model\":\"llama3.2\",\n"
            "    \"messages\":[{\"role\":\n"
            "    \"user\",\"content\":\"Hi\"}]}'\n"
            "\n"
            "# Without auth = 401:\n"
            "$ curl \\\n"
            "    https://llm.example.com/\n"
            "    api/chat\n"
            "# 401 Authorization Required"
        )

    def get_tls_setup(self) -> str:
        """TLS setup with Let's Encrypt."""
        return (
            "# === INSTALL CERTBOT ===\n"
            "$ apt install certbot \\\n"
            "    python3-certbot-nginx\n"
            "\n"
            "# === GET CERTIFICATE ===\n"
            "$ certbot --nginx -d \\\n"
            "    llm.example.com \\\n"
            "    --redirect --hsts\n"
            "\n"
            "# --redirect: HTTP to HTTPS\n"
            "# --hsts: add HSTS header\n"
            "\n"
            "# === VERIFY CERT ===\n"
            "$ certbot certificates\n"
            "\n"
            "# === AUTO RENEW ===\n"
            "$ certbot renew --dry-run\n"
            "# Cron auto-renews at 60 days\n"
            "\n"
            "# === VERIFY TLS ===\n"
            "$ curl -vI \\\n"
            "    https://llm.example.com/\n"
            "# Check TLS version, cipher\n"
            "\n"
            "# === FIREWALL ===\n"
            "$ ufw allow 80/tcp\n"
            "$ ufw allow 443/tcp\n"
            "$ ufw deny 11434/tcp\n"
            "# Block direct Ollama access"
        )

    def get_troubleshooting(self) -> dict:
        """Common issues and fixes."""
        return {
            "streaming_broken": (
                "Tokens appear all at once "
                "instead of streaming. Fix: "
                "proxy_buffering off, "
                "proxy_cache off, "
                "proxy_http_version 1.1. "
                "HTTP/1.0 doesn't support "
                "chunked transfer."
            ),
            "504_timeout": (
                "504 Gateway Timeout on "
                "long prompts. Fix: "
                "increase proxy_read_timeout "
                "to 600s+. Default 60s "
                "too short for LLM."
            ),
            "401_auth": (
                "401 Authorization Required. "
                "Check htpasswd file exists, "
                "correct path in "
                "auth_basic_user_file. "
                "Test with curl -u user:pass."
            ),
            "connection_refused": (
                "Ollama not running or "
                "wrong port. Check: "
                "systemctl status ollama, "
                "curl localhost:11434. "
                "OLLAMA_HOST=127.0.0.1."
            ),
            "ssl_error": (
                "SSL certificate error. "
                "Check cert paths, "
                "certbot certificates. "
                "Verify ssl_protocols "
                "TLSv1.2 TLSv1.3."
            ),
            "rate_limit_429": (
                "429 Too Many Requests. "
                "User hit rate limit. "
                "Increase burst or rate. "
                "Check limit_req_zone "
                "settings."
            ),
        }

Ollama Nginx Reverse Proxy Checklist

  • [ ] Ollama ships with zero security: no auth, no TLS, no rate limiting
  • [ ] Bind Ollama to localhost only: OLLAMA_HOST=127.0.0.1:11434
  • [ ] Block port 11434 at firewall: ufw deny 11434/tcp
  • [ ] Nginx reverse proxy: proxy_pass http://127.0.0.1:11434
  • [ ] proxy_buffering off — critical for NDJSON streaming (default breaks streaming)
  • [ ] proxy_cache off — no caching for streaming responses
  • [ ] proxy_http_version 1.1 — HTTP/1.0 doesn't support chunked transfer
  • [ ] proxy_set_header Connection "" — clear connection header for keepalive
  • [ ] proxy_read_timeout 600s — default 60s kills long LLM generation
  • [ ] proxy_send_timeout 600s — match read timeout
  • [ ] proxy_connect_timeout 10s — fast fail if Ollama down
  • [ ] proxy_set_header Host localhost:11434 — match Ollama docs pattern
  • [ ] proxy_set_header X-Real-IP $remote_addr — forward client IP
  • [ ] proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for
  • [ ] proxy_set_header X-Forwarded-Proto $scheme
  • [ ] WebSocket: proxy_set_header Upgrade $http_upgrade, Connection $connection_upgrade
  • [ ] TLS: certbot --nginx -d llm.example.com --redirect --hsts
  • [ ] TLS: ssl_certificate and ssl_certificate_key paths from Let's Encrypt
  • [ ] TLS: ssl_protocols TLSv1.2 TLSv1.3 — force modern TLS
  • [ ] TLS: ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
  • [ ] TLS: HSTS header max-age=63072000; includeSubDomains
  • [ ] HTTP 80 redirect to HTTPS 443
  • [ ] Auth: apt install apache2-utils, htpasswd -c /etc/nginx/.ollama-users alice
  • [ ] Auth: auth_basic "Ollama API", auth_basic_user_file /etc/nginx/.ollama-users
  • [ ] Auth: always over TLS — without HTTPS, base64 credentials trivially decoded
  • [ ] Auth: proxy_set_header X-Ollama-User $remote_user for per-user audit trail
  • [ ] Rate limiting: limit_req_zone $remote_user zone=ollama_user:10m rate=30r/m
  • [ ] Rate limiting: limit_req zone=ollama_user burst=10 nodelay in location
  • [ ] Connection limit: limit_conn_zone $remote_user zone=ollama_conn:10m
  • [ ] Connection limit: limit_conn ollama_conn 3 — max 3 concurrent per user
  • [ ] Block management endpoints: location ~ ^/api/(pull|push|create|delete) { allow ops_ip; deny all; }
  • [ ] client_max_body_size 2m — limit prompt size
  • [ ] Access logging: tail -F /var/log/nginx/ollama.access.log for audit
  • [ ] upstream ollama_backend with keepalive 32 for connection reuse
  • [ ] map $http_upgrade $connection_upgrade for WebSocket support
  • [ ] Test streaming: curl output should appear token by token, not all at once
  • [ ] Test auth: curl without credentials returns 401
  • [ ] Test TLS: curl -vI shows TLS 1.2/1.3 and valid certificate
  • [ ] Test rate limit: rapid requests return 429 after burst exceeded
  • [ ] Test management block: /api/pull returns 403 from non-ops IP
  • [ ] Troubleshoot: streaming broken → proxy_buffering off + proxy_http_version 1.1
  • [ ] Troubleshoot: 504 timeout → increase proxy_read_timeout to 600s+
  • [ ] Troubleshoot: 401 auth → check htpasswd file and path
  • [ ] Troubleshoot: connection refused → check Ollama running on localhost:11434
  • [ ] Read VPS setup for server deployment
  • [ ] Read Docker production for container deployment
  • [ ] Read REST API for endpoint reference
  • [ ] Read Ollama tutorial for basics
  • [ ] Document Nginx config, TLS cert, auth users, rate limits, blocked endpoints, firewall rules

FAQ

How do you set up Nginx as a reverse proxy for Ollama?

Configure proxy_pass to localhost:11434 with proxy_buffering off and long timeouts. GetPageSpeed: "NGINX default settings silently break LLM token streaming — buffering delays tokens, HTTP/1.0 blocks chunked transfer, 60-second timeout kills long completions. proxy_buffering off, proxy_http_version 1.1, proxy_read_timeout 600s required." LocalAIOps: "By default Ollama listens on localhost:11434 with no auth, no encryption, no rate limiting. Nginx solves all three as reverse proxy: SSL/TLS, authentication, rate limiting, access logging, WebSocket support." LinuxProfessional: "Ollama ships with zero security features. No auth, no TLS, no rate limiting. Nginx reverse proxy: SSL certificates, authentication, rate limiting, streaming support, Docker Compose stack." Setup: (1) proxy_pass http://127.0.0.1:11434. (2) proxy_buffering off for streaming. (3) proxy_http_version 1.1 for chunked transfer. (4) proxy_read_timeout 600s for long generation. (5) proxy_set_header Host, X-Real-IP, X-Forwarded-For.

How do you enable TLS for Ollama with Nginx?

Use Let's Encrypt with certbot to get free TLS certificates, configure ssl_protocols TLSv1.2 and TLSv1.3. GetPageSpeed: "Production needs TLS, forwarded client info, request size limits, caching disabled, management endpoints blocked. ssl_certificate and ssl_certificate_key paths, ssl_protocols TLSv1.2 TLSv1.3." LocalAIOps: "SSL/TLS encryption for API traffic. Certbot defaults reasonable, verify not using outdated TLS versions." LocalAIMaster: "Add TLS with certbot: certbot --nginx -d ollama.yourdomain.com --redirect --hsts. Force TLS 1.2+: ssl_protocols TLSv1.2 TLSv1.3, ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384, HSTS header max-age=63072000." TLS: (1) certbot --nginx -d ollama.example.com. (2) ssl_certificate /etc/letsencrypt/live/.../fullchain.pem. (3) ssl_protocols TLSv1.2 TLSv1.3. (4) HTTP 80 redirect to HTTPS 443. (5) HSTS header for security.

How do you add authentication to Ollama with Nginx?

Use Nginx Basic Auth with htpasswd over TLS — never send credentials without HTTPS. GetPageSpeed: "Always use Basic Auth over TLS. Without HTTPS, credentials are sent in base64 encoding, trivially decoded." LocalAIOps: "Authentication to restrict who can access the API. auth_basic and auth_basic_user_file." LocalAIMaster: "Front it with nginx + Basic Auth: apt install nginx apache2-utils, htpasswd -c /etc/nginx/.ollama_users alice. auth_basic Ollama, auth_basic_user_file /etc/nginx/.ollama_users. proxy_set_header X-Ollama-User $remote_user for per-user tracking." Auth: (1) apt install apache2-utils. (2) htpasswd -c /etc/nginx/.ollama-users alice. (3) auth_basic "Ollama API". (4) auth_basic_user_file /etc/nginx/.ollama-users. (5) Always over TLS. (6) proxy_set_header X-Ollama-User $remote_user for audit.

How do you configure rate limiting for Ollama in Nginx?

Use limit_req_zone and limit_conn_zone to prevent GPU saturation from single users. GetPageSpeed: "Nginx built-in rate limiting: limit_req zone=llm_api burst=20 nodelay." LocalAIOps: "Rate limiting prevents single user from saturating GPU with concurrent requests. Nginx rate limiting works at connection level." LocalAIMaster: "limit_req_zone $remote_user zone=ollama_user:10m rate=30r/m, limit_conn_zone $remote_user zone=ollama_conn:10m. limit_req zone=ollama_user burst=10 nodelay, limit_conn ollama_conn 3." Rate limiting: (1) Define in http block: limit_req_zone $remote_user zone=ollama:10m rate=30r/m. (2) Apply in location: limit_req zone=ollama burst=10 nodelay. (3) Connection limit: limit_conn ollama_conn 3. (4) Per-user tracking with $remote_user. (5) Burst allows short spikes, nodelay rejects immediately after burst.

Why does Ollama streaming break behind Nginx and how do you fix it?

Nginx buffers responses by default, which breaks NDJSON streaming — disable with proxy_buffering off. GetPageSpeed: "NGINX default settings silently break LLM token streaming — buffering delays tokens, HTTP/1.0 blocks chunked transfer, 60-second timeout kills long completions. Fix: proxy_buffering off, proxy_http_version 1.1, proxy_read_timeout 600s." Glukhov: "Nginx buffers proxied responses by default, opposite of what you want for NDJSON streaming. If curl output only appears at end, it is buffering. Re-check proxy_buffering off. proxy_read_timeout 3600s to prevent idle timeouts while waiting for tokens." LinuxProfessional: "proxy_buffering off and proxy_cache off for streaming. proxy_http_version 1.1 — HTTP/1.0 does not support chunked transfer. proxy_read_timeout at least 300 seconds. 504 errors on long prompts = increase timeouts." Fix: (1) proxy_buffering off. (2) proxy_cache off. (3) proxy_http_version 1.1. (4) proxy_read_timeout 600s+. (5) proxy_set_header Connection "". (6) WebSocket: Upgrade $http_upgrade headers.


Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →