Ollama: Project Deployment and Optimization
Deployment and optimization is SupportBot's graduation ceremony — from development to production, from functional to excellent.
⚠️ Note: Before deployment, complete the security checklist — ① requests without API Key return 401; ② rate-limited requests return 429; ③ port 11434 is not externally accessible; ④ SSL certificate is valid and HTTPS is enforced; ⑤ input/output filtering is working. Security configuration "looks correct" ≠ "actually works" — test each item with
curl.
📋 Prerequisites: You should first master the following
1. What You Will Learn
- Docker Compose production configuration: Ollama + FastAPI + Chroma + Nginx
- Performance benchmarking: TTFT / Tokens/s / concurrent capacity optimization
- Security hardening: API Key authentication, rate limiting, output filtering
- Monitoring integration: Prometheus metric collection + Grafana dashboards
- Go-live checklist and operations runbook
2. Project Background
💡 Tip: Best practice for production environment variable management — all keys and sensitive configuration should be injected via
.env files or a Secrets Manager, never hard-coded in code or Docker Compose files. Add .env to .gitignore; provide only a .env.example template in the repository.
(1) Key Pre-Deployment Issues
Alice's SupportBot is developed but faces deployment challenges:
| Issue | Risk | Solution |
|---|---|---|
| Single-machine deployment, no redundancy | Service outage | Docker Compose multi-service |
| No authentication, API exposed | Security vulnerability | Nginx + API Key |
| No monitoring blind spots | Slow fault detection | Prometheus + Grafana |
| Performance unverified | Latency exceeds target | Benchmarking + tuning |
| No operations documentation | Slow fault recovery | Operations runbook |
3. Docker Compose Production Configuration
⚠️ Warning: In production, Ollama's
OLLAMA_HOST=0.0.0.0 is only safe within the Docker internal network; port 11434 must never be directly mapped to the public internet. Nginx is the only external entry point and must be configured with API Key authentication and HTTPS.
(1) Production Architecture
flowchart TD
A[Internet<br/>:443] --> B[Nginx<br/>SSL + Auth + Rate Limit]
B --> C[FastAPI SupportBot<br/>:8000]
C --> D[Ollama<br/>:11434<br/>GPU Accelerated]
C --> E[Chroma<br/>:8001<br/>Vector Store]
F[Prometheus<br/>:9090] --> G[All Services<br/>Metrics Collection]
G --> H[Grafana<br/>:3001<br/>Dashboards]
(2) Service List
| Service | Image | Port | Resources | Persistence |
|---|---|---|---|---|
| nginx | nginx:alpine | 80/443 | 1 vCPU, 512MB | Config files |
| supportbot | custom | 8000 | 2 vCPU, 4GB | — |
| ollama | ollama/ollama | 11434 | 8 vCPU, 16GB, 1x GPU | ollama_data |
| chroma | chromadb/chroma | 8001 | 2 vCPU, 4GB | chroma_data |
| prometheus | prom/prometheus | 9090 | 1 vCPU, 2GB | prometheus_data |
| grafana | grafana/grafana | 3001 | 1 vCPU, 1GB | grafana_data |
▶ Example 1: Production Docker Compose
YAML
# docker-compose.prod.yml
version: "3.8"
services:
nginx:
image: nginx:alpine
container_name: supportbot-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/ssl/certs:ro
depends_on:
supportbot:
condition: service_healthy
restart: unless-stopped
supportbot:
build:
context: ./app
dockerfile: Dockerfile
container_name: supportbot-api
ports:
- "8000:8000"
environment:
- OLLAMA_HOST=http://ollama:11434
- CHROMA_HOST=http://chroma:8000
- API_KEY=${SUPPORTBOT_API_KEY}
- CHAT_MODEL=qwen2.5
- CLASSIFIER_MODEL=llama3.2:3b
- EMBED_MODEL=nomic-embed-text
depends_on:
ollama:
condition: service_healthy
chroma:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 15s
timeout: 5s
retries: 3
restart: unless-stopped
ollama:
image: ollama/ollama
container_name: supportbot-ollama
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
environment:
- OLLAMA_HOST=0.0.0.0:11434
- OLLAMA_NUM_PARALLEL=4
- OLLAMA_KEEP_ALIVE=30m
- OLLAMA_MAX_LOADED_MODELS=2
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:11434/api/tags"]
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped
chroma:
image: chromadb/chroma
container_name: supportbot-chroma
ports:
- "8001:8000"
volumes:
- chroma_data:/chroma/chroma
environment:
- ANONYMIZED_TELEMETRY=FALSE
restart: unless-stopped
prometheus:
image: prom/prometheus
container_name: supportbot-prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
restart: unless-stopped
grafana:
image: grafana/grafana
container_name: supportbot-grafana
ports:
- "3001:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
depends_on: [prometheus]
restart: unless-stopped
volumes:
ollama_data:
chroma_data:
prometheus_data:
grafana_data:
⚠️ Security Warning: Grafana's default admin password is
admin — you must change it immediately after first login! In production, set a strong password via the GRAFANA_PASSWORD environment variable, e.g.: GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}, and configure a randomly generated password value in the .env file.
Output:
TEXT
Docker Compose production deployment successful, all services healthy
▶ Example 2: Nginx Production Configuration
NGINX
# nginx/nginx.conf
worker_processes auto;
events {
worker_connections 1024;
}
http {
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
upstream supportbot {
server supportbot:8000;
}
# Redirect HTTP to HTTPS
server {
listen 80;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl;
server_name ai.globalshop.example.com;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/certs/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
# SupportBot API
location /v1/ {
limit_req zone=api burst=20 nodelay;
# API Key authentication
if ($http_x_api_key = "") {
return 401 '{"error": "API key required"}';
}
proxy_pass http://supportbot;
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_connect_timeout 5s;
proxy_read_timeout 30s;
}
# Health check (no auth)
location /health {
proxy_pass http://supportbot/health;
}
}
}
Output:
TEXT
// Execution successful
4. Performance Benchmarking and Optimization
💡 Tip: Always "warm up" before benchmarking — send 1-2 requests to load the model onto the GPU. The first request includes model loading time (cold start 5-30 seconds); subsequent requests reflect true inference latency.
OLLAMA_KEEP_ALIVE controls model residence time.
(1) Key Performance Targets
| Metric | Target | Test Method |
|---|---|---|
| TTFT | < 500ms | First-token latency measurement |
| P95 latency | < 3s | 100 requests, take P95 |
| Throughput | > 20 QPS | Concurrent testing |
| Auto-resolution rate | > 80% | 100 real questions evaluation |
▶ Example 3: Benchmark Script
PYTHON
# benchmark.py
import asyncio
import aiohttp
import time
import json
import statistics
import os
API_URL = "http://localhost:8000/v1/chat"
API_KEY = os.environ.get("SUPPORTBOT_API_KEY", "your-api-key-here")
TEST_QUESTIONS = [
"What is the return policy?",
"How long does shipping take?",
"Combien coûte la livraison?",
"I received a damaged item, what do I do?",
"Compare the wireless headphones models",
"Where is order #88765?",
"What is the return policy?",
"My product broke after 2 weeks, I want a refund!",
"Do you ship to Germany?",
"What is the warranty for electronics?",
]
async def single_request(session: aiohttp.ClientSession, question: str) -> dict:
start = time.time()
try:
async with session.post(
API_URL,
json={"message": question},
headers={"X-Api-Key": API_KEY},
timeout=aiohttp.ClientTimeout(total=30)
) as response:
data = await response.json()
elapsed = time.time() - start
return {
"question": question[:40],
"latency_s": round(elapsed, 2),
"intent": data.get("intent", "?"),
"handler": data.get("handler", "?"),
"language": data.get("language", "?"),
"status": "success" if response.status == 200 else "error"
}
except Exception as e:
return {"question": question[:40], "latency_s": round(time.time() - start, 2),
"status": "error", "error": str(e)}
async def run_benchmark(concurrency: int = 1, total_requests: int = 50) -> dict:
async with aiohttp.ClientSession() as session:
# Warm up
await single_request(session, "Hello")
# Run benchmark
start = time.time()
tasks = []
for i in range(total_requests):
q = TEST_QUESTIONS[i % len(TEST_QUESTIONS)]
tasks.append(single_request(session, q))
if len(tasks) >= concurrency:
results = await asyncio.gather(*tasks)
tasks = []
if tasks:
results += await asyncio.gather(*tasks)
total_time = time.time() - start
latencies = [r["latency_s"] for r in results if r["status"] == "success"]
errors = sum(1 for r in results if r["status"] == "error")
return {
"concurrency": concurrency,
"total_requests": total_requests,
"total_time_s": round(total_time, 1),
"errors": errors,
"error_rate": round(errors / total_requests * 100, 1),
"avg_latency_s": round(statistics.mean(latencies), 2) if latencies else 0,
"p50_latency_s": round(statistics.median(latencies), 2) if latencies else 0,
"p95_latency_s": round(sorted(latencies)[int(len(latencies) * 0.95)], 2) if latencies else 0,
"max_latency_s": round(max(latencies), 2) if latencies else 0,
"throughput_rps": round(total_requests / total_time, 1)
}
if __name__ == "__main__":
print("=== SupportBot Benchmark ===")
for c in [1, 4, 8]:
result = asyncio.run(run_benchmark(concurrency=c, total_requests=20))
print(f"\nConcurrency {c}:")
for k, v in result.items():
print(f" {k}: {v}")
Output:
TEXT
=== SupportBot Benchmark ===
5. Security Hardening
⚠️ Warning: Before going live, verify each security item individually — requests without API Key should return 401, rate-limited requests should return 429, port 11434 should not be externally accessible. Security configuration "looks correct" ≠ "actually works" — test with
curl.
(1) Security Checklist
| Check Item | Configuration | Status |
|---|---|---|
| API Key authentication | Nginx X-Api-Key validation | ✅ |
| HTTPS encryption | Nginx SSL configuration | ✅ |
| Rate limiting | 60 req/min/IP | ✅ |
| Input filtering | Prompt injection detection | ✅ |
| Output filtering | Sensitive content filtering | ✅ |
| Data sanitization | PII auto-removal | ✅ |
| Ollama internal network | 127.0.0.1 binding | ✅ |
▶ Example 4: Integrated Security Middleware
PYTHON
# security.py - FastAPI security middleware
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
import re
import time
from collections import defaultdict
class SecurityMiddleware:
def __init__(self, api_key: str, rate_limit: int = 60):
self.api_key = api_key
self.rate_limit = rate_limit
self.request_counts: dict = defaultdict(list)
self.injection_patterns = [
r"ignore\s+(previous|all)\s+instructions",
r"you\s+are\s+now\s+DAN",
r"show\s+(me\s+)?(your\s+)?system\s+prompt",
r"reveal\s+(your|the)\s+(initial|system)",
]
self.output_patterns = [
r"system\s*prompt[:\s]",
r"RETURN_POLICY_INTERNAL",
r"INTERNAL_PRICING",
]
def check_api_key(self, request: Request) -> bool:
key = request.headers.get("X-Api-Key", "")
return key == self.api_key
def check_rate_limit(self, client_ip: str) -> bool:
now = time.time()
self.request_counts[client_ip] = [
t for t in self.request_counts[client_ip]
if now - t < 60
]
if len(self.request_counts[client_ip]) >= self.rate_limit:
return False
self.request_counts[client_ip].append(now)
return True
def check_input(self, text: str) -> tuple[bool, str]:
for pattern in self.injection_patterns:
if re.search(pattern, text, re.IGNORECASE):
return False, "Potential prompt injection detected"
return True, ""
def check_output(self, text: str) -> tuple[bool, str]:
for pattern in self.output_patterns:
if re.search(pattern, text, re.IGNORECASE):
return False, "Sensitive content in response filtered"
return True, ""
def sanitize_pii(self, text: str) -> str:
text = re.sub(r"[\w.-]+@[\w.-]+\.\w+", "[EMAIL_REDACTED]", text)
text = re.sub(r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", "[PHONE_REDACTED]", text)
text = re.sub(r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b", "[CC_REDACTED]", text)
return text
Output:
TEXT
# Function defined successfully
6. Monitoring Integration
ℹ️ Info: GPU monitoring recommends using DCGM Exporter (NVIDIA official), which can collect GPU utilization, VRAM usage, temperature, and power consumption metrics. Combined with Grafana's GPU Dashboard template, you can quickly build a visualization panel to detect OOM and overheating risks in advance.
(1) Grafana Dashboard Metrics
| Panel | Metric | Unit | Alert |
|---|---|---|---|
| Request latency | supportbot_request_duration_seconds | s | P95 > 3s |
| Request rate | supportbot_requests_total | req/min | < 5 req/min |
| Error rate | supportbot_errors_total | % | > 5% |
| GPU utilization | DCGM_FI_DEV_GPU_UTIL | % | > 95% |
| VRAM usage | DCGM_FI_DEV_FB_USED | MB | > 95% |
| Intent distribution | supportbot_intent_count | count | — |
▶ Example 5: Prometheus Configuration
YAML
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "supportbot"
static_configs:
- targets: ["supportbot:8000"]
metrics_path: /metrics
- job_name: "ollama"
static_configs:
- targets: ["ollama:11434"]
metrics_path: /metrics
scheme: http
- job_name: "node"
static_configs:
- targets: ["node-exporter:9100"]
rule_files:
- "alert_rules.yml"
# alert_rules.yml
groups:
- name: supportbot_alerts
rules:
- alert: HighLatency
expr: histogram_quantile(0.95, rate(supportbot_request_duration_seconds_bucket[5m])) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "SupportBot P95 latency above 3s"
- alert: ServiceDown
expr: up{job="supportbot"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "SupportBot service is down"
- alert: HighErrorRate
expr: rate(supportbot_errors_total[5m]) / rate(supportbot_requests_total[5m]) > 0.05
for: 3m
labels:
severity: warning
annotations:
summary: "SupportBot error rate above 5%"
Output:
TEXT
# Prometheus configuration loaded successfully
# Target status: 3 scrape targets all UP
# supportbot (UP) | ollama (UP) | node-exporter (UP)
7. Comprehensive Example: Go-Live Checklist and Operations Runbook
💡 Tip: An operations runbook's value becomes apparent when failures occur. Conduct a quarterly "fire drill" — deliberately shut down a service and follow the runbook to recover, verifying the document's accuracy and operability, and updating outdated steps.
PYTHON
# ============================================
# Comprehensive: Production deployment checklist
# Final verification before going live
# ============================================
PRODUCTION_CHECKLIST = {
"infrastructure": {
"items": [
"Docker Compose services all running (6/6)",
"Ollama health check passing",
"Chroma health check passing",
"FastAPI health check passing",
"Nginx SSL certificate valid (not expired)",
"GPU acceleration verified (nvidia-smi inside container)",
],
"commands": [
"docker compose ps",
"curl -f http://localhost:11434/api/tags",
"curl -f http://localhost:8001/api/v1/heartbeat",
"curl -f http://localhost:8000/health",
"curl -f https://ai.globalshop.example.com/health",
]
},
"models": {
"items": [
"qwen2.5:7b pulled and verified",
"llama3.2:3b pulled and verified",
"nomic-embed-text pulled and verified",
"supportbot Modelfile created and tested",
],
"commands": [
"docker exec supportbot-ollama ollama list",
"docker exec supportbot-ollama ollama run supportbot 'Hello'",
]
},
"security": {
"items": [
"API Key authentication working (unauthorized request returns 401)",
"Rate limiting working (burst above limit returns 429)",
"Input injection filter tested",
"Output content filter tested",
"PII sanitization verified",
"Ollama not accessible from internet (port 11434 blocked)",
]
},
"performance": {
"items": [
"Single request latency < 3s (P95)",
"Concurrent 4 requests latency < 5s (P95)",
"Throughput > 10 QPS",
"RAG retrieval relevant (top-3 precision > 80%)",
"Intent classification accuracy > 85%",
]
},
"monitoring": {
"items": [
"Prometheus scraping all services",
"Grafana dashboards created and visible",
"Alert rules configured and tested",
"Log aggregation working",
]
},
"disaster_recovery": {
"items": [
"All configs in Git repository",
"Chroma data backup scheduled (daily)",
"Model files backed up on NFS/S3",
"Recovery procedure documented and tested",
]
}
}
def generate_runbook() -> str:
"""Generate operations runbook."""
runbook = [
"# SupportBot Operations Runbook\n",
"## Service Overview",
"- SupportBot API: https://ai.globalshop.example.com/v1/chat",
"- Health Check: https://ai.globalshop.example.com/health",
"- Grafana: http://localhost:3001 ⚠️ Change default admin password immediately!",
"- Prometheus: http://localhost:9090\n",
"## Common Operations\n",
"### Restart a Service",
"```bash",
"docker compose restart supportbot # Restart API",
"docker compose restart ollama # Restart Ollama",
"```\n",
"### Pull New Model",
"```bash",
"docker exec supportbot-ollama ollama pull model_name",
"```\n",
"### Rebuild Knowledge Base",
"```bash",
"docker exec supportbot-api python indexer.py /app/product_docs",
"```\n",
"### Check Logs",
"```bash",
"docker compose logs -f supportbot # API logs",
"docker compose logs -f ollama # Ollama logs",
"```\n",
"### Emergency: Service Down",
"1. Check: `docker compose ps`",
"2. Restart: `docker compose restart <service>`",
"3. Verify: `curl https://ai.globalshop.example.com/health`",
"4. If Ollama OOM: restart with `OLLAMA_NUM_PARALLEL=2`",
"5. Escalate: notify ops team\n",
"## Performance Baseline",
"| Metric | Target | Current |",
"|:-------|:-------|:--------|",
"| P95 Latency | < 3s | _fill after benchmark_ |",
"| Throughput | > 10 QPS | _fill after benchmark_ |",
"| Error Rate | < 1% | _fill after benchmark_ |",
"| GPU Util | < 85% | _fill after benchmark_ |",
]
return "\n".join(runbook)
# Generate and save
if __name__ == "__main__":
print(generate_runbook())
with open("RUNBOOK.md", "w") as f:
f.write(generate_runbook())
print("\nRunbook saved: RUNBOOK.md")
❓ FAQ
Q How long does the first startup take after deployment?
A Model pulling takes about 10-30 minutes (depending on network speed and model size). Subsequent restarts take about 30 seconds (models cached in Volume). Pull models in advance.
Q How do I obtain SSL certificates for production?
A Use certbot for free Let's Encrypt certificates, or Caddy for automatic HTTPS. Self-signed certificates work for internal networks.
Q How do I verify that security configuration is actually working?
A 1) Requests without API Key should return 401; 2) Rate-limited requests should return 429; 3) Port 11434 should not be externally accessible; 4) Injection attacks should be blocked. Test each one.
Q How long does it take to configure monitoring dashboards?
A Basic dashboards about 1 hour (import Node Exporter template + custom Ollama panel). Alert rules about 30 minutes. Total about 2 hours.
Q How do I continuously optimize SupportBot after going live?
A 1) Analyze logs for low-quality answers; 2) Add knowledge base documents; 3) Tune intent classification; 4) Expand Modelfile MESSAGE examples; 5) Regularly re-run benchmarks.
Q How do I roll back to a previous version?
A Git checkout configuration files → docker compose down → docker compose up -d. Model data in Volumes is unaffected. Chroma data can be restored from backup.
📖 Summary
- Docker Compose production configuration includes 6 services: Nginx + FastAPI + Ollama + Chroma + Prometheus + Grafana
- Performance targets: P95 < 3s, throughput > 10 QPS, auto-resolution rate > 80%
- Security hardening: API Key + HTTPS + rate limiting + input/output filtering + data sanitization
- Monitoring integration: Prometheus collection + Grafana visualization + Alertmanager alerts
- Go-live Checklist covers 6 areas: infrastructure, models, security, performance, monitoring, disaster recovery
- Operations runbook includes daily operations, troubleshooting, performance baselines, and emergency procedures
📝 Exercises
- Basic (⭐): Deploy a minimal SupportBot version using Docker Compose (Ollama + FastAPI), and verify the /health endpoint returns OK.
- Intermediate (⭐⭐): Run the benchmark script, record P95 latency, throughput, and error rate, and compare against targets. If not meeting targets, adjust OLLAMA_NUM_PARALLEL or num_ctx and re-test.
- Advanced (⭐⭐⭐): Complete the full SupportBot production deployment — including Nginx security configuration + Prometheus/Grafana monitoring + alert rules + operations runbook — and execute the complete go-live Checklist verification.