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:
- Puerto 11434 ya en uso
- Volumen corrupto
- 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:
- Ollama service no está healthy
- Network issue entre containers
- 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:
- Modelo muy grande para CPU
- No está usando GPU
num_ctxmuy alto- 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. 🎉