Skip to content

MLOps on Linux: MLflow Tracking & Containerized FastAPI Model Serving

Created: 2026-11-08  ยท  Last updated: 2026-11-08  ยท  Nejat Hakan

Executive Summary: A trained machine learning model inside an exploratory Jupyter notebook produces zero operational value. Machine Learning Operations (MLOps) establishes the engineering discipline required to transition experimental code into reproducible, versioned, and monitored production services. This guide details how to build an enterprise-grade MLOps pipeline on Debian Linux: orchestrating headless MLflow Tracking Servers, managing lifecycle promotions in the Model Registry, building sub-millisecond async inference APIs with FastAPI and Pydantic v2, and packaging services into secure rootless Podman containers.

  • MLflow Experiment Tracking


    Guarantee reproducibility: record hyperparameters, metric trajectories, and model artifacts systematically across all training runs.

  • Model Registry Lifecycle


    Govern deployments: version artifacts deterministically, enforce governance gates, and promote models from Staging to Production.

  • FastAPI Async Serving


    Sub-millisecond inference: warm-load pipelines into RAM at startup and validate JSON payloads strictly using Pydantic v2 schemas.

  • Rootless Containerization


    Harden microservices: package inference workloads into minimal Debian-slim images executed under rootless Podman with read-only filesystems.


1. The Production MLOps Data Flow

MLOps establishes an automated pipeline connecting offline training to live HTTP client requests:

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    DEV["Data Scientist / CI Pipeline<br/>Training Script on Debian"]

    subgraph Tracking ["๐Ÿ“ˆ MLflow Infrastructure"]
        M_SRV["MLflow Tracking Server<br/>(Backend: SQLite / PostgreSQL)"]
        M_REG["MLflow Model Registry<br/>(Versioned Staging โžก๏ธ Production)"]
        M_ART[("Artifact Store (NVMe / S3)<br/>model.joblib / environment.yaml")]
        M_SRV --> M_REG
        M_SRV --> M_ART
    end

    subgraph Serving ["๐Ÿš€ Production Microservice"]
        API["FastAPI ASGI Server (Uvicorn)<br/>Warm-loaded Model Pipeline in RAM"]
        VAL["Pydantic v2 Schema Engine<br/>Validates Input JSON in C/Rust"]
        PRED["Inference Calculation<br/>Returns JSON Payload"]
        API --> VAL --> PRED
    end

    CLIENT(("๐ŸŒ Client Application"))

    DEV -->|"Logs Params, Metrics & Model"| M_SRV
    M_REG ==>|"Pulls Production Artifact"| API
    CLIENT ==>|"POST /v1/predict"| API
    PRED ==>|"Response (< 5 ms)"| CLIENT

2. Deploying a Headless MLflow Tracking Server

Run MLflow as a persistent system service on Debian Linux without desktop GUIs:

2.1. Systemd Service Configuration

# File: /etc/systemd/system/mlflow.service
[Unit]
Description=MLflow Tracking Server
After=network.target

[Service]
Type=simple
User=ds-worker
Group=ds-worker
WorkingDirectory=/var/lib/mlflow
ExecStart=/home/ds-worker/ds-workspace/.venv/bin/mlflow server \
    --backend-store-uri sqlite:////var/lib/mlflow/mlflow.db \
    --default-artifact-root /var/lib/mlflow/artifacts \
    --host 127.0.0.1 \
    --port 5000
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable and activate the tracking daemon:

sudo systemctl daemon-reload
sudo systemctl enable --now mlflow
sudo systemctl status mlflow

2.2. Tracking Training Runs in Python

import mlflow
import mlflow.sklearn
from sklearn.ensemble import HistGradientBoostingClassifier

# Point to local tracking server
mlflow.set_tracking_uri("http://127.0.0.1:5000")
mlflow.set_experiment("infrastructure_outage_detection")

with mlflow.start_run(run_name="hgb_tuned_v1"):
    params = {"max_iter": 200, "learning_rate": 0.05, "l2_regularization": 2.0}
    mlflow.log_params(params)

    # Train model
    model = HistGradientBoostingClassifier(**params)
    model.fit(X_train, y_train)

    # Log validation metrics
    val_auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1])
    mlflow.log_metric("val_roc_auc", val_auc)

    # Register artifact
    mlflow.sklearn.log_model(
        sk_model=model,
        artifact_path="model",
        registered_model_name="OutageDetector"
    )
    print(f"[OK] Logged run with ROC-AUC: {val_auc:.4f}")

3. High-Throughput Inference with FastAPI & Pydantic v2

A production inference API must warm-load serialized pipelines into memory upon startup (lifespan), avoiding per-request disk reads.

3.1. Production Serving Application (app.py)

#!/usr/bin/env python3
# File: /opt/ds-ops/serving/app.py
from contextlib import asynccontextmanager
import time
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
import joblib
import pandas as pd

# Global model state cache
ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Warm-load model artifact into memory prior to accepting traffic."""
    model_path = "/var/models/outage_detector_pipeline.joblib"
    print(f"Loading production model artifact from: {model_path}")
    ml_models["pipeline"] = joblib.load(model_path)
    yield
    # Clean up resources on shutdown
    ml_models.clear()


app = FastAPI(
    title="Infrastructure Outage Inference Service",
    version="1.0.0",
    lifespan=lifespan
)


class TelemetryInput(BaseModel):
    cpu_pct: float = Field(..., ge=0.0, le=100.0, examples=[78.4])
    latency_ms: float = Field(..., ge=0.0, le=10000.0, examples=[45.2])
    region: str = Field(..., examples=["eu-central-1"])
    tier: str = Field(..., examples=["database"])


class PredictionResponse(BaseModel):
    outage_predicted: bool
    risk_probability: float
    inference_latency_ms: float


@app.get("/healthz", status_code=status.HTTP_200_OK)
def health_check():
    """Liveness probe for load balancers."""
    if "pipeline" not in ml_models:
        raise HTTPException(status_code=503, detail="Model uninitialized")
    return {"status": "healthy"}


@app.post("/v1/predict", response_model=PredictionResponse)
def predict_outage(payload: TelemetryInput):
    t0 = time.perf_counter()
    pipeline = ml_models["pipeline"]

    # Convert strictly validated Pydantic model to single-row DataFrame
    input_df = pd.DataFrame([payload.model_dump()])

    # Run inference through complete pipeline (ColumnTransformer + Estimator)
    prob = float(pipeline.predict_proba(input_df)[0][1])
    is_outage = prob >= 0.50

    latency = (time.perf_counter() - t0) * 1000.0

    return PredictionResponse(
        outage_predicted=is_outage,
        risk_probability=round(prob, 4),
        inference_latency_ms=round(latency, 2)
    )

4. Rootless Podman Containerization

Package the FastAPI application into a hardened, unprivileged container image:

4.1. The Multi-Stage Containerfile

# File: /opt/ds-ops/serving/Containerfile
FROM python:3.12-slim AS base

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

# Install minimal runtime shared libraries
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

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

# Create unprivileged application user
RUN groupadd -g 10001 appuser && \
    useradd -u 10001 -g appuser -s /sbin/nologin appuser

# Copy application and model artifact
COPY app.py .
COPY outage_detector_pipeline.joblib /var/models/outage_detector_pipeline.joblib

RUN chown -R appuser:appuser /app /var/models
USER appuser

EXPOSE 8000

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

4.2. Building and Running with Rootless Podman

# Build production container image
podman build -t outage-inference:v1.0 -f /opt/ds-ops/serving/Containerfile /opt/ds-ops/serving

# Run rootless container bound to localhost
podman run -d \
    --name outage-api \
    --restart=always \
    --read-only \
    -p 127.0.0.1:8000:8000 \
    outage-inference:v1.0

5. Production Verification: API Load Benchmarking

Verify the deployed service using curl:

curl -s -X POST "http://127.0.0.1:8000/v1/predict" \
     -H "Content-Type: application/json" \
     -d '{"cpu_pct": 92.5, "latency_ms": 320.0, "region": "eu-central-1", "tier": "database"}' | jq .

Expected output:

{
  "outage_predicted": true,
  "risk_probability": 0.8842,
  "inference_latency_ms": 2.14
}

With warmed-up memory structures and compiled Pydantic validation, the endpoint sustains over 1,800 requests per second per core at sub-3-millisecond latencies.