Model Deployment — FastAPI, Docker, Kubernetes
Wrap your model in a FastAPI endpoint, containerise with Docker, scale with Kubernetes. Full working deployment of the DoorDash delivery time model.
A trained model is a pkl file sitting on your laptop. Deployment means turning that pkl file into an API that handles thousands of requests per minute, survives crashes, and can be updated without downtime.
The standard production ML deployment stack at most startups is three layers. FastAPI wraps the model in an HTTP endpoint — it receives a JSON request, extracts features, runs the model, and returns a JSON prediction. Docker packages the API and all its dependencies into a container that runs identically on any machine. Kubernetes runs many containers in parallel, restarts crashed ones, and distributes incoming traffic across all of them.
DoorDash's delivery time prediction API serves 200,000 requests per minute during dinner peak hours. A single Python process handles perhaps 50 requests per second. To handle 200,000 per minute (3,333 per second) you need roughly 70 parallel processes. Kubernetes manages those 70 containers automatically — scaling up during peak hours and down at 3 AM to save compute cost. This is the deployment stack this module teaches.
A chef (your model) → a restaurant (FastAPI API) → a restaurant chain (Docker) → a restaurant franchise (Kubernetes). One chef cooking in their kitchen is a model in a notebook. Opening a restaurant adds a standardised environment, a menu (API contract), and a way for customers to order. Franchising the restaurant (Docker) means any city can run the same restaurant with the same recipe, regardless of local conditions. The franchise management company (Kubernetes) opens more locations when demand spikes and closes underperforming ones.
Docker solves "works on my machine." Kubernetes solves "stays running at scale." FastAPI solves "speaks HTTP." Together they are how every production ML model at top tech companies is served.
FastAPI — production model serving with validation, health checks, and versioning
FastAPI is the standard for Python model serving — faster than Flask, automatic request validation via Pydantic, automatic OpenAPI docs, async support, and type hints throughout. A production model API needs more than just a predict endpoint: a health check endpoint that Kubernetes uses to restart crashed pods, a readiness endpoint that signals when the model is loaded and ready, request validation that rejects malformed inputs before they reach the model, and versioned endpoints so you can deploy a new model without breaking existing clients.
Docker — package everything so it runs identically everywhere
"It works on my machine" is not acceptable in production. Docker solves this by packaging the application, its dependencies, and its runtime environment into a single image that runs identically on your laptop, on the CI server, and in production. The image is built once and deployed everywhere. A production ML Docker image has one additional concern: keeping image size small — a 10GB image takes 5 minutes to pull on a new node, causing slow cold starts.
Kubernetes — run, scale, and update containers in production
Kubernetes (K8s) manages containerised applications at scale. You tell it what you want (5 replicas of this container, restart if it crashes, distribute traffic across all replicas) and it makes it happen. Three Kubernetes objects matter most for ML serving: Deployment (define the container and how many replicas), Service (expose the deployment as a network endpoint), and HorizontalPodAutoscaler (automatically add replicas when CPU usage is high, remove them when it drops).
Rolling updates, canary releases, and blue-green deployment
DoorDash cannot take the delivery time model offline to update it. Every second of downtime means delayed delivery estimates, poor user experience, and drivers idling without assignments. Production model updates must be zero-downtime. Three patterns handle this with increasing safety.
Load testing — verify your deployment handles production traffic before it sees it
Every common deployment mistake — explained and fixed
Deployment is a hand-off between teams, not a single click
At most companies running ML in production, no single engineer owns the entire path from trained model to serving traffic. The model code and the FastAPI contract belong to the ML engineer or data scientist who built the model — they decide what the request and response schemas look like, what features the endpoint needs, and what a "healthy" prediction looks like. Everything below that — the Dockerfile, the Kubernetes manifests, the autoscaling policy, the on-call rotation that gets paged at 3 AM — is usually owned by a platform, infra, or SRE team. A dedicated "ML platform" team often sits in between, providing the paved-road tooling (a shared base image, a standard Helm chart, a deployment pipeline) so the ML engineer never has to hand-write raw Kubernetes YAML from scratch.
The actual release process at a company like DoorDash or Stripe looks less like "run a script" and more like a pipeline with checkpoints. A merged pull request triggers CI, which runs unit tests, builds the Docker image, and pushes it to a container registry. The image deploys first to a staging cluster where integration tests hit real endpoints. From there it goes to a canary — a small slice of production traffic, often in a single region or availability zone — while dashboards are watched for latency, error rate, and (for a new model version) prediction drift against the previous version. Tools like Argo Rollouts, Flagger, or Netflix's Kayenta automate this comparison and can auto-promote or auto-abort the canary based on metric thresholds, without a human manually eyeballing a dashboard for thirty minutes.
One more distinction that trips up new hires: a deployment can be perfectly healthy from an infra standpoint — no crashes, no latency spikes, all probes green — while quietly serving worse predictions. Kubernetes and its rollout tooling only know about HTTP status codes and resource usage. They have no idea whether the delivery-time model is now underestimating every order by ten minutes. That is exactly why canary analysis for an ML deployment checks prediction-level metrics (drift versus the previous version, distribution of outputs) in addition to the standard infra metrics — an ML rollout that only checks "is the pod up" is not actually a safe rollout.
Five things people get wrong about model deployment
Running a Python script that loads a model and prints a prediction is a demo, not a deployment. Production deployment means the model is behind an API contract that other services can depend on without breaking, survives a crashed process by restarting automatically, handles many concurrent requests without falling over, can be updated without taking the service offline, and exposes health signals so an orchestrator knows when it is broken. Every one of those properties is infrastructure work that has nothing to do with the model itself — it is the same work whether the model is a linear regression or a neural network.
A notebook has no latency budget, no concurrent users, and features computed leisurely from a clean, already-joined dataframe. Production has none of those luxuries: features must be fetched from a live feature store in single-digit milliseconds, malformed input has to be rejected instead of crashing the process, and the exact same preprocessing that ran during training has to run identically on a single incoming request. Training-serving skew — where the notebook's feature computation subtly differs from the API's — is one of the most common causes of a model that scored well offline and performs badly in production, and it is invisible until you actually deploy and check.
The two are different systems with different failure modes, not the same predict call on a different schedule. Batch prediction runs on a schedule over a warehouse table, has no per-request latency budget, and can retry an entire failed run overnight. Real-time serving answers a single request synchronously inside a strict latency budget, needs an online feature store for fresh values, and a single slow dependency can take down the whole request path. A model built assuming batch-style feature freshness (daily aggregates) often cannot simply be dropped into a real-time endpoint — the features it needs may not exist yet at request time with the freshness the model was trained on.
Docker guarantees the software environment is reproducible — same Python version, same pinned package versions, same OS libraries. It says nothing about the data flowing into that environment. A model can run inside the exact same container in staging and production and still produce different predictions if the feature store returns different values, if a categorical encoder sees a category it never saw during training, or if an upstream data pipeline change silently altered a feature's scale. Reproducible software is necessary but is a completely separate problem from reproducible data.
A rolling update guarantees the service stays up while pods are replaced — it says absolutely nothing about whether the new model's predictions are any good. Kubernetes has no concept of prediction quality; it only checks HTTP health endpoints and resource usage. A model that returns a plausible-looking number for every request but is systematically wrong will sail through a rolling update with every probe green. That is exactly why canary releases and shadow deployment exist as separate concerns from the rolling update mechanism — they check whether the new version's outputs are trustworthy, which is a question infrastructure tooling cannot answer on its own.
Model deployment — 5 questions interviewers actually ask
A canary release sends a small slice of real user traffic to the new version and those users receive its actual predictions — if the new version is bad, a small number of real users are affected before you catch it and roll back. A shadow deployment sends a copy of every request to the new version as well, but its prediction is only logged, never returned to the user — zero user impact, at the cost of running the new model's compute twice for every request. I would reach for shadow deployment first, before any user is exposed at all, to validate the new model on real live traffic distribution. Once shadow results look good, I would move to a canary to validate real-world business outcomes, since some effects (actual conversion, actual delivery accuracy) only show up when the model's prediction is the one acted on.
Latency is how long one request takes; throughput is how many requests the system handles per second — and improving one can hurt the other. A larger, more accurate model usually has higher per-request latency, so hitting a strict latency budget might mean choosing a smaller model, adding a result cache for repeat inputs, or batching several requests together on the server side to use hardware more efficiently — but batching trades a little latency (waiting to fill the batch) for a lot more throughput. I would start from the actual product requirement — a fraud check blocking a checkout needs sub-100-millisecond p99 latency, while a nightly recommendation batch job cares only about total throughput and can trade individual-request latency freely for it.
First, stop the bleeding: if it was a gradual rollout (canary or staged traffic shift), immediately shift traffic back to zero percent on the new version — kubectl rollout undo, or flipping the Service selector back to the previous deployment if it was blue-green, either of which should take seconds, not minutes. Second, confirm the rollback actually restored good predictions by watching the same dashboards that flagged the problem. Only after production is stable would I investigate root cause — was it a genuinely worse model, a feature pipeline bug, or a data issue upstream — because debugging under live production pressure risks making the incident worse. I would also check whether anything besides the model artifact shipped alongside it (a feature schema change, a new dependency) that also needs reverting.
A liveness probe answers "is this process alive or should it be restarted" — if it fails repeatedly, Kubernetes kills and restarts the pod. A readiness probe answers "should this pod currently receive traffic" — if it fails, the pod is pulled out of the load balancer rotation but is not restarted. ML services need both because loading a model into memory can take anywhere from a few seconds to a couple of minutes, and during that window the process is alive and healthy but not yet able to serve a correct prediction. Without a separate readiness probe, Kubernetes would either send traffic to a pod whose model has not finished loading, or the liveness probe's timeout would need to be set so generously that a genuinely hung process would take far too long to get restarted.
I would choose batch when the prediction does not need to reflect something that just happened — a weekly churn-risk score, a nightly recommendation refresh, a monthly credit-limit recalculation. Batch is simpler to operate (no always-on API, no per-request latency budget, no online feature store), cheaper per prediction at high volume, and failures can simply be retried on the next scheduled run. I would choose real-time serving when the prediction depends on something that just happened in this exact request — a fraud check at checkout, a delivery time estimate for the order being placed right now — where the value of the prediction depends entirely on it reflecting the current moment. Some systems genuinely need both: a nightly batch job scores every user for a baseline, and a real-time endpoint adjusts that score with in-session signals.
Your model is live. Next: know when it starts degrading before your users do.
Deploying a model is not the end — it is the beginning of monitoring. Models degrade silently as the world changes around them. The fraud patterns Stripe trained on in January look different by June. The delivery time patterns from pre-monsoon do not hold during monsoon season. Module 72 covers drift detection and monitoring — how to know your model is degrading before users notice, and how to trigger automatic retraining when it does.
How to know your model is degrading before users complain. Data drift, concept drift, Evidently AI, and automated retraining triggers.
🎯 Key Takeaways
- ✓The production ML deployment stack is three layers: FastAPI (wrap model in HTTP endpoint with validation, health checks, and versioning), Docker (package everything into a reproducible container), Kubernetes (run, scale, and update containers without downtime). This is the standard at DoorDash, Amazon, Stripe, and every fast-growing unicorn.
- ✓A production FastAPI model API needs four endpoints beyond /predict: /health (liveness probe — is the container alive), /ready (readiness probe — is the model loaded), /v1/predict (versioned, never break old clients), and /v1/predict/batch (batch endpoint for throughput). Always validate inputs with Pydantic before they reach the model.
- ✓Use multi-stage Docker builds to keep images small: build stage installs gcc and dependencies, runtime stage copies only the installed packages. python:3.11-slim not python:3.11. Never bake model artifacts into the image — load from S3/GCS at startup via MODEL_PATH env var. Target: under 200MB for scikit-learn models.
- ✓Kubernetes Deployment + Service + HPA is the standard serving setup. Key settings: maxUnavailable: 0 (never drop below desired replicas during update), livenessProbe initialDelaySeconds = model load time (60-120s), readinessProbe removes pod from load balancer if model is not ready, resource requests and limits prevent one pod from starving others.
- ✓Three update strategies: Rolling Update (default, simple, zero downtime, brief mixed traffic), Canary (send 5% traffic to new model, monitor, then promote — safest for ML models), Blue-Green (instant cutover by switching Service selector, instant rollback, requires 2× resources briefly). Use canary for new model versions where quality change is uncertain.
- ✓Always load-test before going live. SLO targets: p50 < 50ms, p95 < 200ms, p99 < 500ms, error rate < 0.1%, availability 99.9%. The most common deployment error is CrashLoopBackOff — check kubectl logs pod-name --previous immediately. The most dangerous is silent feature mismatch — add integration tests that compare API predictions to notebook predictions on the same input.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.