MLOps Guide 2026 — Deploy ML Models to Production at Scale

Sanjeev SharmaSanjeev Sharma
5 min read

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 False

Common 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading