So far in this series you've learned to version your code with Git and your data and models with DVC. But there's one more problem that breaks ML projects constantly, and it has nothing to do with versioning.
"It works on my machine."
You build a model, it runs perfectly on your laptop. You send it to a teammate, or deploy it to a server, and it crashes. Wrong Python version. Missing library. Different operating system. A dependency that needs a system package you forgot about.
Docker solves this. It packages your entire application, code, dependencies, system libraries, the exact runtime, into one portable unit that runs identically everywhere. This article explains what Docker is, how it works, every core piece of it, with analogies, and finishes with a full deep dive into the Dockerfile.
What this article covers
| # | Topic |
|---|---|
| 1 | The problem: "it works on my machine" |
| 2 | Virtual machines vs. containers |
| 3 | What Docker is and how it works |
| 4 | Core Docker concepts (image, container, registry...) |
| 5 | Docker architecture (daemon, client, engine) |
| 6 | Essential Docker commands |
| 7 | The Dockerfile: a complete deep dive |
Part 1The problem: "it works on my machine"
The shipping analogy (where the name comes from)
Before containers existed in computing, think about how physical goods used to be shipped. Loading coffee beans, cars, bananas, and machinery onto a ship one at a time, each with different needs, refrigeration for bananas, dry storage for machinery, securing for cars, was slow and chaotic, and every port handled it differently.
Then came the standardized shipping container. Suddenly it didn't matter what was inside, coffee or cars: every container was the same size, and any crane, ship, truck, or port in the world could handle it the same way, with the contents isolated and protected. This one change reshaped global shipping.
Docker does the same thing for software. It doesn't matter whether your "cargo" is a Python ML model, a Node.js web app, or a database. Docker packages it into a standard container that runs identically on any machine that has Docker, your laptop, a colleague's Mac, a cloud server, anywhere.
What actually goes wrong without it
An ML project on your machine depends on far more than just your code:
Your ML Project Actually Depends On:
┌────────────────────────────────────────┐
│ Your code (train.py, model.py) │
│ Python 3.11 (specific version!) │
│ scikit-learn 1.4.0 │
│ numpy 1.26.4 │
│ pandas 2.2.0 │
│ System libraries (libgomp, BLAS...) │
│ Operating system (Ubuntu? Mac? Win?) │
│ Environment variables │
└────────────────────────────────────────┘
If even one of these differs on another machine, things break:
| Scenario | What breaks |
|---|---|
| Teammate has Python 3.9, you used 3.11 | Syntax features fail |
| Server has an older numpy | Model unpickling crashes |
| Mac vs. Linux line endings / paths | Scripts fail mysteriously |
Missing system library (e.g. libgomp) | scikit-learn won't import |
| "I forgot to add that to requirements.txt" | Import errors in production |
requirements.txt only captures Python packages. It captures none of the Python version, the OS, or system-level libraries. Docker captures all of it.
Part 2Virtual machines vs. containers
Before Docker, the standard way to get a consistent environment was a virtual machine. Understanding why containers beat VMs for most of this work means understanding the difference.
The apartment building analogy
Think of a physical server as a plot of land. Virtual machines are separate houses on that land: each one has its own complete operating system, its own foundation, plumbing, electrical. Running three applications in isolation means building three entire houses, secure but wasteful, three kitchens and three furnaces even if the apps themselves are small.
Containers are more like apartments in one building. They share the building's foundation and structure, the host OS kernel, but each apartment stays private and isolated. You get isolation without rebuilding the infrastructure for every tenant, so you can fit far more containers than VMs on the same underlying hardware.
| Aspect | Virtual machine | Container |
|---|---|---|
| Size | Gigabytes (full OS) | Megabytes (app + deps) |
| Startup time | Minutes | Seconds, or less |
| Resource use | Heavy | Light |
| Isolation | Complete (own OS) | Process-level (shared kernel) |
| How many per server | A handful | Hundreds |
For ML work, where you're often running many experiments or several model services at once, that lighter weight adds up fast.
Part 3What Docker is, and how it works
Docker is a platform for building, shipping, and running applications inside containers. It's the tool that makes containerization easy and standardized.
The restaurant kitchen analogy
Think of Docker as a system for running standardized meal kits. A Dockerfile is the recipe, written instructions for preparing the meal. A Docker image is the meal kit, a sealed package with everything pre-measured and ready to cook, a template you can make many meals from. A Docker container is the actual cooked meal, one running instance made from that kit; you can cook many containers from one image, each separate from the others. A Docker registry, like Docker Hub, is the meal-kit warehouse, where kits are stored and shared so anyone can grab one.
So the flow runs: write a recipe (Dockerfile) → produce a meal kit (build an image) → cook meals from it (run containers) → store kits in the warehouse (push to a registry) so others can use them too.
Part 4Core Docker concepts, with analogies
These are the vocabulary of Docker. Every piece you'll encounter maps to one of these.
Image
What it is A read-only template with everything needed to run an application: code, runtime, libraries, environment variables, config. Built in layers (more on this in Part 7).
Analogy A blueprint of a house, or a class in programming. You can't live in a blueprint, but you can build many identical houses from it.
docker images # list images on your machine
Container
What it is A running instance of an image, the live, executing version. You can start, stop, and delete containers; many can run from one image, each isolated.
Analogy The actual house built from the blueprint, or an object created from a class. If the image is a cookie cutter, containers are the cookies.
docker ps # list running containers
docker ps -a # list ALL containers, including stopped ones
Dockerfile
What it is A plain text file with step-by-step instructions to build an image. All of Part 7 is dedicated to this.
Analogy A recipe. Follow it and you get the meal kit, the image.
Registry & repository
What it is A storage and distribution system for Docker images. Docker Hub is the default public registry; a repository is a collection of related images, different versions of the same app.
Analogy Docker Hub is like GitHub, but for images instead of code. A repository on it is like a single GitHub repo holding versions.
docker pull python:3.11-slim # download an image from Docker Hub
docker push myusername/myapp # upload your image
Docker volume
What it is A way to store data outside the container, so it survives even after the container itself is deleted. Containers are ephemeral by default; delete one and its internal data goes with it. Volumes fix that.
Analogy An external hard drive. Your computer, the container, can be wiped and rebuilt, but the external drive, the volume, keeps your files safe.
docker volume create mydata
docker run -v mydata:/app/data myimage
For ML this matters a lot: you mount datasets and save trained models to volumes so they survive container restarts.
Docker network
What it is A system that lets containers talk to each other and to the outside world, for example a model API container talking to a database container.
Analogy The internal phone system in an office building, letting different departments (containers) reach each other.
Port mapping
What it is Connecting a port inside the container to a port on your host machine, so you can actually reach a service running inside it.
Analogy A building's internal room number mapped to a public street address. Your API might run on port 5000 inside the container; you map it to port 8000 on your laptop so you can visit localhost:8000.
docker run -p 8000:5000 myimage # host port 8000 → container port 5000
Docker Compose
What it is A tool for defining and running multi-container applications from a single YAML file, instead of running a pile of docker run commands by hand.
Analogy An orchestra conductor. Instead of directing each musician (container) individually, the conductor coordinates all of them from one score, the YAML file.
docker compose up # start every service defined in the compose file
docker compose down # stop them all
Part 5Docker architecture
Understanding what's happening underneath makes Docker much easier to debug. It runs on a client-server architecture.
┌─────────────────────────────────────────────────────────────┐
│ YOUR MACHINE │
│ │
│ ┌─────────────┐ ┌──────────────────────────────┐ │
│ │ Docker CLI │ ──────► │ Docker Daemon (dockerd) │ │
│ │ (Client) │ REST │ │ │
│ │ │ API │ - builds images │ │
│ │ "docker run"│ │ - runs containers │ │
│ └─────────────┘ │ - manages volumes/networks │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ┌───────────┴──────────┐ │
│ │ Images Containers │ │
│ └──────────────────────┘ │
└──────────────────────────────────────┬───────────────────────┘
│ pull / push
▼
┌────────────────────────┐
│ Registry (Docker Hub)│
└────────────────────────┘
Docker Client (the CLI). What you interact with directly. Type docker run or docker build and the client sends that instruction to the daemon. Like the waiter who takes your order.
Docker Daemon (dockerd). The background service doing the actual work: building images, running containers, managing storage and networks. The client just gives it instructions. Like the kitchen and the chef.
Docker Engine. The umbrella term for the whole system, client plus daemon plus the underlying technology. When people say "install Docker," this is what they mean.
Client and daemon talk over a REST API. Usually they're on the same machine, but the daemon could just as easily live on a remote server, with the client simply sending commands over the network.
Part 6Essential Docker commands
Working with images
# download an image from Docker Hub
docker pull python:3.11-slim
# list images on your machine
docker images
# build from a Dockerfile in the current directory
# -t tags (names) the image; "." means "look here for the Dockerfile"
docker build -t my-ml-app:v1 .
# remove an image
docker rmi my-ml-app:v1
# tag an image under another name, e.g. before pushing
docker tag my-ml-app:v1 myusername/my-ml-app:v1
# push to a registry
docker push myusername/my-ml-app:v1
Working with containers
# run a container from an image
docker run my-ml-app:v1
# run interactively with a terminal — good for debugging
docker run -it my-ml-app:v1 /bin/bash
# run detached, in the background
docker run -d my-ml-app:v1
# run with a port mapping and a name
docker run -d -p 8000:5000 --name my-api my-ml-app:v1
# run with a volume mounted
docker run -v $(pwd)/data:/app/data my-ml-app:v1
docker ps # running containers
docker ps -a # all containers, including stopped
docker stop my-api # stop one
docker start my-api # start a stopped one
docker rm my-api # remove — must be stopped first
docker logs my-api # view its logs
docker exec -it my-api /bin/bash # open a shell inside a running container
Cleanup, worth knowing early
Images and containers pile up and eat disk space quietly. These help:
docker container prune # remove all stopped containers
docker image prune # remove all unused images
docker system prune -a # remove everything unused
docker system df # see how much disk Docker is using
Quick reference
| Command | What it does |
|---|---|
docker pull <image> | Download an image |
docker build -t <name> . | Build an image from a Dockerfile |
docker images | List images |
docker run <image> | Create and start a container |
docker ps / docker ps -a | List running / all containers |
docker stop <container> | Stop a container |
docker rm <container> | Delete a container |
docker rmi <image> | Delete an image |
docker logs <container> | View container output |
docker exec -it <c> bash | Open a shell in a running container |
docker push <image> | Upload an image to a registry |
docker system prune -a | Clean up unused resources |
Part 7The Dockerfile: a complete deep dive
This is the heart of Docker. A Dockerfile is a plain text file, literally named Dockerfile, no extension, containing the step-by-step instructions Docker follows to build an image. Understand this well and you've understood Docker.
How it works: the layer system
Each instruction in a Dockerfile creates a layer, a snapshot of whatever that instruction changed. Layers stack on top of each other to form the final image, a bit like a stack of transparent sheets, each adding something to a drawing, or a sandwich built one ingredient at a time.
Docker caches each layer. Rebuild without changing a layer's inputs, and Docker reuses the cached version instead of redoing the work, which is what makes rebuilds fast. That's exactly why the order of instructions in a Dockerfile matters so much, which we'll get to below.
Final Image = sum of all layers
┌─────────────────────────────┐
│ Layer 5: COPY your code │ ← changes often
├─────────────────────────────┤
│ Layer 4: pip install deps │ ← changes sometimes
├─────────────────────────────┤
│ Layer 3: COPY requirements │
├─────────────────────────────┤
│ Layer 2: set working dir │
├─────────────────────────────┤
│ Layer 1: base OS + Python │ ← rarely changes
└─────────────────────────────┘
The core instructions, one at a time
FROM — choose the base image
What it does Every Dockerfile starts with FROM. It sets the base image, the foundation you build on top of. You almost never start from nothing; you start from an existing image, an OS or a Python environment.
Analogy Choosing the foundation and frame of a house before you customize it. Nobody builds from raw materials up.
# start from an official Python image; slim = smaller
FROM python:3.11-slim
ML tip: use
-slimvariants for smaller images. For GPU deep learning, reach for specialized bases likepytorch/pytorchortensorflow/tensorflow:latest-gpu.
WORKDIR — set the working directory
Sets the directory inside the container where later commands run, creating it if it doesn't exist. Everything after it, COPY, RUN, and so on, happens relative to this path. Like saying "let's work in the kitchen" once, instead of repeating the location for every instruction.
WORKDIR /app
Now everything happens inside /app in the container.
COPY — copy files into the image
Copies files or folders from your machine, the "build context," into the image. Like moving ingredients from the grocery bag onto the counter where you'll actually cook.
# COPY <source on your machine> <destination in image>
COPY requirements.txt .
COPY src/ ./src/
There's also
ADD, which behaves likeCOPYbut can also fetch URLs and auto-extract archives. Default toCOPYunless you specifically need one of those extras; it's more predictable.
RUN — execute commands during the build
Runs a command while the image is being built, not when the container later runs. Commonly used for installing dependencies. Each RUN creates a new layer. Like pre-chopping vegetables before cooking.
# install Python dependencies during the build
RUN pip install --no-cache-dir -r requirements.txt
# any shell command works
RUN apt-get update && apt-get install -y libgomp1
Combine related commands with
&&inside oneRUNto keep the layer count down.--no-cache-dirkeeps the image smaller by skipping pip's own download cache.
ENV — set environment variables
Sets environment variables available inside the container. Like posting house rules on the fridge that everyone inside follows.
ENV PYTHONUNBUFFERED=1
ENV MODEL_PATH=/app/models/model.pkl
PYTHONUNBUFFERED=1is a common ML setting: it makes Python print logs immediately instead of buffering them, so output shows up in real time.
EXPOSE — document the port
Documents which port the container listens on. This is informational only; it doesn't actually publish the port. You still need -p at docker run time to map it. Like a sign that says "reception is on floor 5": it tells people where to go, but you still need an actual door.
EXPOSE 5000
CMD — the default command on start
The default command that runs when a container starts. Only one CMD takes effect, and it can be overridden at docker run time. The default action when you press "play."
# run the training script when the container starts
CMD ["python", "src/train.py"]
That
["python", "src/train.py"]JSON-array format is called exec form, and it's the preferred style: the command runs directly as the container's PID 1, so it receives signals like a shutdown request correctly. Shell-form CMD instead runs under/bin/sh -c, which becomes PID 1 itself and often fails to forward that signal along, leading to slow or unclean shutdowns.
ENTRYPOINT — the fixed command
Similar to CMD, but harder to override. ENTRYPOINT sets the main executable; CMD supplies its default arguments. The difference that matters: CMD gets fully replaced if you pass a command to docker run, while ENTRYPOINT always runs, and anything you pass to docker run becomes arguments to it instead.
Think of ENTRYPOINT as a fixed machine, a blender that always blends. CMD supplies the default ingredients, but you can swap them without changing the fact that it blends.
ENTRYPOINT ["python", "src/predict.py"]
CMD ["--input", "default.csv"]
# docker run myimage → python src/predict.py --input default.csv
# docker run myimage --input new.csv → python src/predict.py --input new.csv
For most beginner ML projects, CMD alone is all you need.
.dockerignore — the Docker equivalent of .gitignore
A file named .dockerignore lists what Docker should never copy into the image, keeping builds small and fast. A "do not pack" list for a move: you don't box up the trash.
__pycache__/
*.pyc
.git/
.venv/
data/raw/
*.csv
.ipynb_checkpoints/
README.md
For ML specifically, always exclude large data, virtual environments, and
.git: they bloat the image and add nothing at runtime.
Putting it together: a full annotated Dockerfile
# 1. Start from a slim Python base image
FROM python:3.11-slim
# 2. Set the working directory inside the container
WORKDIR /app
# 3. Environment variables for clean Python behavior
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# 4. System dependencies some ML libs need
RUN apt-get update && \
apt-get install -y --no-install-recommends libgomp1 && \
rm -rf /var/lib/apt/lists/*
# 5. Copy ONLY requirements first — see the caching note below
COPY requirements.txt .
# 6. Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# 7. Copy the rest of the application code
COPY src/ ./src/
# 8. Document the port the app uses
EXPOSE 5000
# 9. Default command when the container starts
CMD ["python", "src/train.py"]
The #1 Dockerfile habit: order for caching
Notice that requirements.txt gets copied and installed before the application code. That's deliberate, and it's the single most important habit in this whole article.
Docker caches layers. Your code changes constantly; your dependencies change rarely. Install dependencies first, and changing your code later reuses the cached dependency layer, rebuilding only the code-copy step, in seconds. Copy everything at once instead, and every code change invalidates the cache and forces a full dependency reinstall, taking minutes instead.
Set up your kitchen and stock the pantry once, slowly, since it rarely changes. Then for each new dish you just swap the fresh ingredients, fast. You don't rebuild the whole kitchen every time you cook.
Summary
You now have a working model of containerization end to end. Containers exist to solve "it works on my machine" by packaging everything an app needs into one portable unit. Compared to virtual machines, they share the host OS kernel instead of each carrying a full guest OS, which is what makes them so much lighter. Docker itself maps cleanly onto the recipe, meal-kit, cooked-meal model: Dockerfile becomes image becomes container. You've got the core vocabulary, images, containers, registries, volumes, networks, ports, Compose, and the architecture underneath, client, daemon, and engine working together. You know the commands you'll actually use day to day, and you've been through the Dockerfile instruction by instruction, including the one habit, ordering for layer caching, that matters more than any other single thing in this article.
Article 5 takes all of this and applies it to a real ML project: writing a Dockerfile for a working model, building the image, running it, and walking through what every single line does in context.