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:
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:
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.