Ollama: Production Deployment
Production deployment is local AI's final leap from lab to battlefield — highly available, scalable, and disaster-resilient.
💡 Tip: Nginx reverse proxy + load balancing is the standard architecture for production deployment — Nginx handles SSL termination, API Key authentication, rate limiting, and traffic distribution, while Ollama nodes only need to bind to 127.0.0.1. For multi-node clusters, the
least_conn (least connections) strategy is recommended because inference request durations vary significantly.
📋 Prerequisites: You should first master the following
- Lesson 15: Docker Containerized Deployment
- Lesson 20: Security Hardening
1. What You Will Learn
- Production architecture design: Load balancing + multi-node cluster
- High availability: Health checks + automatic failover
- Scaling strategies: Queue-based horizontal scaling
- Kubernetes deployment: Helm Charts and GPU scheduling
- Disaster recovery and rollback: Model version management and configuration-as-code
2. A Real Story from a SaaS Entrepreneur
💡 Tip: In production environments, Nginx load balancing should use the
least_conn (least connections) strategy rather than the default round_robin. Ollama inference request durations vary greatly (short Q&A takes 1 second, long text takes 30 seconds), and least_conn distributes load more evenly.
ℹ️ Info: Ollama does not natively support cluster mode (no master-slave sync, no distributed inference). Model consistency and session affinity for multi-node clusters need to be implemented at the upper layer (Nginx/FastAPI). This also applies to Kubernetes deployments.
(1) The Pain Point: Single Point of Failure Causes Total Service Outage
Alice's SupportBot runs on a single Ollama server. Server maintenance or GPU OOM causes service interruption, taking down the customer service system for 2 hours and affecting 500+ customers.
(2) The Solution: Multi-Node High-Availability Cluster
Deploy a 3-node Ollama cluster + Nginx load balancer, with automatic failover if any node goes down:
flowchart TD
A[Nginx Load Balancer] --> B[Ollama Node 1<br/>GPU 1]
A --> C[Ollama Node 2<br/>GPU 2]
A --> D[Ollama Node 3<br/>GPU 3]
3. Production Architecture Design
⚠️ Warning: In a multi-node cluster, each Ollama node independently manages its models and state — there is no master-slave replication like in databases. A model pulled on Node A will not automatically sync to Node B. You must ensure model consistency across all nodes through a unified initialization script or shared storage.
(1) Single Node vs Multi-Node
| Dimension | Single Node | Multi-Node Cluster |
|---|---|---|
| Availability | Single point of failure | N-1 fault tolerance |
| Throughput | Limited | Linear scaling |
| Cost | Low | High (3x+) |
| Operations complexity | Low | Medium |
| Use Case | Dev/small scale | Production |
(2) Production Architecture Overview
flowchart TD
A[Internet] --> B[WAF / CDN]
B --> C[Nginx<br/>SSL + Auth + LB]
C --> D[Ollama Node 1<br/>GPU + Model]
C --> E[Ollama Node 2<br/>GPU + Model]
C --> F[Ollama Node 3<br/>GPU + Model]
D --> G[NFS / S3<br/>Shared Model Storage]
E --> G
F --> G
D --> H[Chroma Cluster]
E --> H
F --> H
I[Prometheus] --> D
I --> E
I --> F
I --> J[Grafana Dashboard]
(3) Component List
| Component | Count | Specifications | Purpose |
|---|---|---|---|
| Nginx LB | 1 | 2 vCPU, 4GB RAM | Load balancing + SSL |
| Ollama Node | 3 | 8 vCPU, 16GB RAM, 1x RTX 4090 | Model inference |
| Chroma | 1 | 4 vCPU, 8GB RAM, SSD | Vector storage |
| NFS/S3 | 1 | 500GB+ | Model shared storage |
| Prometheus | 1 | 2 vCPU, 4GB RAM | Monitoring |
4. High Availability
⚠️ Note: SSL certificate configuration is mandatory for production — HTTPS not only encrypts transmission but is also a prerequisite for API Key authentication (sending API Keys in plaintext over HTTP means no security at all). Use certbot for free Let's Encrypt certificates, or Caddy for automatic HTTPS.
(1) Health Checks and Failover
| Mechanism | Check Method | Interval | Timeout |
|---|---|---|---|
| Nginx passive check | Mark unavailable on request failure | Every request | 5s |
| Active health check | Periodic GET /api/tags | 10s | 3s |
| Application-layer check | Custom inference test | 30s | 10s |
▶ Example 1: Nginx Load Balancer Configuration
NGINX
# /etc/nginx/conf.d/ollama-lb.conf
upstream ollama_cluster {
least_conn; # Route to least busy node
server ollama-node1:11434 max_fails=3 fail_timeout=30s;
server ollama-node2:11434 max_fails=3 fail_timeout=30s;
server ollama-node3:11434 max_fails=3 fail_timeout=30s;
}
server {
listen 443 ssl;
server_name ai.example.com;
ssl_certificate /etc/ssl/certs/ai.example.com.crt;
ssl_certificate_key /etc/ssl/private/ai.example.com.key;
location /v1/ {
# API Key authentication
if ($http_x_api_key != "your-secret-key") {
return 401 '{"error": "Unauthorized"}';
}
proxy_pass http://ollama_cluster;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 120s;
# Rate limiting
limit_req zone=api burst=20 nodelay;
}
# Health check endpoint
location /health {
proxy_pass http://ollama_cluster/api/tags;
}
}
Output:
TEXT
// Execution successful
▶ Example 2: Automatic Failover Script
BASH
#!/bin/bash
# Ollama cluster health monitor
NODES=("ollama-node1:11434" "ollama-node2:11434" "ollama-node3:11434")
HEALTH_URL="/api/tags"
ALERT_EMAIL="ops@example.com"
check_node() {
local node=$1
if curl -sf --connect-timeout 3 "http://$node$HEALTH_URL" > /dev/null; then
echo "UP"
else
echo "DOWN"
fi
}
while true; do
for node in "${NODES[@]}"; do
status=$(check_node "$node")
if [ "$status" = "DOWN" ]; then
echo "ALERT: $node is DOWN at $(date)" >> /var/log/ollama_health.log
# Send alert
echo "Ollama node $node is DOWN" | mail -s "OLLAMA ALERT" "$ALERT_EMAIL" 2>/dev/null
fi
done
sleep 10
done
Output:
TEXT
NAME ID SIZE
llama3.2:latest a80... 2.0 GB
mistral:latest 61... 4.1 GB
5. Scaling Strategies
(1) Scaling Trigger Conditions
| Metric | Scale-Up Threshold | Scale-Down Threshold | Wait Time |
|---|---|---|---|
| Request latency P95 | > 5s | < 2s | 5 minutes |
| Concurrent connections | > 80% capacity | < 30% capacity | 5 minutes |
| GPU utilization | > 85% | < 40% | 10 minutes |
| Queue length | > 10 | = 0 | 3 minutes |
(2) Scaling Method Comparison
| Method | Speed | Cost | Complexity |
|---|---|---|---|
| Manual scaling | Slow (hours) | Controllable | Low |
| Automatic scripts | Medium (minutes) | Controllable | Medium |
| K8s HPA | Fast (seconds) | Automatic | High |
▶ Example 3: Latency-Based Auto-Scaling
PYTHON
import subprocess
import time
from typing import Optional
class AutoScaler:
def __init__(self, scale_up_threshold: float = 5.0,
scale_down_threshold: float = 2.0,
cooldown: int = 300):
self.scale_up_threshold = scale_up_threshold
self.scale_down_threshold = scale_down_threshold
self.cooldown = cooldown
self.last_scale_time = 0
def get_avg_latency(self) -> Optional[float]:
try:
result = subprocess.run(
["curl", "-sf", "http://localhost:11434/api/chat", "-d",
'{"model":"qwen2.5","messages":[{"role":"user","content":"hi"}],"stream":false}'],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
import json
data = json.loads(result.stdout)
duration_ns = data.get("total_duration", 0)
return duration_ns / 1e9
except Exception:
pass
return None
def check_and_scale(self):
latency = self.get_avg_latency()
if latency is None:
return
now = time.time()
if now - self.last_scale_time < self.cooldown:
return
if latency > self.scale_up_threshold:
print(f"High latency ({latency:.1f}s), scaling up...")
self._scale_up()
self.last_scale_time = now
elif latency < self.scale_down_threshold:
print(f"Low latency ({latency:.1f}s), scaling down...")
self._scale_down()
self.last_scale_time = now
def _scale_up(self):
# Add new Ollama node (e.g., via Docker or cloud API)
print("Adding Ollama node...")
def _scale_down(self):
# Remove idle Ollama node
print("Removing idle Ollama node...")
# Usage
# scaler = AutoScaler()
# while True:
# scaler.check_and_scale()
# time.sleep(60)
Output:
TEXT
Adding Ollama node...
Removing idle Ollama node...
6. Kubernetes Deployment
(1) K8s Deployment Architecture
flowchart TD
A[Ingress<br/>nginx-ingress] --> B[Service<br/>ollama-svc]
B --> C[Deployment<br/>ollama-deploy<br/>3 replicas]
C --> D[Pod 1<br/>GPU + Model]
C --> E[Pod 2<br/>GPU + Model]
C --> F[Pod 3<br/>GPU + Model]
G[PVC<br/>model-storage] --> D
G --> E
G --> F
(2) GPU Scheduling Key Configuration
| Resource | Configuration | Description |
|---|---|---|
| nvidia.com/gpu (requests) | Resource request | 1 GPU per Pod |
| nvidia.com/gpu (limits) | Resource limit | Maximum 1 GPU |
| nodeSelector | GPU node selection | Specify GPU nodes |
| tolerations | GPU taint toleration | Allow scheduling to GPU nodes |
▶ Example 4: Ollama K8s Deployment
YAML
# ollama-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ollama
labels:
app: ollama
spec:
replicas: 3
selector:
matchLabels:
app: ollama
template:
metadata:
labels:
app: ollama
spec:
nodeSelector:
gpu: "true"
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: ollama
image: ollama/ollama
ports:
- containerPort: 11434
resources:
requests:
nvidia.com/gpu: 1
memory: "8Gi"
limits:
nvidia.com/gpu: 1
memory: "16Gi"
env:
- name: OLLAMA_HOST
value: "0.0.0.0:11434"
- name: OLLAMA_NUM_PARALLEL
value: "4"
- name: OLLAMA_KEEP_ALIVE
value: "30m"
volumeMounts:
- name: model-storage
mountPath: /root/.ollama
livenessProbe:
httpGet:
path: /api/tags
port: 11434
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/tags
port: 11434
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: ollama-models-pvc
---
apiVersion: v1
kind: Service
metadata:
name: ollama-svc
spec:
selector:
app: ollama
ports:
- port: 11434
targetPort: 11434
type: ClusterIP
Output:
TEXT
K8s Deployment created successfully, Pods running normally
Output:
TEXT
HPA auto-scaling configured successfully, cluster resource monitoring enabled
▶ Example 5: K8s HPA Auto-Scaling
YAML
# ollama-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ollama-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ollama
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: nvidia.com/gpu
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 300
Output:
TEXT
# HPA created successfully
kubectl get hpa ollama-hpa
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
# ollama-hpa Deployment/ollama 45%/80% 2 6 3 5m
7. Disaster Recovery and Rollback
(1) Disaster Recovery Strategies
| Strategy | RTO | RPO | Cost |
|---|---|---|---|
| Active-passive | 5-15 minutes | 0 | 2x hardware |
| Active-active | 0 (automatic failover) | 0 | 3x hardware |
| Cold standby | 1-4 hours | Lossy | 1x + storage |
(2) Configuration-as-Code
| Resource | Version Management Method |
|---|---|
| Modelfile | Git repository |
| Docker Compose | Git repository |
| K8s Manifests | Git + ArgoCD |
| Nginx configuration | Git repository |
| Model versions | Ollama tags + documentation |
8. Comprehensive Example: Production Deployment Checklist
PYTHON
# ============================================
# Comprehensive: Production deployment checklist
# Full verification before going live
# ============================================
PRODUCTION_CHECKLIST = {
"infrastructure": [
("Ollama nodes: 3+ replicas running", "CRITICAL"),
("GPU drivers: installed and verified", "CRITICAL"),
("Model storage: shared NFS/S3 mounted", "CRITICAL"),
("Network: internal VLAN for Ollama traffic", "HIGH"),
("DNS: ai.example.com resolves to LB", "HIGH"),
],
"security": [
("Ollama bound to 127.0.0.1 on each node", "CRITICAL"),
("Nginx SSL certificate valid", "CRITICAL"),
("API Key authentication enabled", "CRITICAL"),
("Rate limiting configured (60 req/min)", "HIGH"),
("WAF rules in place", "MEDIUM"),
],
"high_availability": [
("Health checks configured (Nginx + app layer)", "CRITICAL"),
("Failover tested: kill 1 node, service continues", "CRITICAL"),
("Load balancer: least_conn algorithm", "HIGH"),
("Session affinity: disabled (stateless)", "HIGH"),
],
"monitoring": [
("Prometheus scraping all nodes", "HIGH"),
("Grafana dashboards created", "HIGH"),
("Alert rules: GPU OOM, high latency, service down", "CRITICAL"),
("Log aggregation: Loki or ELK", "MEDIUM"),
],
"disaster_recovery": [
("Config in Git (Modelfile, Compose, K8s)", "HIGH"),
("Model backup: GGUF files on S3/NFS", "HIGH"),
("Chroma data backup: daily snapshot", "HIGH"),
("Recovery drill: tested within last 30 days", "MEDIUM"),
],
"performance": [
("Baseline benchmark recorded", "HIGH"),
("OLLAMA_NUM_PARALLEL configured", "HIGH"),
("OLLAMA_KEEP_ALIVE=30m set", "MEDIUM"),
("num_ctx per endpoint optimized", "MEDIUM"),
]
}
def run_checklist() -> str:
report = ["# Production Deployment Checklist\n"]
total = 0
checked = 0
for category, items in PRODUCTION_CHECKLIST.items():
report.append(f"\n## {category.replace('_', ' ').title()}")
for item, priority in items:
total += 1
report.append(f"- [ ] [{priority}] {item}")
report.append(f"\n---\nTotal items: {total}")
return "\n".join(report)
print(run_checklist())
❓ FAQ
Q What is the minimum number of GPUs for a 3-node cluster?
A Minimum 3 (1 per node). If budget is limited, a 2-node setup (1 primary + 1 backup) works, but throughput is halved during failover.
Q Does every machine need to pull model files?
A If using shared storage (NFS), pull only once. If using local storage, each machine needs to pull. NFS shared + local cache is recommended.
Q Should I choose Kubernetes or Docker Compose?
A Docker Compose for < 5 nodes (simpler). Kubernetes for > 5 nodes or when auto-scaling is needed. Docker Compose is sufficient for most scenarios.
Q How do I test failover?
A Manually stop an Ollama node (docker stop or systemctl stop), verify that Nginx automatically routes traffic to other nodes with no user impact.
Q Does Ollama have a built-in cluster mode?
A No. Ollama is a single-instance service; clusters require an external load balancer combination. Each Ollama instance runs independently; Nginx handles request distribution.
Q How do I manage model versions?
A Use Modelfile + Git for configuration management. Model files use tags (qwen2.5:v1.0) for version identification. In production, use fixed tags, not :latest.
📖 Summary
- Production architecture: Nginx LB + multiple Ollama nodes + shared storage + Chroma
- High availability: Nginx least_conn + health checks + automatic failover
- Scaling: Triggered by latency/GPU utilization; Docker Compose manual or K8s HPA automatic
- Kubernetes: GPU scheduling + PVC persistence + HPA auto-scaling
- Disaster recovery: Configuration-as-code (Git) + model backup (NFS/S3) + regular drills
- Production Checklist covers 6 areas: infrastructure, security, high availability, monitoring, disaster recovery, performance
📝 Exercises
- Basic (⭐): Design a single-machine production deployment plan — Nginx + single Ollama + Chroma. Draw an architecture diagram and write a Docker Compose configuration.
- Intermediate (⭐⭐): Set up a 2-node Ollama cluster + Nginx load balancer, and test manual failover.
- Advanced (⭐⭐⭐): Write a complete production deployment document — architecture diagram, Docker Compose/K8s configuration, security hardening, monitoring and alerting, disaster recovery plan — and complete a failover drill.