2409 words
12 minutes
From Notebooks to Containers: Dockerizing Your ML Projects

From Notebooks to Containers: Dockerizing Your ML Projects#

Machine learning (ML) projects can involve a complex web of dependencies: libraries, frameworks, drivers, environment variables, model files, and more. If you’ve ever tried to share your Jupyter notebook with a colleague only to run into “it worked on my machine�?issues, Docker might be your new best friend. Containerization provides a consistent, reproducible environment to train, serve, and collaborate on ML projects. In this blog post, we’ll explore how to transition from working in local notebooks to packaging your machine learning workloads into Docker containers―step by step.

This guide should help you, whether you’re a newcomer or an experienced developer seeking professional-level best practices. We’ll begin with Docker fundamentals, proceed to basic containerization techniques, and eventually move on to advanced concepts like multi-stage builds, GPU support, and integrating Docker with orchestration systems.

Table of Contents#

  1. What Is Docker and Why Use It for ML?
  2. Getting Started with Docker
  3. Building Your First Dockerfile
  4. Dockerizing a Simple ML “Hello World”
  5. Dockerizing Jupyter Notebooks and ML Environments
  6. Managing Data and Model Artifacts
  7. Using Docker Compose for Complex ML Workflows
  8. Production Best Practices
  9. Advanced Docker Topics for ML
  10. Common Pitfalls and How to Avoid Them
  11. Conclusion

1. What Is Docker and Why Use It for ML?#

Docker is a platform for building, running, and distributing applications in lightweight, stand-alone containers. Instead of juggling various Python versions, internal libraries, environment variables, and package installations, you can use Docker to create a consistent environment that “just works�?for anyone with Docker installed. Here’s why developers in machine learning (and beyond) love it:

  • Consistency Across Environments: Whether you’re on Windows, macOS, or Linux, running a Docker container yields the same environment with the same versions of Python, libraries, and system tools.
  • Isolation: Containers keep your environment separate from your host machine, preventing conflicts with other projects.
  • Scalability and Deployment: Once containerized, your ML workloads can be easily deployed on cloud services, local clusters, or even tiny edge devices (depending on resource constraints).
  • Collaboration: Share your container (via an image hosted on Docker Hub or a private registry) so collaborators or end users can pull, run, and replicate your setup.

Container vs. Virtual Machine#

In traditional virtual machines (VMs), the OS is virtualized entirely; each VM needs its own full operating system kernel. In contrast, Docker containers share the host’s OS kernel but isolate processes, libraries, and the file system. This makes containers more lightweight and faster to spin up or tear down.

2. Getting Started with Docker#

Installation#

If you haven’t already, install Docker Desktop (for macOS or Windows) or the Docker Engine (for Linux). Below are some helpful links:

Once installed, you can check your version:

Terminal window
docker --version

Basic Commands#

Familiarize yourself with some core Docker commands:

CommandDescription
docker run Creates and runs a container from the specified image.
docker psLists currently running containers.
docker imagesLists downloaded images.
docker build -t : .Builds a Docker image from a Dockerfile in the current folder.
docker stop <container_id>Stops a running container.
docker rm <container_id>Removes a container (after stopping it).

You can think of an image as a recipe, while a container is its running instance. Docker Hub, Docker’s default public registry, has a vast catalog of pre-built images (e.g., Python, Ubuntu, TensorFlow, PyTorch, etc.).

3. Building Your First Dockerfile#

Dockerfiles define the instructions to build a Docker image. Let’s see a simple structure:

# Use an official Python base image
FROM python:3.9-slim
# Set a working directory
WORKDIR /app
# Copy requirements file
COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the remaining code
COPY . .
# Define the command to run
CMD ["python", "main.py"]

Breakdown of Dockerfile Statements#

  1. FROM: Mandatory. Specifies the base image (e.g., python:3.9-slim).
  2. WORKDIR: Sets the working directory inside the container.
  3. COPY: Copies files from your host machine into the container image.
  4. RUN: Executes commands during the build (e.g., installing packages).
  5. CMD: Specifies the default command to run when the container starts.

Building and Running#

To build this Docker image, run:

Terminal window
docker build -t my-ml-image:latest .

The -t flag names and tags your image. You can replace my-ml-image and latest with any name or version tag you prefer.

To run a container using the newly built image, execute:

Terminal window
docker run --rm my-ml-image:latest

The --rm flag removes the container once it stops (helpful for testing).

4. Dockerizing a Simple ML “Hello World�?#

Let’s illustrate the steps more concretely with a minimal machine learning example. Suppose we have the following file structure:

my_docker_ml_project/
├── requirements.txt
├── main.py
└── Dockerfile

Example Files#

requirements.txt#

scikit-learn==1.2.2
pandas==1.5.3
numpy==1.23.5

(Adjust versions as needed.)

main.py#

import pandas as pd
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
# Load example dataset
iris = load_iris()
X = iris.data
y = iris.target
# Train a simple model
clf = RandomForestClassifier()
clf.fit(X, y)
# Predict the first sample
prediction = clf.predict([X[0]])
print(f"Predicted class for the first sample: {prediction[0]}")

Dockerfile#

# Use Python as base
FROM python:3.9-slim
# Working directory
WORKDIR /app
# Copy the requirements
COPY requirements.txt requirements.txt
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the code
COPY . /app
# Default command
CMD ["python", "main.py"]

Building and Running#

  1. In your project directory, run:

    Terminal window
    docker build -t simple-ml:latest .
  2. Run the container:

    Terminal window
    docker run --rm simple-ml:latest

You should see terminal output indicating the predicted class for the first Iris dataset sample. This example demonstrates how you can package a basic Python script, plus dependencies, into a container.

5. Dockerizing Jupyter Notebooks and ML Environments#

An ML workflow commonly involves interactive notebooks. Let’s create a Docker image that launches a Jupyter Notebook inside the container, accessible via a browser on your host machine.

Dockerfile for Jupyter Notebooks#

Consider the following Dockerfile:

FROM python:3.9-slim
# Install some system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /notebooks
# Copy requirement files
COPY requirements.txt /tmp/requirements.txt
# Install Python dependencies
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# Expose port 8888 for Jupyter
EXPOSE 8888
# Set environment variable to avoid writing pyc files
ENV PYTHONDONTWRITEBYTECODE=1
# Start Jupyter Notebook
CMD ["jupyter", "notebook", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--allow-root"]

Running the Container#

Build the image:

Terminal window
docker build -t ml-notebook:latest .

When you run your container, map port 8888 in the container to an available port on your host (also 8888, typically):

Terminal window
docker run -p 8888:8888 ml-notebook:latest

Then open your browser at http://localhost:8888 to see the Jupyter environment. You’ll note a token or password prompt that Jupyter automatically provides. If you’d like to skip the token requirement (not recommended in production), you can pass additional flags or set a password in the Dockerfile.

Sharing Files with the Host#

You’ll often want your local notebooks to be accessible inside the container. You can use a volume mount for that:

Terminal window
docker run -p 8888:8888 -v $(pwd):/notebooks ml-notebook:latest

Now your current host directory is mapped to /notebooks in the container, so you can edit notebooks locally and see changes reflected in the container.

6. Managing Data and Model Artifacts#

Machine learning typically requires large datasets and trained models. How organizations handle data in Docker depends on workflow considerations.

Option 1: Copy Data into the Image#

You can copy data into the Docker image via COPY dataset.csv /app/dataset.csv. This is convenient for small data or publicly available files. However, large data files will bloat your image size and slow down builds.

Option 2: Mount Volumes#

For larger files, consider mounting a volume so your container can access data stored on the host:

Terminal window
docker run -v /path/on/host:/path/in/container ...

This is generally preferred for local development or ephemeral containers.

Option 3: Fetch Data Dynamically#

You could fetch data from a remote source (e.g., S3, Azure Blob, or an HTTP endpoint) using a script inside your container. This approach is flexible but adds complexity—managing authentication, ensuring reproducibility, etc.

Option 4: Docker Data Volumes#

For more advanced setups, you might create named volumes that are persistent across container runs. For example:

Terminal window
docker volume create my_data_volume
docker run -v my_data_volume:/app/data ...

This volume can persist even if the container is removed.

7. Using Docker Compose for Complex ML Workflows#

A typical ML application might require multiple services: a database, a queue, a training container, an inference container, and so on. Docker Compose allows you to define multi-container applications using a docker-compose.yml file.

Example docker-compose.yml#

version: '3'
services:
ml_notebook:
build: .
ports:
- "8888:8888"
volumes:
- .:/notebooks
environment:
- PYTHONDONTWRITEBYTECODE=1
db:
image: postgres:14
environment:
- POSTGRES_USER=ml_user
- POSTGRES_PASSWORD=secret

In this example:

  • ml_notebook: Built from your local Dockerfile, exposes port 8888, and mounts the current directory as a volume.
  • db: Uses the official Postgres image. This service can be used to store data or model metadata.

Commands#

  1. docker-compose up: Starts all services.
  2. docker-compose down: Stops and removes all containers, networks, and volumes created by up.

Compose helps you manage configurations for multiple containers with minimal overhead.

8. Production Best Practices#

Once you move toward production environments, consider these best practices and constraints:

8.1 Use a Lightweight Base Image#

Large base images (e.g., an unoptimized OS) can balloon your image size. Using official images like python:3.9-slim or python:3.9-alpine is common. The slim and alpine variants remove unnecessary packages, resulting in smaller and often more secure images.

8.2 Pin Dependencies#

In your requirements.txt or conda environment.yml, pin exact versions. This ensures that subsequent builds won’t break due to version drift.

Example:

scikit-learn==1.2.2
pandas==1.5.3
numpy==1.23.5

8.3 Minimize Layer Size#

Every RUN, COPY, or ADD statement in a Dockerfile creates a new layer. Optimize your Dockerfile to reduce the number of layers, or at least ensure each layer has minimal overhead. For instance, chaining package installation commands into one RUN statement can reduce total size:

RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
wget && \
rm -rf /var/lib/apt/lists/*

8.4 Security Scanning#

Scan your images for known vulnerabilities. Tools like Trivy or Docker’s built-in scanning features can help. Older packages can expose you to security risks and outdated libraries.

8.5 .dockerignore#

Much like .gitignore, the .dockerignore file defines which files not to include in your build context. For instance, ignoring large logs, .git directories, or local environment files. This can reduce image size and speed up builds:

.git
__pycache__
*.pyc
venv
.dockerignore
.idea

9. Advanced Docker Topics for ML#

Once you’ve nailed the fundamentals, you can start exploring more advanced topics that can significantly enhance your ML workflows.

9.1 Multi-Stage Builds#

In multi-stage builds, you can have multiple FROM instructions to use distinct images for building code and final distribution. For example, you might compile a shared library in one stage, then copy the compiled binaries into a minimal final image.

# Stage 1: Build environment
FROM python:3.9-slim as builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt
# Stage 2: Final image
FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /app/deps /app/deps
ENV PYTHONPATH=/app/deps
COPY . /app
CMD ["python", "main.py"]

This approach can reduce the size of your final image by excluding build dependencies.

9.2 GPU Support with NVIDIA Docker#

If you need GPU acceleration (e.g., TensorFlow or PyTorch), you can leverage NVIDIA Container Toolkit. This allows the container to access GPU resources, provided the host machine has an NVIDIA GPU and appropriate drivers:

  1. Install the NVIDIA Container Toolkit on the host.
  2. Use Docker images that include GPU-accelerated frameworks (like nvidia/cuda as a base or official PyTorch/TensorFlow GPU images).
  3. Run your container with --gpus all:
    Terminal window
    docker run --gpus all my_gpu_image

9.3 Docker in Kubernetes#

Kubernetes has become the de-facto standard for container orchestration. You can deploy your ML containers in a Kubernetes cluster, harnessing features like auto-scaling, rolling updates, and advanced networking. You’ll typically define a Deployment resource for your container. For ML training or batch jobs, you might use a Job resource. Integrating with Kubernetes can be a logical progression when you need robust scaling or distributed computing.

9.4 Serving ML Models in Docker#

When it’s time to serve a trained model, you can build a Docker image containing your inference code, plus any libraries needed for real-time or batch predictions. Examples:

  • Flask, FastAPI, or Flask-RESTPlus for a quick REST service.
  • Streamlit or Dash for interactive dashboards.
  • Model servers like TensorFlow Serving or TorchServe.

A typical Dockerfile for a model-serving API could look like:

FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app
CMD ["python", "app.py"]

Then you expose a port in app.py (e.g., a FastAPI running on port 8000), and run the container with:

Terminal window
docker run -p 8000:8000 model-serving:latest

9.5 Testing and CI/CD Integration#

Continuous integration (CI) systems (like GitHub Actions, GitLab CI, Jenkins) can be configured to build and test your Docker images on every commit:

  1. Build the Docker image: Use a Docker build step in your pipeline.
  2. Run tests inside the container: If your CMD or entrypoint can run tests, or you can override the command specifically for the test step.
  3. Push the image to a registry: If tests pass, push to Docker Hub or another registry.

This approach ensures that any new code commits remain compatible with your Dockerized environment, preventing breakage in production.

10. Common Pitfalls and How to Avoid Them#

Even experienced Docker users can run into a few snags. Let’s review some common pitfalls:

10.1 Large Images#

  • Symptom: Slow builds, bloated images.
  • Solution: Use slim or Alpine base images. Remove caches. Use multi-stage builds. Avoid copying unnecessary files with .dockerignore.

10.2 Permissions Issues#

  • Symptom: Container can’t read/write a volume mounted from the host.
  • Solution: Ensure you set appropriate ownership and permissions. Sometimes you must run chown or use user flags in your Dockerfile.

10.3 Networking Confusion#

  • Symptom: Container tries to connect to a service on the host but fails.
  • Solution: Remember that localhost inside a container is not the host machine. You can use host.docker.internal on macOS/Windows or set up a user-defined network for communication between containers.

10.4 Unpinned Dependencies#

  • Symptom: Inconsistent environment after each build.
  • Solution: Pin your Python dependencies and system packages. This ensures deterministic builds.

10.5 Data Persistence#

  • Symptom: Data or model artifacts lost when the container is removed.
  • Solution: Use volumes or external storage solutions (cloud object storage, local persistent volumes, etc.).

11. Conclusion#

Docker has revolutionized the way developers build, ship, and run applications. For machine learning projects, containerization can be a game-changer: it eliminates environment inconsistencies, streamlines collaboration, and accelerates deployment. In this post, we covered the journey from a simple Python script to more sophisticated multi-container deployments:

  • We explored Docker basics: installation, images vs. containers, essential commands.
  • We learned to write a Dockerfile, define environment dependencies, and build images.
  • We ran through a quick ML “Hello World�?example to demonstrate containerization.
  • We packaged Jupyter Notebooks to run in our container, mapped local volumes, and leveraged Docker Compose for multi-service workflows.
  • We discussed production-level best practices, including smaller base images, pinned dependencies, security scanning, and robust .dockerignore usage.
  • We delved into advanced tooling: multi-stage builds, GPU-equipped containers, Kubernetes orchestration, and CI/CD integration.
  • We surveyed common pitfalls and solutions.

Ultimately, Docker helps you spend less time wrestling with environment issues and more time refining your ML models. By effectively containerizing your workflows, you’ll find it easier to collaborate, scale, and deploy. Whether training in notebooks or serving large-scale models in production, containers keep your projects consistent, portable, and reliable.

If you haven’t already, adopt Docker in your ML pipelines and share your images with your team. It’s a simple step that can yield enormous benefits in the long run. Good luck building, training—containerizing—and happy modeling!

From Notebooks to Containers: Dockerizing Your ML Projects
https://science-ai-hub.vercel.app/posts/c45ead1e-87d9-441f-aae1-ec2b0c0b70e2/2/
Author
AICore
Published at
2025-06-23
License
CC BY-NC-SA 4.0