Ollama: プロジェクトデプロイと最適化:インテリジェントカスタマーサービスシステム
デプロイと最適化はSupportBotの卒業式——開発から本番へ、機能から卓越へ。
⚠️ 注: デプロイ前にセキュリティチェックリストを完了すること——①API Keyなしのリクエストは401を返す;②レート制限されたリクエストは429を返す;③ポート11434は外部からアクセス不可;④SSL証明書が有効でHTTPSが強制;⑤入出力フィルタリングが動作。セキュリティ設定が「正しく見える」≠「実際に動作する」——各項目を
curlでテスト。
📋 前提条件: まず以下を習得していること
1. 学べること
- Docker Compose本番設定:Ollama + FastAPI + Chroma + Nginx
- パフォーマンスベンチマーク:TTFT / Tokens/s / 同時実行容量の最適化
- セキュリティ強化:API Key認証、レート制限、出力フィルタリング
- モニタリング統合:Prometheus指標収集 + Grafanaダッシュボード
- 本番稼働チェックリストと運用ランブック
2. プロジェクト背景
💡 ヒント: 本番環境変数管理のベストプラクティス——すべてのキーと機密設定は
.envファイルまたはシークレットマネージャー経由で注入し、コードやDocker Composeファイルにハードコードしない。.envを.gitignoreに追加;リポジトリには.env.exampleテンプレートのみ提供。
(1) デプロイ前の主要課題
AliceのSupportBotは開発完了したが、デプロイの課題に直面:
| 課題 | リスク | ソリューション |
|---|---|---|
| 単一マシンデプロイ、冗長性なし | サービス停止 | Docker Composeマルチサービス |
| 認証なし、API露出 | セキュリティ脆弱性 | Nginx + API Key |
| モニタリングの死角 | 障害検出が遅い | Prometheus + Grafana |
| パフォーマンス未検証 | レイテンシ目標超過 | ベンチマーク + チューニング |
| 運用ドキュメントなし | 障害復旧が遅い | 運用ランブック |
3. Docker Compose本番設定
⚠️ 警告: 本番ではOllamaの
OLLAMA_HOST=0.0.0.0はDocker内部ネットワーク内でのみ安全;ポート11434はインターネットに直接マッピングしてはならない。Nginxが唯一の外部エントリポイントであり、API Key認証とHTTPSを設定必須。
(1) 本番アーキテクチャ
flowchart TD
A[インターネット<br/>:443] --> B[Nginx<br/>SSL + 認証 + レート制限]
B --> C[FastAPI SupportBot<br/>:8000]
C --> D[Ollama<br/>:11434<br/>GPUアクセラレーション]
C --> E[Chroma<br/>:8001<br/>ベクトルストア]
F[Prometheus<br/>:9090] --> G[全サービス<br/>指標収集]
G --> H[Grafana<br/>:3001<br/>ダッシュボード]
(2) サービス一覧
| サービス | イメージ | ポート | リソース | 永続化 |
|---|---|---|---|---|
| nginx | nginx:alpine | 80/443 | 1 vCPU、512MB | 設定ファイル |
| supportbot | カスタム | 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 |
(3) ▶ サンプル:本番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:
⚠️ セキュリティ警告: Grafanaのデフォルト管理者パスワードは
admin——初回ログイン後に直ちに変更必須! 本番ではGRAFANA_PASSWORD環境変数で強力なパスワードを設定、例:GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}、.envファイルにランダム生成パスワード値を設定。
出力:
TEXT
Docker Compose production deployment successful, all services healthy
(4) ▶ サンプル:Nginx本番設定
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;
}
}
}
出力:
TEXT
// Execution successful
4. パフォーマンスベンチマークと最適化
💡 ヒント: ベンチマーク前に必ず「ウォームアップ」——1〜2リクエストを送信してモデルをGPUにロード。最初のリクエストにはモデルロード時間(コールドスタート5〜30秒)が含まれる;以降のリクエストが真の推論レイテンシを反映。
OLLAMA_KEEP_ALIVEがモデル常駐時間を制御。
(1) 主要パフォーマンス目標
| 指標 | 目標 | テスト方法 |
|---|---|---|
| TTFT | 500ms未満 | 最初のトークンレイテンシ測定 |
| P95レイテンシ | 3秒未満 | 100リクエスト、P95を取得 |
| スループット | 20 QPS以上 | 同時実行テスト |
| 自動解決率 | 80%以上 | 100の実際の質問で評価 |
(2) ▶ サンプル:ベンチマークスクリプト
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}")
出力:
TEXT
=== SupportBot Benchmark ===
5. セキュリティ強化
⚠️ 警告: 本番稼働前に各セキュリティ項目を個別に検証——API Keyなしのリクエストは401を返す、レート制限されたリクエストは429を返す、ポート11434は外部からアクセス不可であること。セキュリティ設定が「正しく見える」≠「実際に動作する」——
curlでテスト。
(1) セキュリティチェックリスト
| チェック項目 | 設定 | ステータス |
|---|---|---|
| API Key認証 | Nginx X-Api-Key検証 | ✅ |
| HTTPS暗号化 | Nginx SSL設定 | ✅ |
| レート制限 | 60 req/min/IP | ✅ |
| 入力フィルタリング | プロンプトインジェクション検出 | ✅ |
| 出力フィルタリング | 機密コンテンツフィルタリング | ✅ |
| データサニタイズ | PII自動除去 | ✅ |
| Ollama内部ネットワーク | 127.0.0.1バインド | ✅ |
(2) ▶ サンプル:統合セキュリティミドルウェア
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
出力:
TEXT
# Function defined successfully
6. モニタリング統合
ℹ️ 情報: GPUモニタリングにはDCGM Exporter(NVIDIA公式)の使用を推奨。GPU使用率、VRAM使用量、温度、消費電力の指標を収集可能。GrafanaのGPUダッシュボードテンプレートと組み合わせれば、可視化パネルを迅速に構築し、OOMや過熱リスクを事前に検出できる。
(1) Grafanaダッシュボード指標
| パネル | 指標 | 単位 | アラート |
|---|---|---|---|
| リクエストレイテンシ | supportbot_request_duration_seconds | 秒 | P95 > 3秒 |
| リクエスト率 | supportbot_requests_total | req/min | < 5 req/min |
| エラー率 | supportbot_errors_total | % | > 5% |
| GPU使用率 | DCGM_FI_DEV_GPU_UTIL | % | > 95% |
| VRAM使用量 | DCGM_FI_DEV_FB_USED | MB | > 95% |
| 意図分布 | supportbot_intent_count | 件 | — |
(2) ▶ サンプル:Prometheus設定
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%"
出力:
TEXT
# Prometheus configuration loaded successfully
# Target status: 3 scrape targets all UP
# supportbot (UP) | ollama (UP) | node-exporter (UP)
7. 総合サンプル:本番稼働チェックリストと運用ランブック
💡 ヒント: 運用ランブックの価値は障害発生時に発揮される。四半期ごとに「消防訓練」を実施——意図的にサービスを停止し、ランブックに従って復旧し、ドキュメントの正確性と実運用性を検証し、古い手順を更新。
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")
❓ よくある質問
Q デプロイ後の初回起動にどれくらいかかる?
A モデルのプルに約10〜30分(ネットワーク速度とモデルサイズによる)。以降の再起動は約30秒(モデルはVolumeにキャッシュ)。事前にモデルをプルしておくこと。
Q 本番のSSL証明書はどう取得する?
A 無料のLet's Encrypt証明書はcertbotで取得、またはCaddyで自動HTTPS。社内ネットワークでは自己署名証明書も可。
Q セキュリティ設定が実際に動作していることをどう確認する?
A 1)API Keyなしのリクエストは401を返す;2)レート制限されたリクエストは429を返す;3)ポート11434は外部からアクセス不可;4)インジェクション攻撃はブロックされる。1つずつテスト。
Q モニタリングダッシュボードの設定にどれくらいかかる?
A 基本ダッシュボードは約1時間(Node Exporterテンプレートインポート + カスタムOllamaパネル)。アラートルールは約30分。合計約2時間。
Q 本番稼働後のSupportBotの継続的最適化はどうする?
A 1)ログで低品質回答を分析;2)ナレッジベース文書を追加;3)意図分類をチューニング;4)ModelfileのMESSAGE例を拡充;5)定期的にベンチマークを再実行。
Q 以前のバージョンにロールバックするには?
A Git checkoutで設定ファイルを戻す → docker compose down → docker compose up -d。Volume内のモデルデータは影響なし。Chromaデータはバックアップから復元。
📖 まとめ
- Docker Compose本番設定は6サービス:Nginx + FastAPI + Ollama + Chroma + Prometheus + Grafana
- パフォーマンス目標:P95 < 3秒、スループット > 10 QPS、自動解決率 > 80%
- セキュリティ強化:API Key + HTTPS + レート制限 + 入出力フィルタリング + データサニタイズ
- モニタリング統合:Prometheus収集 + Grafana可視化 + Alertmanagerアラート
- 本番稼働チェックリストは6領域をカバー:インフラ、モデル、セキュリティ、パフォーマンス、モニタリング、災害復旧
- 運用ランブックには日常操作、トラブルシューティング、パフォーマンスベースライン、緊急手順を含む
📝 練習問題
- 基本(難易度 ⭐):Docker Composeで最小限のSupportBotバージョンをデプロイ(Ollama + FastAPI)、/healthエンドポイントがOKを返すことを確認する。
- 中級(難易度 ⭐⭐):ベンチマークスクリプトを実行し、P95レイテンシ、スループット、エラー率を記録し、目標値と比較。目標未達の場合はOLLAMA_NUM_PARALLELやnum_ctxを調整して再テスト。
- 上級(難易度 ⭐⭐⭐):完全なSupportBot本番デプロイを完了——Nginxセキュリティ設定 + Prometheus/Grafanaモニタリング + アラートルール + 運用ランブックを含め——本番稼働チェックリストの完全検証を実行。