> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# A Python App Dockerfile

> A typical Python app Dockerfile, why instruction order matters, and shrinking the image with a multi-stage build.

`Dockerfile` `Python`

Assembling the [instructions](/en/notes/docker/dockerfile/instructions/) into a real, working Python app Dockerfile, where the key is the **order** of instructions.

## A typical Python app

```dockerfile theme={null}
# 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](/en/notes/docker/dockerfile/caching/)). 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:

```dockerfile theme={null}
# 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:

```dockerfile theme={null}
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

* [Choosing a prebuilt image](/en/notes/docker/dockerfile/images/): picking between `python:3.12-slim`, alpine, and the full version.
* [Layer cache and best practices](/en/notes/docker/dockerfile/caching/): the cache mechanism and a copy-ready template.

Reference: [docs.docker.com/guides/python/containerize](https://docs.docker.com/guides/python/containerize/), [multi-stage builds](https://docs.docker.com/build/building/multi-stage/)
