Skip to content

Dockerizing an ML Application

“It works on my machine” is not a deployment strategy. Your model was trained with a specific Python version, a specific scikit-learn version, maybe a specific BLAS library underneath NumPy — and any mismatch on the server can silently change predictions or crash the app outright. Docker solves this by packaging your code, its dependencies, and the runtime it needs into one image that runs identically everywhere: your laptop, a teammate’s laptop, or a cloud VM.

Why Docker helps

Docker packages:

  • code
  • dependencies
  • runtime environment

So it runs the same everywhere.

diagram Diagram mermaid

A simple Dockerfile (FastAPI)

Dockerfile
FROM python:3.11-slim
 
WORKDIR /app
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfile
FROM python:3.11-slim
 
WORKDIR /app
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Building and running the container

Build and run the image
# build the image and tag it "ml-api"
docker build -t ml-api .
 
# run it, mapping container port 8000 to host port 8000
docker run -p 8000:8000 ml-api
 
# in another terminal, confirm it responds
curl http://127.0.0.1:8000/predict -X POST \
  -H "Content-Type: application/json" \
  -d ‘{"age": 35, "income": 50000, "city": "Pune", "plan": "Pro"}’
Build and run the image
# build the image and tag it "ml-api"
docker build -t ml-api .
 
# run it, mapping container port 8000 to host port 8000
docker run -p 8000:8000 ml-api
 
# in another terminal, confirm it responds
curl http://127.0.0.1:8000/predict -X POST \
  -H "Content-Type: application/json" \
  -d ‘{"age": 35, "income": 50000, "city": "Pune", "plan": "Pro"}’

Keep the image lean with .dockerignore

Without one, COPY . .COPY . . also copies your .venv.venv, .git.git history, notebooks, and any large .csv.csv/.joblib.joblib files sitting in the repo — bloating the image and slowing every rebuild. Add a .dockerignore.dockerignore right next to the Dockerfile:

.dockerignore
.git
.venv
__pycache__
*.ipynb
*.csv
tests/
.dockerignore
.git
.venv
__pycache__
*.ipynb
*.csv
tests/

Mount the model as a volume instead of baking it in

Géron’s TF Serving walkthrough mounts the model directory into the container at run time rather than copying it in at build time:

TF Serving mounts the model directory (from the book)
docker run -it --rm -p 8500:8500 -p 8501:8501 \
  -v "$ML_PATH/my_mnist_model:/models/my_mnist_model" \
  -e MODEL_NAME=my_mnist_model \
  tensorflow/serving
TF Serving mounts the model directory (from the book)
docker run -it --rm -p 8500:8500 -p 8501:8501 \
  -v "$ML_PATH/my_mnist_model:/models/my_mnist_model" \
  -e MODEL_NAME=my_mnist_model \
  tensorflow/serving

The same trick works for a Flask/FastAPI image. Instead of COPY model.joblib .COPY model.joblib . baked into the image, mount the artifact from the host (or a shared volume):

Swap model versions without rebuilding the image
docker run -p 8000:8000 \
  -v "$(pwd)/models:/app/models" \
  -e MODEL_PATH=/app/models/model_v2.joblib \
  ml-api
Swap model versions without rebuilding the image
docker run -p 8000:8000 \
  -v "$(pwd)/models:/app/models" \
  -e MODEL_PATH=/app/models/model_v2.joblib \
  ml-api

Now shipping model_v3.joblibmodel_v3.joblib and pointing MODEL_PATHMODEL_PATH at it is a config change, not a new image build — the same “rolling back is as simple as removing the directory” idea Géron describes for TF Serving’s versioned model folders applies just as well to a hand-rolled API image.

Let Docker check the container’s own health

A HEALTHCHECKHEALTHCHECK instruction lets Docker (and orchestrators built on top of it) know when a running container has actually finished starting up and is serving traffic — not just that the process launched:

Dockerfile (with HEALTHCHECK)
FROM python:3.11-slim
 
WORKDIR /app
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8000/health || exit 1
 
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfile (with HEALTHCHECK)
FROM python:3.11-slim
 
WORKDIR /app
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8000/health || exit 1
 
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

docker psdocker ps will now show (healthy)(healthy) or (unhealthy)(unhealthy) next to the container, and tools like Kubernetes or a load balancer can use that status to decide whether to keep sending it traffic.

Running the API and a Streamlit UI together with Compose

Most real setups aren’t a single container — an API and a demo UI often run side by side, sharing the same model artifact. docker-compose.ymldocker-compose.yml lets you describe both services and start them with one command:

docker-compose.yml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - ./models:/app/models
 
  ui:
    build: ./streamlit_app
    ports:
      - "8501:8501"
    depends_on:
      - api
docker-compose.yml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - ./models:/app/models
 
  ui:
    build: ./streamlit_app
    ports:
      - "8501:8501"
    depends_on:
      - api
Start both services
docker compose up --build
# API   -> http://localhost:8000
# UI    -> http://localhost:8501
Start both services
docker compose up --build
# API   -> http://localhost:8000
# UI    -> http://localhost:8501

From one container to many

A single container is fine for a demo, but real traffic needs more than one replica behind a load balancer, plus a way to roll out new image versions without downtime. That’s the job of an orchestrator like Kubernetes — Docker gives you the portable unit, Kubernetes (or a managed platform) gives you the scaling and rollout logic.

diagram Diagram mermaid

tips

  • pin versions in requirements.txtrequirements.txt
  • keep images small (slimslim base)
  • don’t bake secrets into images

Mini-checkpoint

Why is Docker useful for ML specifically?

  • ML dependencies are often heavy and version-sensitive.

🧪 Try It Yourself

Exercise 1 – Build the docker run Command

Exercise 2 – Read the PORT from the Environment

Exercise 3 – Tag an Image with a Version

Next

Continue to Monitoring Model Drift — once your model is packaged and running somewhere, the next job is watching it stay accurate over time.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did