MLOps
FastAPI
Docker
scikit-learn
Python
MLOps
API REST
producción

Despliega un Modelo de Machine Learning con Sklearn, FastAPI y Docker

Crea una API REST para desplegar modelos de scikit-learn con FastAPI y Docker: del entrenamiento a un contenedor listo para producción en minutos.

19 de marzo de 2026·6 min read

Entrenar un modelo de machine learning es solo la mitad del trabajo. El otro 50% — el que muchos científicos de datos postergan — es hacer que ese modelo esté disponible para que otros sistemas lo consuman. Un notebook de Jupyter no es una API. Un archivo .pkl en tu escritorio no está en producción. Este artículo te muestra cómo cerrar esa brecha: entrenarás un clasificador de scikit-learn, lo expondrás como una API REST validada con FastAPI y empaquetarás todo en un contenedor Docker que puedes desplegar donde quieras.

Por Qué Necesitas Desplegar Modelos de ML

Cuando tu modelo vive en un notebook, el único que puede usarlo eres tú, manualmente. Para integrarlo con una aplicación web, una app móvil, un pipeline de datos o cualquier otro servicio, necesitas exponerlo a través de una interfaz estándar. Una API REST es el contrato más universal: cualquier sistema que pueda hacer una petición HTTP puede consumir tu modelo, sin importar el lenguaje en que esté escrito.

FastAPI ofrece ventajas concretas sobre alternativas como Flask para este caso de uso:

  • Validación automática de datos con Pydantic
  • Documentación interactiva generada automáticamente (Swagger UI en /docs)
  • Soporte nativo para async, útil cuando el modelo tarda en responder
  • Tipado estático que evita bugs en producción

Entrenando y Guardando el Modelo con joblib

Empezamos entrenando un clasificador de iris y guardándolo en disco. joblib es más eficiente que pickle para objetos que contienen arrays NumPy grandes (como los árboles de decisión de sklearn). Si el paso de entrenamiento es nuevo para ti, Tu Primer Modelo de Machine Learning con Scikit-learn lo explica en detalle.

python
# train_model.py
import joblib
import numpy as np
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# Cargar datos y entrenar
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.2, random_state=42
)

model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

# Verificar rendimiento
accuracy = accuracy_score(y_test, model.predict(X_test))
print(f"Accuracy en test: {accuracy:.4f}")

# Guardar modelo y metadata de clases
joblib.dump(model, "model/classifier.joblib")
joblib.dump(iris.target_names.tolist(), "model/class_names.joblib")
print("Modelo guardado en model/classifier.joblib")

Ejecuta este script primero:

bash
mkdir model
python train_model.py
# Accuracy en test: 1.0000
# Modelo guardado en model/classifier.joblib

Construyendo la API con FastAPI

Con el modelo guardado, construimos la API. La clave es definir los esquemas de entrada y salida con Pydantic: FastAPI los usa para validar automáticamente cada petición y generar la documentación.

python
# app/main.py
from contextlib import asynccontextmanager
from typing import List
import joblib
import numpy as np
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

# Esquema de entrada: las cuatro medidas de una flor iris
class IrisFeatures(BaseModel):
    sepal_length: float = Field(..., gt=0, description="Longitud del sépalo en cm")
    sepal_width:  float = Field(..., gt=0, description="Ancho del sépalo en cm")
    petal_length: float = Field(..., gt=0, description="Longitud del pétalo en cm")
    petal_width:  float = Field(..., gt=0, description="Ancho del pétalo en cm")

    model_config = {
        "json_schema_extra": {
            "example": {
                "sepal_length": 5.1,
                "sepal_width": 3.5,
                "petal_length": 1.4,
                "petal_width": 0.2
            }
        }
    }

# Esquema de salida
class PredictionResponse(BaseModel):
    prediction: int
    class_name: str
    probabilities: List[float]

# Cargar modelo al iniciar la aplicación
ml_models = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    ml_models["classifier"] = joblib.load("model/classifier.joblib")
    ml_models["class_names"] = joblib.load("model/class_names.joblib")
    yield
    ml_models.clear()

app = FastAPI(
    title="Iris Classifier API",
    description="Clasifica flores iris usando un Random Forest",
    version="1.0.0",
    lifespan=lifespan
)

@app.get("/health")
def health_check():
    return {"status": "healthy", "model_loaded": "classifier" in ml_models}

@app.post("/predict", response_model=PredictionResponse)
def predict(features: IrisFeatures):
    if "classifier" not in ml_models:
        raise HTTPException(status_code=503, detail="Modelo no disponible")

    # Preparar features como array 2D
    X = np.array([[
        features.sepal_length,
        features.sepal_width,
        features.petal_length,
        features.petal_width
    ]])

    model = ml_models["classifier"]
    prediction = int(model.predict(X)[0])
    probabilities = model.predict_proba(X)[0].tolist()
    class_name = ml_models["class_names"][prediction]

    return PredictionResponse(
        prediction=prediction,
        class_name=class_name,
        probabilities=probabilities
    )

@app.post("/predict/batch", response_model=List[PredictionResponse])
def predict_batch(features_list: List[IrisFeatures]):
    if "classifier" not in ml_models:
        raise HTTPException(status_code=503, detail="Modelo no disponible")

    X = np.array([[f.sepal_length, f.sepal_width, f.petal_length, f.petal_width]
                  for f in features_list])

    model = ml_models["classifier"]
    predictions = model.predict(X).tolist()
    probabilities = model.predict_proba(X).tolist()
    class_names = ml_models["class_names"]

    return [
        PredictionResponse(
            prediction=pred,
            class_name=class_names[pred],
            probabilities=probs
        )
        for pred, probs in zip(predictions, probabilities)
    ]

Probando la API Localmente con uvicorn

Instala las dependencias y levanta el servidor:

bash
pip install fastapi uvicorn joblib scikit-learn numpy
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

Abre http://localhost:8000/docs para la documentación interactiva. También puedes probar con curl:

bash
curl -X POST "http://localhost:8000/predict" \
  -H "Content-Type: application/json" \
  -d '{"sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2}'

Respuesta esperada:

json
{
  "prediction": 0,
  "class_name": "setosa",
  "probabilities": [0.97, 0.02, 0.01]
}

Escribiendo el Dockerfile

Para containerizar la API necesitamos un Dockerfile. Usamos python:3.12-slim para mantener la imagen pequeña:

dockerfile
# Dockerfile
FROM python:3.12-slim

# Crear usuario no-root para seguridad
RUN useradd --create-home appuser

WORKDIR /app

# Instalar dependencias primero (aprovecha caché de Docker)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copiar código y modelo
COPY app/ ./app/
COPY model/ ./model/

# Cambiar a usuario no-root
USER appuser

# Exponer puerto
EXPOSE 8000

# Comando de inicio
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

El archivo requirements.txt:

text
fastapi==0.115.6
uvicorn[standard]==0.34.0
scikit-learn==1.6.1
joblib==1.5.0
numpy==2.2.3
pydantic==2.11.3

Construyendo y Ejecutando el Contenedor Docker

bash
# Construir la imagen
docker build -t iris-classifier:v1 .

# Ejecutar el contenedor
docker run -d \
  --name iris-api \
  -p 8000:8000 \
  iris-classifier:v1

# Verificar que está corriendo
docker ps
docker logs iris-api

Probando la API Dockerizada

Con el contenedor corriendo, las mismas pruebas funcionan:

bash
# Health check
curl http://localhost:8000/health
# {"status":"healthy","model_loaded":true}

# Predicción individual
curl -X POST "http://localhost:8000/predict" \
  -H "Content-Type: application/json" \
  -d '{"sepal_length": 6.7, "sepal_width": 3.0, "petal_length": 5.2, "petal_width": 2.3}'
# {"prediction":2,"class_name":"virginica","probabilities":[0.0,0.05,0.95]}

O usando Python requests:

python
import requests

url = "http://localhost:8000/predict/batch"
payload = [
    {"sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2},
    {"sepal_length": 6.7, "sepal_width": 3.0, "petal_length": 5.2, "petal_width": 2.3},
]

response = requests.post(url, json=payload)
for result in response.json():
    print(f"{result['class_name']}: {max(result['probabilities']):.2%} confianza")
# setosa: 97.00% confianza
# virginica: 95.00% confianza

Consideraciones para Producción

El código anterior es funcional, pero un entorno de producción real requiere algunos ajustes adicionales.

CORS: si tu API es consumida desde un frontend en otro dominio, necesitas configurar CORS:

python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://tu-dominio.com"],
    allow_methods=["POST", "GET"],
    allow_headers=["*"],
)

Autenticación: para APIs no públicas, añade un header de API key:

python
import os

from fastapi import Security
from fastapi.security.api_key import APIKeyHeader

api_key_header = APIKeyHeader(name="X-API-Key")

@app.post("/predict")
def predict(features: IrisFeatures, api_key: str = Security(api_key_header)):
    if api_key != os.environ["API_KEY"]:
        raise HTTPException(status_code=403, detail="Clave inválida")
    # ... resto del handler

Escalado: corre uvicorn con múltiples workers o detrás de gunicorn (añade gunicorn a requirements.txt):

bash
CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]

Conclusión

Pasar de un modelo entrenado a una API REST containerizada toma menos de una hora siguiendo este flujo: entrenar con sklearn, guardar con joblib, desplegar con FastAPI, empaquetar con Docker. La estructura es lo suficientemente simple para iterar rápido y lo suficientemente robusta para desplegarse en cualquier orquestador de contenedores (Kubernetes, Azure Container Apps, AWS ECS). El siguiente paso es añadir un pipeline de CI/CD que reconstruya la imagen cada vez que reentrenes el modelo — un re-entrenamiento que puedes automatizar con Prefect — y vigilar el drift del modelo desplegado: Monitoreo de Modelos ML en Producción con Evidently.


Sobre el autor

Rodrigo Arenas es arquitecto de software e ingeniero de machine learning en Medellín, Colombia. Construye productos, plataformas y software open source para IA, Data Engineering y Machine Learning — es el creador de Ciaren, sklearn-genetic-opt y PyWorkforce.



Rodrigo Arenas — ML Engineer & Open Source Maintainer

¿Estás trabajando en algo similar? Puedo ayudarte a llevarlo a producción.

¿Vas a construir una plataforma de IA o de datos?

Productos, plataformas y software open source para IA, Data Engineering y Machine Learning.

Ver proyectos
Visión

Construyendo un ecosistema de productos y software open source para IA, Data Engineering y Machine Learning.

Proyectos
sklearn-genetic-optPyWorkforceCiaren

Iterixs (En construcción)

© 2026 Rodrigo Arenas