Módulo 4: Ollama - Introducción

Mini-Proyecto: Chatbot en Docker con Ollama

Descripción del proyecto

Proyecto final del Módulo 4: Chatbot production-ready en Docker con Ollama, health checks, y persistent storage.

Tiempo: 60 minutos
Dificultad: Media-Alta


🎯 Objetivo

Crear stack completo:

  • ✅ Ollama en Docker container
  • ✅ Python chatbot API (FastAPI)
  • ✅ Persistent storage (modelos + conversaciones)
  • ✅ Health checks
  • ✅ Docker Compose orchestration

📁 Estructura del Proyecto

chatbot-docker/
├── docker-compose.yml
├── ollama/
│   └── Modelfile
├── api/
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── main.py
│   └── chatbot.py
└── README.md

🐳 Docker Compose Setup

docker-compose.yml:

version: '3.8'

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
      interval: 30s
      timeout: 10s
      retries: 3
    environment:
      - OLLAMA_HOST=0.0.0.0

  chatbot-api:
    build: ./api
    container_name: chatbot-api
    ports:
      - "8000:8000"
    depends_on:
      ollama:
        condition: service_healthy
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    volumes:
      - conversations_data:/app/conversations
    restart: unless-stopped

volumes:
  ollama_data:
  conversations_data:

🔧 Ollama Modelfile

ollama/Modelfile:

FROM mistral:latest

# Optimizado para producción
PARAMETER num_ctx 4096
PARAMETER temperature 0.7
PARAMETER keep_alive 3600
PARAMETER num_gpu 35

🐍 API con FastAPI

api/requirements.txt:

fastapi==0.109.0
uvicorn==0.27.0
openai==1.12.0
pydantic==2.5.0

api/Dockerfile:

FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy app code
COPY . .

# Create conversations dir
RUN mkdir -p /app/conversations

# Expose port
EXPOSE 8000

# Run API
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

api/chatbot.py:

"""Chatbot logic"""

from openai import OpenAI
import os
import json
from datetime import datetime
from pathlib import Path

class OllamaChatbot:
    """Chatbot con Ollama backend."""
    
    def __init__(self, model: str = "mistral"):
        self.client = OpenAI(
            base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + "/v1",
            api_key="ollama"
        )
        self.model = model
        self.conversations_dir = Path("/app/conversations")
        self.conversations_dir.mkdir(exist_ok=True)
    
    def chat(self, session_id: str, messages: list) -> dict:
        """Send chat and return response."""
        
        try:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                temperature=0.7,
                max_tokens=500
            )
            
            assistant_message = response.choices[0].message.content
            
            # Save conversation
            self._save_conversation(session_id, messages + [
                {"role": "assistant", "content": assistant_message}
            ])
            
            return {
                "response": assistant_message,
                "model": self.model,
                "session_id": session_id
            }
            
        except Exception as e:
            return {"error": str(e)}
    
    def _save_conversation(self, session_id: str, messages: list):
        """Save conversation to disk."""
        
        filepath = self.conversations_dir / f"{session_id}.json"
        
        data = {
            "session_id": session_id,
            "timestamp": datetime.now().isoformat(),
            "messages": messages
        }
        
        with open(filepath, "w") as f:
            json.dump(data, f, indent=2)

api/main.py:

"""FastAPI REST API"""

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
from chatbot import OllamaChatbot
import uuid

app = FastAPI(title="Ollama Chatbot API")
chatbot = OllamaChatbot()

class Message(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    session_id: str | None = None
    messages: List[Message]

class ChatResponse(BaseModel):
    response: str
    model: str
    session_id: str

@app.get("/")
def read_root():
    return {"status": "healthy", "service": "Ollama Chatbot API"}

@app.get("/health")
def health_check():
    return {"status": "ok"}

@app.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest):
    """
    Chat endpoint.
    
    Example:
    {
      "messages": [
        {"role": "user", "content": "Hola"}
      ]
    }
    """
    
    # Generate session ID if not provided
    session_id = request.session_id or str(uuid.uuid4())
    
    # Convert Pydantic models to dicts
    messages = [msg.dict() for msg in request.messages]
    
    # Get response from chatbot
    result = chatbot.chat(session_id, messages)
    
    if "error" in result:
        raise HTTPException(status_code=500, detail=result["error"])
    
    return result

@app.get("/models")
def list_models():
    """List available models."""
    return {"models": ["mistral"]}

🚀 Deployment Instructions

1. Build y start:

cd chatbot-docker

# Build images
docker-compose build

# Start stack
docker-compose up -d

# Verificar logs
docker-compose logs -f

2. Pull modelo Ollama:

docker exec ollama ollama pull mistral

3. Test API:

# Health check
curl http://localhost:8000/health

# Chat request
curl -X POST http://localhost:8000/chat \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {"role": "user", "content": "Hola, ¿cómo estás?"}
    ]
  }'

Output:

{
  "response": "¡Hola! Estoy bien, gracias. ¿En qué puedo ayudarte?",
  "model": "mistral",
  "session_id": "a1b2c3d4-..."
}

📊 Monitoring

Logs:

# Todos los servicios
docker-compose logs -f

# Solo Ollama
docker-compose logs -f ollama

# Solo API
docker-compose logs -f chatbot-api

Stats:

docker stats ollama chatbot-api

Health checks:

# Ollama
curl http://localhost:11434/api/tags

# API
curl http://localhost:8000/health

🧪 Test Script

import requests

API_URL = "http://localhost:8000"

# Test 1: Health
response = requests.get(f"{API_URL}/health")
print("Health:", response.json())

# Test 2: Simple chat
response = requests.post(
    f"{API_URL}/chat",
    json={
        "messages": [
            {"role": "user", "content": "Hola"}
        ]
    }
)
print("\nChat:", response.json())

# Test 3: Conversación con contexto
session_id = response.json()["session_id"]
response = requests.post(
    f"{API_URL}/chat",
    json={
        "session_id": session_id,
        "messages": [
            {"role": "user", "content": "Hola"},
            {"role": "assistant", "content": "Hola, ¿cómo estás?"},
            {"role": "user", "content": "¿Cuál es la capital de Francia?"}
        ]
    }
)
print("\nContextual:", response.json())

🔧 Troubleshooting

Problema 1: Ollama container no inicia

Síntoma:

Error: failed to start container: container init failed

Causas comunes:

  1. Puerto 11434 ya en uso
  2. Volumen corrupto
  3. Permisos insuficientes

Solución:

# 1. Verificar puertos
lsof -i :11434
# Si algo usa el puerto, mátalo o cambia puerto en docker-compose.yml

# 2. Limpiar volúmenes
docker-compose down -v
docker volume prune

# 3. Recrear todo
docker-compose up --build

Problema 2: GPU no detectada en container

Síntoma:

WARNING: No GPU detected, using CPU

Causa: NVIDIA Container Toolkit no instalado o mal configurado.

Solución:

# Verificar NVIDIA drivers
nvidia-smi

# Instalar NVIDIA Container Toolkit (Linux)
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
  sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

# Actualizar docker-compose.yml
services:
  ollama:
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

Verificar:

docker exec -it ollama nvidia-smi

Problema 3: API retorna "Connection refused"

Síntoma:

ConnectionError: ('Connection aborted.', ConnectionRefusedError(111, 'Connection refused'))

Causas:

  1. Ollama service no está healthy
  2. Network issue entre containers
  3. API usando URL incorrecta

Solución:

# 1. Verificar health de Ollama
docker-compose ps

# Si no está healthy, ver logs
docker-compose logs ollama

# 2. Verificar network
docker network inspect chatbot-docker_default

# 3. Test manual desde API container
docker exec -it chatbot-api curl http://ollama:11434/api/tags

# Si falla, verificar que depends_on está configurado con condition: service_healthy

Fix en docker-compose.yml:

chatbot-api:
  depends_on:
    ollama:
      condition: service_healthy  # Crítico

Problema 4: Modelo no se encuentra

Síntoma:

Error: model 'mistral:latest' not found

Causa: Modelo no está pulled en el volumen de Ollama.

Solución:

# Entrar al container de Ollama
docker exec -it ollama bash

# Listar modelos disponibles
ollama list

# Si no está, pullarlo
ollama pull mistral:latest

# Verificar
ollama list

Para pre-pull al iniciar:

Modificar Dockerfile de Ollama:

FROM ollama/ollama:latest

# Pre-pull modelos
RUN ollama pull mistral:latest && \
    ollama pull llama2:latest

Problema 5: Conversaciones no persisten

Síntoma: Después de docker-compose down, conversaciones se pierden.

Causa: Volumen no montado correctamente.

Solución:

# Verificar que volumen existe
docker volume ls | grep conversations

# Si no existe, recrear
docker-compose down
docker-compose up -d

# Verificar montaje
docker exec -it chatbot-api ls -la /app/conversations

# Si vacío, verificar permisos
docker exec -it chatbot-api chown -R 1000:1000 /app/conversations

Problema 6: Performance lento (high latency)

Síntoma: Responses toman >30s.

Causas:

  1. Modelo muy grande para CPU
  2. No está usando GPU
  3. num_ctx muy alto
  4. Cold start

Solución:

# 1. Verificar recursos
docker stats

# 2. Optimizar Modelfile
PARAMETER num_ctx 2048  # Reducir de 4096
PARAMETER num_gpu 35    # Usar GPU
PARAMETER num_thread 8  # Aumentar threads

# 3. Usar modelo más pequeño
ollama pull mistral:7b-instruct-q4_0  # Quantized

Benchmark:

import time

start = time.time()
# ... request ...
latency = time.time() - start
print(f"Latency: {latency:.2f}s")

# Target: <5s en CPU, <2s en GPU

Problema 7: Docker Compose out of memory

Síntoma:

Error: OOMKilled

Causa: Container excede límite de memoria.

Solución:

# docker-compose.yml
services:
  ollama:
    deploy:
      resources:
        limits:
          memory: 8G  # Aumentar según modelo
        reservations:
          memory: 4G

Verificar:

docker stats ollama

Problema 8: Health check falla intermitentemente

Síntoma:

Health check failed: connection timeout

Causa: Timeout muy corto o Ollama tarda en iniciar.

Solución:

ollama:
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
    interval: 30s  # Aumentar de 10s
    timeout: 10s   # Aumentar de 5s
    retries: 5     # Aumentar de 3
    start_period: 60s  # Dar más tiempo inicial

Problema 9: Windows/Mac GPU issues

Síntoma: GPU no funciona en Docker Desktop (Windows/Mac).

Causa: Docker Desktop en Windows/Mac tiene soporte limitado de GPU.

Solución:

Windows (WSL2):

# Usar WSL2 con NVIDIA support
wsl --install
# Instalar NVIDIA drivers en WSL2

Mac:

# No hay soporte nativo de GPU en Mac
# Usar CPU o deploy en cloud

Alternativa: Deploy en cloud (Vast.ai, RunPod) con GPU.


Problema 10: Logs muy verbosos

Síntoma: Logs llenan disco.

Solución:

services:
  ollama:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Verificar tamaño:

du -sh /var/lib/docker/containers/*/

✅ Checklist Pre-Deploy

Antes de considerar production-ready:

  • Health checks funcionan consistentemente
  • GPU detectada (si aplicable)
  • Volúmenes persisten data correctamente
  • Logs configurados con rotation
  • Memory limits apropiados (4-8GB)
  • Restart policy = unless-stopped
  • Test de latency <5s promedio
  • Test de 100+ requests consecutivos sin crash
  • Backup strategy para volúmenes
  • Monitoring configurado (logs accesibles)

✅ Rúbrica de Auto-Evaluación

Setup (30 pts):

  • (10) Docker Compose funciona
  • (10) Ollama container healthy
  • (10) API container healthy

Funcionalidad (40 pts):

  • (15) API responde a requests
  • (15) Conversaciones se guardan
  • (10) Health checks funcionan

Production-ready (30 pts):

  • (10) Persistent storage configurado
  • (10) Restart policy configurado
  • (10) Logs accesibles

Total: ___/100


🚀 Extensiones Opcionales

1. Nginx reverse proxy:

Añadir a docker-compose.yml:

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - chatbot-api

2. Redis cache:

  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

Cachear respuestas comunes.


3. Prometheus monitoring:

  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

✅ Resumen del Módulo 4

Lo que dominaste:

  • ✅ Ollama CLI (pull, run, list)
  • ✅ API REST local
  • ✅ Docker deployment
  • ✅ Performance tuning
  • ✅ Production-ready stack

Diferencias vs LM Studio:

  • CLI vs GUI
  • Docker support
  • Automatización
  • Production focus

➡️ Próximo Módulo

Módulo 5: OpenRouter (Multi-Provider Aggregator)

Aprenderás OpenRouter, agregador que da acceso a 100+ modelos (OpenAI, Anthropic, Google, etc.) con una sola API.

Features:

  • 100+ modelos disponibles
  • Cost optimization
  • Fallback automático
  • Switching dinámico

Tiempo: 2-3 horas


¡Felicitaciones! Completaste el Módulo 4. 🎉