Ollama: Implantação e Otimização do Projeto

Implantação e otimização é a cerimônia de formatura do SupportBot — do desenvolvimento à produção, do funcional ao excelente.

⚠️ Nota: Antes da implantação, complete o checklist de segurança — ① requisições sem API Key retornam 401; ② requisições com rate limit retornam 429; ③ porta 11434 não é externamente acessível; ④ certificado SSL é válido e HTTPS é obrigatório; ⑤ filtragem de entrada/saída está funcionando. Configuração de segurança "parece correta" ≠ "funciona de verdade" — teste cada item com curl.

📋 Pré-requisitos: Você deve dominar o seguinte primeiro

1. O Que Você Vai Aprender


2. Contexto do Projeto

💡 Dica: Melhor prática para gerenciamento de variáveis de ambiente em produção — todas as keys e configurações sensíveis devem ser injetadas via arquivos .env ou um Secrets Manager, nunca codificadas no código ou arquivos Docker Compose. Adicione .env ao .gitignore; forneça apenas um template .env.example no repositório.

(1) Questões-Chave Pré-Implantação

O SupportBot da Alice está desenvolvido mas enfrenta desafios de implantação:

Questão Risco Solução
Implantação em máquina única, sem redundância Interrupção do serviço Docker Compose multi-serviço
Sem autenticação, API exposta Vulnerabilidade de segurança Nginx + API Key
Pontos cegos de monitoramento Detecção lenta de falhas Prometheus + Grafana
Desempenho não verificado Latência excede o alvo Benchmarking + ajuste
Sem documentação de operações Recuperação lenta de falhas Runbook de operações

3. Configuração Docker Compose de Produção

⚠️ Aviso: Em produção, OLLAMA_HOST=0.0.0.0 do Ollama só é seguro dentro da rede interna Docker; a porta 11434 nunca deve ser mapeada diretamente para a internet pública. Nginx é o único ponto de entrada externo e deve ser configurado com autenticação por API Key e HTTPS.

(1) Arquitetura de Produção

100%
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) Lista de Serviços

Serviço Imagem Porta Recursos Persistência
nginx nginx:alpine 80/443 1 vCPU, 512MB Arquivos de configuração
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

▶ Exemplo 1: Docker Compose de Produção

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:
⚠️ Aviso de Segurança: A senha padrão de administrador do Grafana é adminvocê deve alterá-la imediatamente após o primeiro login! Em produção, defina uma senha forte via variável de ambiente GRAFANA_PASSWORD, ex.: GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}, e configure um valor de senha gerado aleatoriamente no arquivo .env.

Saída:

TEXT
Docker Compose production deployment successful, all services healthy

▶ Exemplo 2: Configuração Nginx de Produção

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;
        }
    }
}

Saída:

TEXT
// Execution successful

4. Benchmarking e Otimização de Desempenho

💡 Dica: Sempre "aqueça" antes do benchmarking — envie 1-2 requisições para carregar o modelo na GPU. A primeira requisição inclui tempo de carregamento do modelo (início a frio 5-30 segundos); requisições subsequentes refletem a latência real de Inferência. OLLAMA_KEEP_ALIVE controla o tempo de residência do modelo.

(1) Alvos-Chave de Desempenho

Métrica Alvo Método de Teste
TTFT < 500ms Medição de latência do primeiro token
Latência P95 < 3s 100 requisições, pegar P95
Throughput > 20 QPS Teste concorrente
Taxa de resolução automática > 80% Avaliação com 100 perguntas reais

▶ Exemplo 3: Script de Benchmark

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}")

Saída:

TEXT
=== SupportBot Benchmark ===

5. Fortalecimento de Segurança

⚠️ Aviso: Antes de entrar em produção, verifique cada item de segurança individualmente — requisições sem API Key devem retornar 401, requisições com rate limit devem retornar 429, porta 11434 não deve ser externamente acessível. Configuração de segurança "parece correta" ≠ "funciona de verdade" — teste com curl.

(1) Checklist de Segurança

Item de Verificação Configuração Status
Autenticação por API Key Validação Nginx X-Api-Key
Criptografia HTTPS Configuração Nginx SSL
Rate limiting 60 req/min/IP
Filtragem de entrada Detecção de injeção de Prompt
Filtragem de saída Filtragem de conteúdo sensível
Sanitização de dados Remoção automática de PII
Rede interna do Ollama Binding 127.0.0.1

▶ Exemplo 4: Middleware de Segurança Integrado

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

Saída:

TEXT
# Function defined successfully

6. Integração de Monitoramento

ℹ️ Info: Monitoramento de GPU recomenda usar DCGM Exporter (oficial NVIDIA), que pode coletar métricas de utilização de GPU, uso de VRAM, temperatura e consumo de energia. Combinado com o template de GPU Dashboard do Grafana, você pode rapidamente construir um painel de visualização para detectar riscos de OOM e superaquecimento com antecedência.

(1) Métricas do Dashboard Grafana

Painel Métrica Unidade Alerta
Latência de requisição supportbot_request_duration_seconds s P95 > 3s
Taxa de requisições supportbot_requests_total req/min < 5 req/min
Taxa de erro supportbot_errors_total % > 5%
Utilização de GPU DCGM_FI_DEV_GPU_UTIL % > 95%
Uso de VRAM DCGM_FI_DEV_FB_USED MB > 95%
Distribuição de intenção supportbot_intent_count count

▶ Exemplo 5: Configuração 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%"

Saída:

TEXT
# Prometheus configuration loaded successfully
# Target status: 3 scrape targets all UP
# supportbot (UP) | ollama (UP) | node-exporter (UP)

7. Exemplo Abrangente: Checklist de Entrada em Produção e Runbook de Operações

💡 Dica: O valor de um runbook de operações se torna evidente quando falhas ocorrem. Conduza um "exercício de incêndio" trimestral — deliberadamente desligue um serviço e siga o runbook para recuperar, verificando a precisão e operabilidade do documento, e atualizando etapas desatualizadas.

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")

❓ Perguntas Frequentes

P: Quanto tempo leva a primeira inicialização após a implantação? R: O download de modelos leva cerca de 10-30 minutos (dependendo da velocidade da rede e tamanho do modelo). Reinicializações subsequentes levam cerca de 30 segundos (modelos em cache no Volume). Baixe modelos com antecedência.

P: Como obtenho certificados SSL para produção? R: Use certbot para certificados Let's Encrypt gratuitos, ou Caddy para HTTPS automático. Certificados autoassinados funcionam para redes internas.

P: Como verifico que a configuração de segurança está realmente funcionando? R: 1) Requisições sem API Key devem retornar 401; 2) Requisições com rate limit devem retornar 429; 3) Porta 11434 não deve ser externamente acessível; 4) Ataques de injeção devem ser bloqueados. Teste cada um.

P: Quanto tempo leva para configurar dashboards de monitoramento? R: Dashboards básicos cerca de 1 hora (importar template Node Exporter + painel Ollama personalizado). Regras de alerta cerca de 30 minutos. Total cerca de 2 horas.

P: Como otimizar continuamente o SupportBot após entrar em produção? R: 1) Analise logs para respostas de baixa qualidade; 2) Adicione documentos à base de conhecimento; 3) Ajuste a classificação de intenção; 4) Expanda exemplos MESSAGE do Modelfile; 5) Re-execute benchmarks regularmente.

P: Como faço rollback para uma versão anterior? R: Git checkout dos arquivos de configuração → docker compose down → docker compose up -d. Dados de modelos em Volumes não são afetados. Dados Chroma podem ser restaurados de backup.


📖 Resumo


📝 Exercícios

  1. Básico (⭐): Implante uma versão mínima do SupportBot usando Docker Compose (Ollama + FastAPI), e verifique que o endpoint /health retorna OK.
  2. Intermediário (⭐⭐): Execute o script de benchmark, registre latência P95, throughput e taxa de erro, e compare com os alvos. Se não atingir os alvos, ajuste OLLAMA_NUM_PARALLEL ou num_ctx e reteste.
  3. Avançado (⭐⭐⭐): Complete a implantação de produção completa do SupportBot — incluindo configuração de segurança Nginx + monitoramento Prometheus/Grafana + regras de alerta + runbook de operações — e execute a verificação completa do Checklist de entrada em produção.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%