Skip to main content
Xenors Xenors
Home › Data Science › Deploy Machine Learning Models with FastAPI and Docker

Module 11: MLOps & Model DeploymentBy Ashok Kumar Yadav4 min read

Deploy Machine Learning Models with FastAPI and Docker

A model in a notebook helps nobody. Wrap it in a small web API, package it in a container, and any service can request predictions over HTTP.

FastAPI service inside a Docker container serving a machine learning model prediction endpoint
Quick answer: Save the trained model with joblib, load it once at FastAPI startup, expose a /predict endpoint with a Pydantic input schema, then build a Docker image that runs uvicorn and publish port 8000.

Train and save the model

Save the whole preprocessing and model pipeline together so training and serving cannot drift apart.

Create model.joblib
# train.py
import joblib
from sklearn.datasets import load_iris
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

X, y = load_iris(return_X_y=True)
pipe = make_pipeline(StandardScaler(), LogisticRegression(max_iter=500)).fit(X, y)
joblib.dump(pipe, "model.joblib")
print("saved, train accuracy:", round(pipe.score(X, y), 3))

Build the FastAPI service

Load the model once at startup, not on every request. Pydantic rejects invalid input automatically with a clear error.

FastAPI app with health check and prediction
# app.py
from contextlib import asynccontextmanager
import joblib
from fastapi import FastAPI
from pydantic import BaseModel, Field

state = {}

@asynccontextmanager
async def lifespan(app):
    state["model"] = joblib.load("model.joblib")
    yield
    state.clear()

app = FastAPI(title="Iris API", lifespan=lifespan)
NAMES = ["setosa", "versicolor", "virginica"]

class Flower(BaseModel):
    sepal_length: float = Field(gt=0, lt=20)
    sepal_width: float = Field(gt=0, lt=20)
    petal_length: float = Field(gt=0, lt=20)
    petal_width: float = Field(gt=0, lt=20)

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/predict")
def predict(f: Flower):
    x = [[f.sepal_length, f.sepal_width, f.petal_length, f.petal_width]]
    proba = state["model"].predict_proba(x)[0]
    i = int(proba.argmax())
    return {"class": NAMES[i], "probability": round(float(proba[i]), 4)}

Containerise with Docker

Pin dependency versions and run as a non-root user.

requirements.txt and Dockerfile
# requirements.txt
fastapi
uvicorn[standard]
scikit-learn
joblib
pydantic

# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py model.joblib ./
RUN useradd -m appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Build, run and test

Use the same scikit-learn version for training and serving, otherwise a saved model may fail to load or behave differently.

Run the container and send a request
docker build -t iris-api .
docker run -p 8000:8000 iris-api

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}'
# {"class":"setosa","probability":0.9...}

Production checklist

Add request logging, a /health endpoint for orchestrators, input limits, automated tests, and model version in every response. Run several workers behind a load balancer for traffic, and keep secrets in environment variables, not in the image.

Hands-on practice

Run the examples in the downloadable Jupyter notebook. The extra checks below focus on mistakes that appear in real projects, not just syntax.

Validate request data with Pydantic

Practice example
from pydantic import BaseModel, Field
class ChurnRequest(BaseModel):
    tenure: int = Field(ge=0)
    monthly_charges: float = Field(gt=0)
print(ChurnRequest(tenure=12, monthly_charges=79.5).model_dump())
{'tenure': 12, 'monthly_charges': 79.5}

Schema validation prevents malformed requests reaching the model.

Smoke-test a prediction endpoint

Practice example
import requests
r = requests.post('http://localhost:8000/predict', json={'tenure':12,'monthly_charges':79.5})
print(r.status_code)
print(r.json())
200
{"prediction": 0, "probability": 0.18}

A deployment is not done until an external client can call it successfully.

Self-check

  • Can you explain why the first practice result is trustworthy rather than just plausible?
  • What would you monitor or test before using this FastAPI + Docker workflow in production?
  • Which assumption in the example would break first on a larger or messier dataset?

Frequently asked questions

Why FastAPI for ML serving?

It is fast, validates input with type hints and generates interactive API docs at /docs automatically.

Why use Docker for a model?

A container bundles the exact Python version and libraries, so the model behaves the same on your laptop and on a server.

Should I load the model on every request?

No. Load it once at startup. Loading per request is slow and wastes memory.

Primary references

Use these official sources when exact syntax, defaults or definitions matter beyond the examples here.

Written by Ashok Kumar Yadav
Ashok Kumar Yadav writes Xenors tutorials on applied AI, programming and data workflows. Every lesson is built around code you can run and check yourself.
GitHub · LinkedIn

Continue exploring