Python & Data Science
MLOps Under review

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 throws ModuleNotFoundError before 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 .pkl file 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 if load succeeds, predict may 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 -slim variant 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 subsequent COPY and RUN commands 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 /app directory. The . means “the current working directory inside the container” (which is /app because of WORKDIR). 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-dir tells 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 before COPY . . — 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 the RUN pip install layer, changing app.py or model.pkl doesn’t invalidate the pip cache — only changing requirements.txt does.
  • CMD ["python", "app.py"] — the command Docker runs when the container starts. The JSON-array syntax (["python", "app.py"]) is the “exec form” — it runs python directly as PID 1, which handles signals (like Ctrl+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 subclassing BaseModel, 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): with feature_1: float and feature_2: float — this tells FastAPI: “Every POST request to this endpoint must include a JSON body with two float fields named feature_1 and feature_2.” If a client sends {"feature_1": "hello"}, FastAPI rejects it with a 422 error before your predict function 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 the predict function as a handler for HTTP POST requests to the URL path /predict. When a client sends POST /predict with a JSON body, FastAPI parses the JSON into an InputData object and passes it as the data parameter.
  • 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’s predict expects a 2-D feature matrix, not a 1-D array.
  • prediction = float(features.sum()) — a placeholder. In Dev’s real app, this would be model.predict(features). The float() 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 to None. This is the “slot” where the loaded model will live. It starts as None so that if load_model hasn’t run yet, any attempt to call model.predict will fail loudly (AttributeError: 'NoneType' object has no attribute 'predict') instead of silently using stale data.
  • @app.on_event("startup") — a FastAPI lifecycle hook that runs load_model exactly 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 the model being assigned inside load_model refers to the module-level model variable, not a new local variable. Without global, the assignment model = joblib.load(...) would create a local variable that disappears when the function returns, and the module-level model would stay None — a silent bug.
  • joblib.load("model.pkl") — deserializes the model from the file model.pkl. Inside the container, this file lives at /app/model.pkl (because the Dockerfile’s WORKDIR /app and COPY 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’s predict. If the payload is {"feature_1": 5.1, "feature_2": 3.5}, then list(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.predict returns a NumPy array (even for a single prediction), so prediction[0] extracts the first (and only) element. int() converts the NumPy integer to a plain Python int for JSON serialization.

Putting It Together: Dockerfile + FastAPI App

Time to bring it all together. You need four files in one folder:

  1. app.py — your FastAPI code
  2. model.pkl — your saved model
  3. requirements.txt — the list of libraries
  4. Dockerfile — 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 updates app.py but not requirements.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 two COPY commands (rather than COPY . .) means that if Dev changes app.py but not the model, only the app.py layer 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:8000 flag in docker run does. EXPOSE is 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 first app is the module (the file app.py), and the second app is 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 used 127.0.0.1 or localhost, the server would only accept connections from inside the container — Docker’s port forwarding from the host wouldn’t reach it. 0.0.0.0 is the container equivalent of “listen on everything.”
    • --port 8000 — the port Uvicorn listens on inside the container. This matches the EXPOSE 8000 and the -p 8000:8000 in docker 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 — the requests library is Python’s de-facto HTTP client. It’s not in the standard library, so you’d pip install requests on 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. localhost means “this machine” and 8000 is the host port we mapped with -p 8000:8000. The /predict path matches the @app.post("/predict") decorator in the FastAPI app. From a different machine, you’d replace localhost with the server’s IP or hostname.
  • data = {"feature_1": 5.1, "feature_2": 3.5} — a plain Python dict matching the InputData Pydantic 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. The json=data parameter does two things: (1) serializes the dict to a JSON string ({"feature_1": 5.1, "feature_2": 3.5}), and (2) sets the Content-Type: application/json header so FastAPI knows to parse the body as JSON. Without json=, 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}, so response.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 dummy features.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 of requirements.txt.
  • Error: FileNotFoundError: model.pkl? You didn’t COPY the 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!

  1. Stick with python:3.11-slim, like the Dockerfiles above, instead of the full python:3.11 image. That choice alone saves about 600MB; if you ever start a new project from the full image, switching to -slim is the first optimization to make.
  2. Use a .dockerignore file. Create a file named .dockerignore and put __pycache__ and .git inside. 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.

  1. docker tag my-model-api yourusername/my-model-api:v1
  2. docker 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.

ApproachWhat it guaranteesSetup costWhen it’s the right callWhen it’s overkill
requirements.txt + venvPython package versions match across machinesTrivial: create venv, pip install -r requirements.txtSingle model, single deployment target, same OS across dev and prod, team of 1–2You 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 + depsLow for simple models: zip your code + requirements.txt, deployStateless 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 containerEverything: OS, system libraries, Python version, pip packages, model file — one sealed unitMedium: write Dockerfile, build image, push to registry, configure container runtime on serverYou 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 environmentYou 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

  1. Wrap your model in a FastAPI app.
  2. Write a Dockerfile.
  3. Build the image with docker build.
  4. Run and test locally with docker run.
  5. 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 /predict endpoint and auto-generates the /docs testing page.
  • uvicorn — the ASGI server that actually runs the FastAPI app. Without this, CMD ["uvicorn", "app:app", ...] in the Dockerfile would fail because the uvicorn executable wouldn’t exist.
  • scikit-learn — the ML library used to train and serialize the Iris model. At runtime, it’s needed to deserialize the .pkl file and run model.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 the predict function 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. The startup event approach at least lets FastAPI start and report a clean error.
  • def predict(sepal_l: float, sepal_w: float): — unlike the earlier InputData(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 like POST /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. The columns argument 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’s predict on 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 Python int for JSON serialization. Without int(), FastAPI might struggle to serialize a NumPy int64 in 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 build again each time you change code. Docker doesn’t rebuild the image automatically.
  • 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.

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.


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 .dockerignore file format.
  • Uvicorn documentation — https://www.uvicorn.org/ — the ASGI server used in this article’s CMD line; 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 plans

Looking for something else?

Search every article by title, summary or topic.