Dockerfile & Multi-stage Builds
Scenario
Your Python Docker image is 1.5GB. It takes 10 minutes to deploy, and a security scanner flags that you shipped a C++ compiler and pip caching artifacts into production.
Mental model
A multi-stage Dockerfile is a factory with two rooms. Room 1 (builder) has all the heavy tools needed to compile dependencies. Room 2 (runtime) is completely empty; you only copy the finished product from Room 1. Room 2 gets shipped.
Deep dive
Docker containers package an application and its dependencies into a single runnable artifact.
A standard Python `Dockerfile` often includes build tools (like `gcc`) required to compile certain packages. If you ship this image, it's bloated and has a large attack surface.
Multi-stage builds solve this. You use a `builder` stage to install dependencies into a virtual environment, then use a slim `runtime` stage where you simply copy the virtual environment over.
Code examples
Single-stage (Bloated)
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
# Caches pip artifacts permanently in the layer
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
This image includes the entire Debian OS, development headers, and `pip` caches. It's huge.
Multi-stage Build (Ground Truth)
# Stage 1: Builder
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install -y gcc
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.11-slim
# Copy only the compiled environment, leave gcc behind
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
The final image drops the `builder` stage completely. It only contains the application code and the clean virtual environment.
Common mistakes
- Baking secrets into the Dockerfile: Never use `ENV SECRET_KEY=1234` in a Dockerfile. Docker caches this in a layer permanently. Anyone with the image can extract the secret. Secrets must be passed at runtime.
Recall questions
- What is the main advantage of a multi-stage Docker build?
- How do you transfer files from a previous stage in a multi-stage Dockerfile?
- Why should you never put API keys in a Dockerfile?
Questions & answers
Why do we create a Python virtual environment (`venv`) inside a Docker container when the container itself is already isolated?
While isolation isn't the issue, a virtual environment makes it extremely easy to move dependencies in a multi-stage build. You can simply `COPY --from=builder /opt/venv /opt/venv` and you've transferred exactly what you need without copying system-level files.
What is the order of operations in a Dockerfile for optimal caching?
Copy `requirements.txt`, run `pip install`, and THEN copy the application code. This ensures that if only application code changes, the dependency installation layer is cached.