Why Your Laptop Model Doesn't Work in Production
Last time, Dev built a clean, skew-resistant training pipeline for his e-commerce company’s product-recommendation model. He’d caught a silent unit mismatch: session durations measured in minutes during training but seconds in the upstream event system. Left unfixed, that gap would have dropped accuracy from a perfect 1.00 to no better than a coin flip. He wired guardrails around it — features defined once in shared code, input validation at every gate, a drift monitor comparing training and serving statistics. The pipeline was clean and reproducible. But a pipeline that works in a notebook isn’t a model serving ten million users.
Dev’s recommender was trained and validated. He packaged the model artifact and sent the .ipynb to the platform engineering team for deployment. Five minutes later, a Slack message arrived: ModuleNotFoundError: No module named 'joblib'.
What happened? Dev’s laptop is a unique ecosystem. Specific Python version, specific libraries, specific operating system. Move that code to the engineering team’s server and the ecosystem changes. The model breaks. This is the classic “works on my machine” problem.
Here’s what happens when you run a simple model script without the right setup:
# predict.py
import joblib
import numpy as np
# Imagine this was trained in a notebook
def make_prediction(data):
# This will fail if joblib isn't installed in the new environment
model = joblib.load('my_model.pkl')
return model.predict(data)
print("Model loaded successfully!")
This is the exact script Dev sent to the engineering team — and the exact script that failed on their server.
import joblib— joblib is the library sklearn uses to serialize (save) trained models to disk. It’s not part of Python’s standard library, so if the server doesn’t have it installed, this line throwsModuleNotFoundErrorbefore the script even reaches the model.import numpy as np— NumPy is the array library that sklearn models depend on internally. Same problem: if the server has a different NumPy version (or none at all), the model’s internal operations may break or produce different results.joblib.load('my_model.pkl')— deserializes the saved model from a binary file. The.pklfile was created on Dev’s laptop with whatever version of joblib and sklearn he had installed. If the server has a different version of either, the load can fail silently or produce a model that behaves differently.model.predict(data)— the standard sklearn prediction call. Even ifloadsucceeds,predictmay crash if the NumPy version on the server handles array internals differently from Dev’s laptop.
The print("Model loaded successfully!") line at the bottom? That never ran. The script crashed on import joblib — the very first line.
If your colleague doesn’t have joblib installed, the script crashes immediately. If they have a different version instead — say 0.14 versus your 1.2 — the failure can be quieter: the load may succeed but behave differently, or fail with a less obvious error. Containers fix this by packaging your code and its entire environment into one box. Docker is the industry standard for building them.
What a Container Actually Is (Without the Jargon)
A Docker container works much like a physical shipping container. Put it on a ship, a train, or a truck — the contents inside stay the same.
In software, a container is a lightweight package. It holds your code, the libraries you depend on (pandas, scikit-learn), and a mini operating system to run them.
- The Image: Your blueprint, like a class in Python. A file that says “Install Python 3.11, copy this folder, and run this command.”
- The Container: The running instance, like an object. When you “run” an image, it becomes a container.
Containers are isolated. Your container needs Python 3.11, your laptop has Python 3.9 — no conflict. They live in separate worlds.
Your First Dockerfile: The Recipe
To build an image, you need a Dockerfile — a text file with instructions. Think of it as a recipe.
# This is a text file named 'Dockerfile' (no extension)
# 1. Start with a base ingredient (Python 3.11)
FROM python:3.11-slim
# 2. Set the working directory inside the container
WORKDIR /app
# 3. Copy our requirements list
COPY requirements.txt .
# 4. Install the libraries
RUN pip install --no-cache-dir -r requirements.txt
# 5. Copy the rest of our code
COPY . .
# 6. Tell the container to run our app
CMD ["python", "app.py"]
This is a Dockerfile — a plain-text file (no .py extension) that Docker reads top-to-bottom to build an image. Each instruction creates a layer, and Docker caches layers so rebuilds are fast.
FROM python:3.11-slim— the base image. This pulls a pre-built Linux image with Python 3.11 already installed. The-slimvariant strips out compilers and system tools that aren’t needed at runtime, shrinking the image by ~600 MB. Dev’s model only needs Python and pip, so slim is the right starting point.WORKDIR /app— sets the working directory inside the container. All subsequentCOPYandRUNcommands operate relative to/app. If the directory doesn’t exist, Docker creates it.COPY requirements.txt .— copies only the requirements file from Dev’s laptop into the container’s/appdirectory. The.means “the current working directory inside the container” (which is/appbecause ofWORKDIR). Copying just the requirements file before the rest of the code is deliberate — it enables layer caching (see the next instruction).RUN pip install --no-cache-dir -r requirements.txt— installs all Python dependencies inside the container.--no-cache-dirtells pip not to store downloaded packages in a cache, which keeps the image smaller. This is the slowest step in the build, so it’s placed beforeCOPY . .— if Dev changes his code but not his dependencies, Docker reuses this cached layer and skips the pip install entirely.COPY . .— copies everything else from Dev’s laptop (the app code, the model file) into the container. Because this comes after theRUN pip installlayer, changingapp.pyormodel.pkldoesn’t invalidate the pip cache — only changingrequirements.txtdoes.CMD ["python", "app.py"]— the command Docker runs when the container starts. The JSON-array syntax (["python", "app.py"]) is the “exec form” — it runspythondirectly as PID 1, which handles signals (likeCtrl+C) correctly. The alternative shell form (CMD python app.py) wraps it in a shell and can cause signal issues.
Each line is a “layer.” If you change your code but not your libraries, Docker reuses the RUN pip install layer. That keeps builds fast.
Building a Simple FastAPI Endpoint for Your Model
A model in a script is hard for anyone else to use. Wrap it in an API, and suddenly other programs can reach it over the web. FastAPI handles the HTTP plumbing for you.
Here’s how to wrap a model in an API:
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import numpy as np
app = FastAPI()
# Define what the input data looks like
class InputData(BaseModel):
feature_1: float
feature_2: float
# Load the model (we'll do this properly in the next section)
# For now, imagine a simple dummy model
@app.post("/predict")
def predict(data: InputData):
# Convert JSON input to a list for the model
features = np.array([[data.feature_1, data.feature_2]])
# In a real app, we'd call model.predict(features)
prediction = float(features.sum())
return {"prediction": prediction}
This block turns a Python function into a web endpoint that accepts JSON and returns JSON — the bridge between “a model in a script” and “a model that other services can call.”
from fastapi import FastAPI— imports the web framework. FastAPI is built on top of Starlette (for HTTP handling) and Pydantic (for data validation), which is why the next import matters.from pydantic import BaseModel— Pydantic is a data-validation library. By subclassingBaseModel, you declare what the incoming JSON should look like, and FastAPI automatically validates every request against that schema before your code runs.class InputData(BaseModel):withfeature_1: floatandfeature_2: float— this tells FastAPI: “Every POST request to this endpoint must include a JSON body with two float fields namedfeature_1andfeature_2.” If a client sends{"feature_1": "hello"}, FastAPI rejects it with a 422 error before yourpredictfunction ever runs — you never have to write manual type checks.app = FastAPI()— creates the application instance. All route decorators (@app.post,@app.get) register endpoints on this object.@app.post("/predict")— a decorator that registers thepredictfunction as a handler for HTTP POST requests to the URL path/predict. When a client sendsPOST /predictwith a JSON body, FastAPI parses the JSON into anInputDataobject and passes it as thedataparameter.features = np.array([[data.feature_1, data.feature_2]])— converts the two Pydantic-validated floats into a 2-D NumPy array. The double brackets ([[...]]) create a 2-D array (a matrix with one row and two columns), because sklearn’spredictexpects a 2-D feature matrix, not a 1-D array.prediction = float(features.sum())— a placeholder. In Dev’s real app, this would bemodel.predict(features). Thefloat()call converts the NumPy scalar returned by.sum()into a plain Python float so FastAPI can serialize it to JSON.return {"prediction": prediction}— FastAPI automatically serializes the returned dict to a JSON response. The client receives{"prediction": 8.6}(or whatever the sum is).
FastAPI also auto-generates an interactive API docs page at /docs (Swagger UI) and a machine-readable schema at /openapi.json — both free, with no extra configuration.
FastAPI also generates a docs page at /docs where you can test endpoints directly. Pretty handy for quick checks.
Loading Your Trained Model Inside the Container
This is the part that trips up most beginners — the container has to “see” your model file. Save the model on your laptop first (with pickle or joblib), then COPY it in.
Here’s what that means in practice: the model loads once when the app starts. Not on every prediction request. That keeps it fast.
import joblib
from fastapi import FastAPI
app = FastAPI()
model = None
@app.on_event("startup")
def load_model():
global model
# The Dockerfile will put this file in the /app folder
model = joblib.load("model.pkl")
print("Model loaded into memory!")
@app.post("/predict")
def predict(payload: dict):
prediction = model.predict([list(payload.values())])
return {"result": int(prediction[0])}
This block solves two problems at once: where the model file lives inside the container, and when the model gets loaded into memory.
model = None— declares a module-level variable initialized toNone. This is the “slot” where the loaded model will live. It starts asNoneso that ifload_modelhasn’t run yet, any attempt to callmodel.predictwill fail loudly (AttributeError: 'NoneType' object has no attribute 'predict') instead of silently using stale data.@app.on_event("startup")— a FastAPI lifecycle hook that runsload_modelexactly once, when the application starts up (before it begins accepting requests). This is the key: the model is loaded a single time into memory, not on every request. For Dev’s recommender, loading the model might take a few seconds (deserializing a large sklearn pipeline); doing that on every prediction would make the API unacceptably slow.global model— tells Python that themodelbeing assigned insideload_modelrefers to the module-levelmodelvariable, not a new local variable. Withoutglobal, the assignmentmodel = joblib.load(...)would create a local variable that disappears when the function returns, and the module-levelmodelwould stayNone— a silent bug.joblib.load("model.pkl")— deserializes the model from the filemodel.pkl. Inside the container, this file lives at/app/model.pkl(because the Dockerfile’sWORKDIR /appandCOPY model.pkl .put it there). The relative path works because the container’s working directory is/app.model.predict([list(payload.values())])— converts the incoming dict’s values to a list, wraps it in another list (to make it 2-D), and calls sklearn’spredict. If the payload is{"feature_1": 5.1, "feature_2": 3.5}, thenlist(payload.values())is[5.1, 3.5]and the outer brackets make it[[5.1, 3.5]]— a 1×2 matrix.int(prediction[0])—model.predictreturns a NumPy array (even for a single prediction), soprediction[0]extracts the first (and only) element.int()converts the NumPy integer to a plain Pythonintfor JSON serialization.
Putting It Together: Dockerfile + FastAPI App
Time to bring it all together. You need four files in one folder:
app.py— your FastAPI codemodel.pkl— your saved modelrequirements.txt— the list of librariesDockerfile— the recipe
Here’s the full Dockerfile for a FastAPI setup:
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
# Copy the model and the code
COPY model.pkl .
COPY app.py .
# Expose the port FastAPI runs on
EXPOSE 8000
# Start the Uvicorn server
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
This is the production-ready Dockerfile — the one Dev’s team uses to ship the recommender. It builds on the first Dockerfile but adds the Uvicorn server and port exposure needed for a live API.
FROM python:3.11-slim— same slim base image as before. For a model-serving API, this is usually sufficient because the model is already trained — you don’t need compilers or CUDA toolkits at inference time (unless the model framework requires them, e.g., some TensorFlow GPU builds).COPY requirements.txt ./RUN pip install -r requirements.txt— same layer-caching pattern as the first Dockerfile. If Dev updatesapp.pybut notrequirements.txt, the pip install layer is reused and the rebuild takes seconds instead of minutes.COPY model.pkl ./COPY app.py .— copies the serialized model and the FastAPI app into the container separately. Splitting them into twoCOPYcommands (rather thanCOPY . .) means that if Dev changesapp.pybut not the model, only theapp.pylayer is invalidated — the model layer (which could be hundreds of MB) is reused from cache.EXPOSE 8000— documentation, not a security rule. It tells Docker (and anyone reading the Dockerfile) that the container listens on port 8000. It does not actually open the port — that’s what the-p 8000:8000flag indocker rundoes.EXPOSEis a hint for orchestration tools (like Docker Compose or Kubernetes) to know which port to forward.CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]— the command that starts the API server:uvicorn— an ASGI server that runs async Python web apps. FastAPI is an ASGI framework, so it needs an ASGI server to run; Uvicorn is the standard choice.app:app— the firstappis the module (the fileapp.py), and the secondappis the FastAPI instance (app = FastAPI()inside that file). Uvicorn imports the module and finds the application object.--host 0.0.0.0— binds to all network interfaces. If you used127.0.0.1orlocalhost, the server would only accept connections from inside the container — Docker’s port forwarding from the host wouldn’t reach it.0.0.0.0is the container equivalent of “listen on everything.”--port 8000— the port Uvicorn listens on inside the container. This matches theEXPOSE 8000and the-p 8000:8000indocker run.
Building and Running Your Container Locally
Open a terminal in that folder. You’ll need two commands.
First, build the image:
docker build -t my-model-api .
The . means “look in this folder.”
Second, run it:
docker run -p 8000:8000 my-model-api
The -p 8000:8000 maps your laptop’s port 8000 to the container’s port 8000. See “Uvicorn running on http://0.0.0.0:8000” in the output? You’re live — the model is now a service.
Testing Your Endpoint: Making Real Predictions
So what happens when we actually send data? Open your browser to http://localhost:8000/docs and look for the “POST” button. Click “Try it out,” enter some numbers, and hit “Execute.”
You can also test it from a separate Python script:
import requests
url = "http://localhost:8000/predict"
data = {"feature_1": 5.1, "feature_2": 3.5}
response = requests.post(url, json=data)
print(response.json())
# Output: {'prediction': 8.6}
# This means our containerized model just processed a real request!
This is the client-side test — a script that sends an HTTP request to the containerized API and prints the result. It’s what Dev’s frontend colleagues would run to verify the recommender is working.
import requests— therequestslibrary is Python’s de-facto HTTP client. It’s not in the standard library, so you’dpip install requestson the machine running this test (not inside the container — the container is the server, this script is the client).url = "http://localhost:8000/predict"— the endpoint URL.localhostmeans “this machine” and8000is the host port we mapped with-p 8000:8000. The/predictpath matches the@app.post("/predict")decorator in the FastAPI app. From a different machine, you’d replacelocalhostwith the server’s IP or hostname.data = {"feature_1": 5.1, "feature_2": 3.5}— a plain Python dict matching theInputDataPydantic model. The keys (feature_1,feature_2) must match the field names in the model; the values must be floats (or float-coercible).requests.post(url, json=data)— sends an HTTP POST request. Thejson=dataparameter does two things: (1) serializes the dict to a JSON string ({"feature_1": 5.1, "feature_2": 3.5}), and (2) sets theContent-Type: application/jsonheader so FastAPI knows to parse the body as JSON. Withoutjson=, you’d have to manually serialize and set the header.response.json()— parses the JSON response body back into a Python dict. The server returned{"prediction": 8.6}, soresponse.json()gives{'prediction': 8.6}. If the server returned an error (e.g., 422 for invalid input),response.json()would give you the error detail instead.- The commented output
{'prediction': 8.6}—5.1 + 3.5 = 8.6, which is what the dummyfeatures.sum()prediction returns. In Dev’s real recommender, this would be a product ID or a ranked list of recommendations.
Debugging Inside the Container: Logs and Common Errors
If the container starts but crashes immediately, don’t panic. Run docker logs <container_name>.
- Error:
ModuleNotFoundError? You left a library out ofrequirements.txt. - Error:
FileNotFoundError: model.pkl? You didn’tCOPYthe model file into your Dockerfile. - Error:
Address already in use? Another version of the app is probably still running. Stop it and try again.
Optimizing Your Container: Smaller Size, Faster Builds
Your first image might be 1GB. That’s too big!
- Stick with
python:3.11-slim, like the Dockerfiles above, instead of the fullpython:3.11image. That choice alone saves about 600MB; if you ever start a new project from the full image, switching to-slimis the first optimization to make. - Use a
.dockerignorefile. Create a file named.dockerignoreand put__pycache__and.gitinside. This prevents Docker from copying junk files into your image.
Sharing Your Container: Docker Hub
To share your model with a friend, push it to Docker Hub.
docker tag my-model-api yourusername/my-model-api:v1docker push yourusername/my-model-api:v1
Anyone can now run docker pull yourusername/my-model-api:v1 and get the same model up in seconds — exactly as it ran on your machine.
What’s Next: From Local Container to Production
Your laptop is step one. In a real company, you’d move this to the cloud (AWS, Google Cloud, or Azure). You might reach for Kubernetes if you need to run hundreds of these containers at once. The container you built today is the exact same one that will run in the cloud.
Docker vs. simpler deployment options — when is a container worth it?
Dev’s engineering team pushed back: “Do we really need Docker for a single model? Can’t we just use a venv or a serverless function?” The answer depends on what you’re optimizing for.
| Approach | What it guarantees | Setup cost | When it’s the right call | When it’s overkill |
|---|---|---|---|---|
requirements.txt + venv | Python package versions match across machines | Trivial: create venv, pip install -r requirements.txt | Single model, single deployment target, same OS across dev and prod, team of 1–2 | You have any system-level dependency (native libraries, CUDA, specific glibc version), or dev and prod run different operating systems (macOS dev, Linux prod) |
| Serverless function (AWS Lambda, Google Cloud Functions) | The cloud provider manages the environment; you upload code + deps | Low for simple models: zip your code + requirements.txt, deploy | Stateless models with small dependencies, low/intermittent traffic, no long-running inference, model artifact under the deployment size limit (250 MB for Lambda) | Model has heavy dependencies (PyTorch, CUDA), inference takes >30 seconds (Lambda timeout), you need GPU access, or you need fine-grained control over the runtime environment |
| Docker container | Everything: OS, system libraries, Python version, pip packages, model file — one sealed unit | Medium: write Dockerfile, build image, push to registry, configure container runtime on server | You need reproducibility across environments (dev → staging → prod), you have system-level dependencies, you deploy to multiple cloud providers, or your team is >2 people who need to share the exact same environment | You have a single tiny model, one deployment target, no system dependencies, and the team is just you — a venv with pinned versions may be enough |
The trap to avoid: reaching for Docker because “that’s what production looks like.” If Dev’s model is a single sklearn model with pure-Python dependencies and the deployment target is one server running the same Linux distribution as his laptop, a venv with a frozen requirements.txt gets him 90% of the way for 5% of the effort. Docker earns its keep when the environment complexity grows — multiple deployment targets, system-level dependencies, or a team that needs to reproduce each other’s setups.
The hybrid most teams land on: start with a venv + frozen requirements for prototyping, add a Dockerfile once the model needs to run on a different machine than the one it was built on, and move to a container registry + orchestration (Kubernetes, ECS, Cloud Run) when you have more than one deployment environment or more than one person deploying.
Recap: From Jupyter to Production
- Wrap your model in a FastAPI app.
- Write a Dockerfile.
- Build the image with
docker build. - Run and test locally with
docker run. - Share your image via a registry.
Hands-On: Build and Deploy Your Own Model
Here’s a complete, runnable example using the Iris dataset.
File: requirements.txt
fastapi
uvicorn
scikit-learn
joblib
pandas
This is a minimal requirements.txt — the dependency manifest that the Dockerfile’s RUN pip install -r requirements.txt reads. Each line is a package name that pip will install from PyPI.
fastapi— the web framework that defines the/predictendpoint and auto-generates the/docstesting page.uvicorn— the ASGI server that actually runs the FastAPI app. Without this,CMD ["uvicorn", "app:app", ...]in the Dockerfile would fail because theuvicornexecutable wouldn’t exist.scikit-learn— the ML library used to train and serialize the Iris model. At runtime, it’s needed to deserialize the.pklfile and runmodel.predict().joblib— the serialization library sklearn uses internally. It’s installed automatically as a dependency of scikit-learn, but listing it explicitly documents the dependency and protects against future versions where it might not be auto-installed.pandas— used in thepredictfunction to construct a DataFrame from the request inputs, because the model was trained on pandas DataFrames and may expect column names to match.
Note: these are unpinned version specifiers. In a real production setup, Dev would pin exact versions (e.g., scikit-learn==1.3.2) to guarantee reproducibility — the same “works on my machine” guarantee that containers provide at the OS level, pinned requirements provide at the Python-package level.
File: app.py
from fastapi import FastAPI
import joblib
import pandas as pd
app = FastAPI()
# Assume you saved a model named iris_model.pkl
model = joblib.load("iris_model.pkl")
@app.post("/predict")
def predict(sepal_l: float, sepal_w: float):
df = pd.DataFrame([[sepal_l, sepal_w]], columns=['sepal_length', 'sepal_width'])
pred = model.predict(df)
return {"species_code": int(pred[0])}
This is a complete, runnable FastAPI app for the Iris dataset — the same pattern as the recommender, but with real model loading and a simpler input schema.
model = joblib.load("iris_model.pkl")— loads the model at module level, which means it runs once when the module is imported (i.e., when Uvicorn starts the server). This is simpler than the@app.on_event("startup")approach but has a trade-off: if the model file is missing, the entire app crashes on import, before any request can be handled. Thestartupevent approach at least lets FastAPI start and report a clean error.def predict(sepal_l: float, sepal_w: float):— unlike the earlierInputData(BaseModel)approach, this uses FastAPI’s “query parameter” style. FastAPI sees the type hints (float) and automatically parses the incoming request’s query parameters. The client would call something likePOST /predict?sepal_l=5.1&sepal_w=3.5. For a production recommender with many features, the Pydantic model approach (JSON body) is cleaner; for a two-parameter demo, this is simpler.pd.DataFrame([[sepal_l, sepal_w]], columns=['sepal_length', 'sepal_width'])— constructs a one-row DataFrame with the exact column names the model was trained on. Thecolumnsargument is critical: if the model was trained with column names['sepal_length', 'sepal_width']and you pass['sl', 'sw']instead, some sklearn models will raise a warning or silently fail. This is a form of the same train-serving skew Dev fought in the previous article.pred = model.predict(df)— runs sklearn’spredicton the DataFrame. Returns a NumPy array of length 1 (one prediction for one row).int(pred[0])— extracts the single prediction (an integer species code: 0, 1, or 2 for the three Iris species) and converts it from a NumPy integer to a plain Pythonintfor JSON serialization. Withoutint(), FastAPI might struggle to serialize a NumPyint64in some configurations.return {"species_code": int(pred[0])}— FastAPI wraps this dict as a JSON response:{"species_code": 1}.
Troubleshooting: Common Gotchas and Fixes
- Gotcha: “I changed my code, but the container still runs the old version!”
- Fix: Run
docker buildagain each time you change code. Docker doesn’t rebuild the image automatically.
- Fix: Run
- Gotcha: “The container runs, but I can’t reach http://localhost:8000.”
- Fix: Check that you passed
-p 8000:8000. Without that flag, the container has no route to your host — like a house with no doors.
- Fix: Check that you passed
Next Steps: From Manual Deploys to Automation
The recommender was containerized and serving predictions, but Dev still deployed by hand. Every update meant SSHing into the server, copying files, running docker build and docker run from a terminal, and hoping he didn’t miss a step. It worked for the first deploy. It wouldn’t survive the next — when a rushed manual push ships a broken model to ten million users with no way back. That’s when Dev would learn why CI/CD for machine learning isn’t optional.
Check Your Understanding
Questions below move from simple recall up to open-ended design, roughly following Bloom’s Taxonomy.
Remember What is the difference between a Docker “Image” and a “Container,” using the article’s blueprint-vs-object analogy?
Understand
In your own words, explain why the article loads the model once at startup (@app.on_event("startup")) instead of inside the predict function that runs on every request.
Apply
Using the article’s Dockerfile layer-caching explanation (“if you change your code but not your libraries, Docker reuses the RUN pip install layer”), would changing only app.py (not requirements.txt) force Docker to reinstall all the pip packages on the next build?
Analyze
The article lists three common errors (ModuleNotFoundError, FileNotFoundError: model.pkl, Address already in use) with three different fixes. Walk through why FileNotFoundError: model.pkl specifically points to a missing COPY instruction in the Dockerfile rather than a missing pip package, even though both errors can make the container crash on startup.
Evaluate
The article’s optimization advice is to switch from python:3.11 to python:3.11-slim to save ~600MB. Critique this as a default recommendation: what could break in a -slim image that wouldn’t break in the full image, and how would you find out before it surprises you in production?
Create
Design a .dockerignore file for a real ML project directory that also contains a data/ folder with 5GB of training CSVs, a notebooks/ folder, and a .venv/ virtual environment folder — none of which the container needs at runtime. List what you’d exclude and explain the risk of forgetting even one of them.
Related articles
- Building an ML Pipeline That Avoids Train-Serving Skew) — Dev builds the skew-resistant training pipeline that produced the model this article ships into a container.
- CI/CD for Machine Learning: What Should You Actually Automate?) — Dev automates the deploy after a manual
docker runships a broken model to production, and learns what a CI/CD pipeline for ML should actually cover.
References & Further reading
- Merkel, D. (2014). Docker: Lightweight Linux Containers for Consistent Development and Deployment. Linux Journal, 2014(239). — the foundational article that introduced Docker’s container model and the case for environment-consistent deployment.
- FastAPI official documentation — https://fastapi.tiangolo.com/ — covers request validation, lifecycle events (
@app.on_event("startup")), and automatic OpenAPI/Swagger docs generation. - Docker official documentation — https://docs.docker.com/ — reference for Dockerfile instructions (
FROM,COPY,RUN,CMD,EXPOSE), layer caching, and the.dockerignorefile format. - Uvicorn documentation — https://www.uvicorn.org/ — the ASGI server used in this article’s
CMDline; covers--host,--port, and worker-process configuration.
Apply What You Learned is for Supporter and Insider subscribers.
Subscribe to unlock the exercises on this post.
See plansRelated articles
- MLOps Under review
CI/CD for Machine Learning: What Should You Actually Automate?
Stop shipping models by hand — learn which ML pipeline steps to automate first, from pytest unit tests to GitHub Actions training and Docker deployment.
- MLOps Under review
Monitoring Model Performance in Production (Without the Fancy Tools)
Models fail silently in production. Learn to detect data drift and prediction collapse with a lightweight Python dashboard before revenue drops.
- MLOps Under review
Model Versioning and Rollbacks: Treating Models Like Code You Can Revert
Version your ML models with metadata, data hashes, and environment snapshots so you can roll back bad deploys in seconds — without guessing or retraining.
- MLOps Under review
How to Detect and Handle Data Drift in Production Models
Learn how to detect data drift in production ML models using KS tests and Wasserstein distance, build a drift monitor, and respond when distributions shift.
Looking for something else?
Search every article by title, summary or topic.