docker run
docker-compose.yml
As you learned in the previous chapter, a Dockerfile
is the blueprint for building your container image. It's a text file containing a sequence of instructions that Docker follows to assemble the image layer by layer. While a functional Dockerfile
can be written with minimal planning, a well-structured Dockerfile
is significantly easier to read, maintain, debug, and optimize, especially for complex Machine Learning environments. Think of it like writing clean, organized code; the same principles apply here.
The order of instructions in your Dockerfile
matters greatly, primarily because of Docker's build cache. Docker builds images in layers, and each instruction in the Dockerfile
typically creates a new layer. When you rebuild an image, Docker checks if the instruction and the files it depends on have changed since the last build. If not, Docker reuses the existing layer from its cache instead of executing the instruction again. This can dramatically speed up build times.
To take advantage of layer caching effectively, structure your Dockerfile
with the least frequently changing instructions first, followed by instructions that change more often. A common structure for ML projects looks something like this:
FROM
): Start by specifying the foundation of your image. This changes infrequently.LABEL
, ARG
): Define labels or build-time arguments. These might change occasionally.RUN apt-get update && apt-get install -y ...
): Install operating system packages (like git
, wget
, or C++ build tools). These dependencies usually don't change often once established.RUN pip install --no-cache-dir --upgrade pip
): Prepare the Python environment.COPY requirements.txt .
, RUN pip install -r requirements.txt
or Conda equivalent): Install the core Python libraries (TensorFlow, PyTorch, Scikit-learn, etc.). This is a frequent point of change, but copying the requirements file before running the install allows Docker to cache the potentially lengthy installation step if the requirements haven't changed.COPY ./src /app/src
): Copy your ML scripts, notebooks, utility files, etc. This often changes with every code modification.WORKDIR
): Set the directory context for subsequent commands.EXPOSE
): Declare network ports the container will listen on (relevant for inference servers).CMD
or ENTRYPOINT
): Specify the command to run when the container starts.Consider this simplified diagram illustrating how reordering instructions impacts cache usage:
Layer caching comparison. In the "Good Order" example, changing only the source code (copied last) allows Docker to reuse the cached layers for dependencies if
requirements.txt
hasn't changed. In the "Less Optimal Order", copying all code early often invalidates the cache for subsequent steps like dependency installation, even if only Python scripts were modified.
Beyond ordering, maintainability is enhanced by:
#
) to explain non-obvious steps, choices of specific versions, or the purpose of a block of commands.
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Install system dependencies needed for some Python libraries
# Example: libgomp1 is often needed for libraries like OpenCV or LightGBM
RUN apt-get update && apt-get install -y --no-install-recommends \
libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies using the requirements file
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy local code to the container image
COPY ./src /app/src
# Default command to run when the container starts
CMD ["python", "/app/src/train.py"]
RUN
commands using &&
and line continuations (\
) to create a single logical unit and potentially reduce the number of image layers. While modern builders optimize layer handling, this practice still improves readability by keeping related operations together. For instance, update the package list, install packages, and clean up apt cache files all in one RUN
instruction.
# Less readable and potentially more layers
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
RUN rm -rf /var/lib/apt/lists/*
# More readable, single layer for this logical operation
RUN apt-get update && apt-get install -y --no-install-recommends \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*
COPY
commands like COPY . .
early in the Dockerfile
if possible, especially before dependency installation. Instead, explicitly copy only what's needed for a specific step (e.g., COPY requirements.txt .
before installing dependencies, then COPY ./src /app/src
later). This fine-grained approach maximizes cache utilization.Adopting a consistent structure for your Dockerfile
makes your ML environments more predictable and your build process more efficient. It also makes collaboration easier, as team members can quickly understand the image's composition. As we proceed through this chapter, you'll see how this structure applies when selecting base images and managing dependencies for your specific ML needs.
© 2025 ApX Machine Learning