MLOps Guide 2026 — Deploy ML Models to Production at Scale
Advertisement
Introduction
Why This Matters
Training a machine learning model is 10% of the work. The other 90% is getting it to production reliably, versioning it, monitoring it for drift, retraining it when performance degrades, and scaling it to handle real traffic. This is MLOps — and most ML projects fail at exactly this stage.
Teams without MLOps practices typically face model versioning chaos (which model is in production?), no data drift alerts (model silently degrades over months), no rollback capability (bad deployment = outage), and 6-week deployment cycles for model updates. MLOps solves all of these with systematic tooling.
In 2026, the MLOps stack is well-established: MLflow for experiment tracking and model registry, FastAPI for serving, Docker for containerization, and GitHub Actions for CI/CD. This guide covers each layer with production-ready code.
The MLOps Stack
Data Pipeline → Training → Evaluation → Registry → Serving → Monitoring
(dbt, Spark) (PyTorch) (Weights&Biases) (MLflow) (FastAPI) (Prometheus)Model Versioning with MLflow
import mlflow
import mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score
mlflow.set_tracking_uri("http://localhost:5000")
mlflow.set_experiment("iris-classifier")
X, y = load_iris(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
with mlflow.start_run(run_name="rf-v3") as run:
params = {"n_estimators": 100, "max_depth": 5, "random_state": 42}
model = RandomForestClassifier(**params)
model.fit(X_train, y_train)
preds = model.predict(X_test)
acc = accuracy_score(y_test, preds)
f1 = f1_score(y_test, preds, average="weighted")
mlflow.log_params(params)
mlflow.log_metrics({"accuracy": acc, "f1_score": f1})
mlflow.sklearn.log_model(model, "model", registered_model_name="iris-rf")
print(f"Run ID: {run.info.run_id}, Accuracy: {acc:.4f}")Model Serving with FastAPI
import mlflow.sklearn
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import numpy as np
app = FastAPI(title="ML Model API", version="1.0")
model_uri = "models:/iris-rf/Production"
model = mlflow.sklearn.load_model(model_uri)
CLASS_NAMES = ["setosa", "versicolor", "virginica"]
class PredictRequest(BaseModel):
sepal_length: float
sepal_width: float
petal_length: float
petal_width: float
class PredictResponse(BaseModel):
prediction: int
confidence: float
class_name: str
@app.post("/predict", response_model=PredictResponse)
async def predict(req: PredictRequest):
features = np.array([[req.sepal_length, req.sepal_width, req.petal_length, req.petal_width]])
try:
prediction = model.predict(features)[0]
proba = model.predict_proba(features)[0]
return PredictResponse(
prediction=int(prediction),
confidence=float(proba[prediction]),
class_name=CLASS_NAMES[prediction]
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health():
return {"status": "healthy", "model": "iris-rf/Production"}Containerization with Docker
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -m appuser && chown -R appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]# docker-compose.yml
version: '3.8'
services:
model-api:
build: .
ports:
- "8000:8000"
environment:
- MLFLOW_TRACKING_URI=http://mlflow:5000
depends_on:
- mlflow
mlflow:
image: ghcr.io/mlflow/mlflow
ports:
- "5000:5000"
command: mlflow server --backend-store-uri sqlite:///mlflow.db --host 0.0.0.0
volumes:
- mlflow_data:/mlflow
volumes:
mlflow_data:CI/CD Pipeline (GitHub Actions)
# .github/workflows/ml-pipeline.yml
name: ML Training and Deployment
on:
push:
paths:
- 'data/**'
- 'train.py'
jobs:
train-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Validate data
run: python validate_data.py
- name: Train model
run: python train.py
env:
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_URI }}
- name: Evaluate (fail if below baseline)
run: python evaluate.py --min-accuracy 0.90
- name: Build and push Docker image
run: |
docker build -t my-model-api:${{ github.sha }} .
docker push my-registry/my-model-api:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/model-api model-api=my-registry/my-model-api:${{ github.sha }}Data Drift and Model Monitoring
import pandas as pd
from scipy import stats
class ModelMonitor:
def __init__(self, baseline_data: pd.DataFrame):
self.baseline = baseline_data
self.alerts = []
def check_data_drift(self, new_data: pd.DataFrame, threshold: float = 0.05):
"""KS test for distribution drift on each feature."""
drifted = []
for col in self.baseline.columns:
ks_stat, p_value = stats.ks_2samp(self.baseline[col], new_data[col])
if p_value < threshold:
drifted.append({"feature": col, "p_value": p_value})
if drifted:
self.alerts.append({"type": "data_drift", "drifted_features": drifted})
return drifted
def check_prediction_drift(self, old_preds: list, new_preds: list):
old_mean = sum(old_preds) / len(old_preds)
new_mean = sum(new_preds) / len(new_preds)
if abs(new_mean - old_mean) / old_mean > 0.1:
self.alerts.append({"type": "prediction_drift", "shift": new_mean - old_mean})
return True
return FalseCommon Mistakes / Pitfalls
- No model registry — without MLflow or similar, "which version is in production" becomes unanswerable
- Skipping the evaluation gate in CI/CD — a model that scores lower than baseline should never reach production
- Not monitoring for data drift — models silently degrade when input distributions shift; KS tests catch this
- Running as root in Docker — always use a non-root user in production containers
- Retraining on demand instead of on schedule — set up automated retraining on data drift alerts
Best Practices
- Register every model training run with MLflow, including params, metrics, and artifacts
- Gate deployments on a minimum accuracy/F1 threshold — never deploy blindly
- Use the Kolmogorov-Smirnov test for numerical feature drift detection (threshold p < 0.05)
- Set up weekly model performance reports comparing current production metrics to baseline
- Implement a shadow deployment pattern — run new model in parallel before fully promoting it
Key Takeaways
- MLOps bridges the gap between ML research and production reliability, monitoring, and governance
- MLflow tracks every training run, parameter, metric, and artifact in a searchable registry
- The FastAPI + Pydantic serving pattern provides type safety and automatic API documentation
- Docker containers ensure identical behavior between development, staging, and production environments
- GitHub Actions CI/CD pipelines automate the train-evaluate-build-deploy cycle on every data change
- KS tests detect feature distribution drift before it causes silent model degradation in production
- Model serving should always expose a /health endpoint for load balancer and Kubernetes liveness checks
- Shadow deployments (running new model alongside old) are the safest way to validate production performance
Advertisement