Skip to main content
Dockerfile Python Assembling the instructions into a real, working Python app Dockerfile, where the key is the order of instructions.

A typical Python app

# syntax=docker/dockerfile:1
FROM python:3.12-slim

WORKDIR /app

# 1. Copy only the dependency manifest first
COPY requirements.txt .

# 2. Install dependencies (this layer rebuilds only when requirements.txt changes)
RUN pip install --no-cache-dir -r requirements.txt

# 3. Copy the source last (editing a .py does not force pip install to rerun)
COPY . .

EXPOSE 8000
CMD ["python", "app.py"]
Why COPY requirements.txt before COPY . .? The layer cache rule is “when a layer changes, every layer after it rebuilds” (full mechanism in layer cache). Source code changes far more often than the dependency manifest. If you COPY . . then pip install, every .py edit forces the whole pip install to rerun (slow). Copying and installing the manifest separately means the pip install layer only rebuilds when requirements.txt actually changes. pip install --no-cache-dir keeps the downloaded wheels out of the image layer, shrinking it.

Shrinking with multi-stage

For a smaller image, use a multi-stage build: install dependencies into a venv in the builder stage, and have the final stage copy only the venv so build tools do not end up in the final image:
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
RUN --mount=type=cache,target=/root/.cache/pip \
    --mount=type=bind,source=requirements.txt,target=requirements.txt \
    pip install -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY . .
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
  • FROM ... AS builder names a build stage; COPY --from=builder copies its output.
  • --mount=type=cache,target=/root/.cache/pip persists the pip download cache across builds (not in an image layer), so rebuilds do not re-download.
  • The final image holds only the runtime and venv, no build tools, and is noticeably smaller.
When you need to compile native packages, add build tools in the builder stage; they do not reach the final image:
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential libpq-dev \
    && rm -rf /var/lib/apt/lists/*
# ... pip install ...

Next

Reference: docs.docker.com/guides/python/containerize, multi-stage builds