Multi-stage для Python
В модуле 8 мы разобрали multi-stage builds в общем виде. В этом уроке — конкретный паттерн для Python: разделение сборки (где есть gcc, libpq-dev, всякие build-tools) и runtime (где только Python + готовые пакеты). Это типично уменьшает образ DE-приложения с ~1 ГБ до ~180-250 МБ.
Дистрибутивы Linux: Debian, RHEL, Arch, Alpine
Зачем нужен multi-stage для Python
Многие Python-пакеты содержат native code: C/C++/Fortran/Rust extensions. Когда pip install psycopg, pip может:
- Найти wheel под твою платформу (
psycopg-3.2.3-cp313-cp313-manylinux_2_28_x86_64.whl) — устанавливает за секунды. Native code уже скомпилирован. - Не найти wheel — пытается собрать из исходников (
pip install --no-binary :all:или просто отсутствие wheel). Это требует gcc, libpq-dev, python3-dev, и занимает минуты.
Для DE-стека (psycopg, pyarrow, numpy, pandas, confluent-kafka) wheels почти всегда есть для python:3.13-slim. Но иногда:
- Сборка-only либы (например,
cython) нужны только во время сборки. - Header-files (
libpq-dev,python3-dev) нужны только для компиляции extensions. - Build-time зависимости (
setuptools-scm,pip-tools) не нужны в runtime.
Single-stage Dockerfile несёт всё это в финальный образ — лишние ~500 МБ.
Single-stage: что внутри
FROM python:3.13-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
docker images:
my-app latest abc123 1.1GB
Содержит:
- Python (50 МБ).
- gcc + build-essentials (300 МБ) — не нужны для runtime.
- libpq-dev + header files (50 МБ) — не нужны (нужен только libpq для runtime).
- pip caches in /root/.cache (если не —no-cache-dir) — не нужны.
- site-packages (300 МБ) — нужны.
- наш код (1 МБ) — нужен.
Раздуто из-за build-tools.
Multi-stage с wheels
Идея: в builder стадии устанавливаем build-tools, собираем все зависимости в wheel-файлы. В runtime стадии копируем wheels и устанавливаем их без build-tools.
FROM python:3.13-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --wheel-dir /wheels -r requirements.txt
FROM python:3.13-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/*.whl \
&& rm -rf /wheels
COPY . .
CMD ["python", "main.py"]
Что происходит:
- builder: с gcc, libpq-dev, python3-dev.
pip wheel --wheel-dir /wheels -r requirements.txtсобирает (или скачивает готовые) wheels для всех зависимостей в/wheels. - runtime: только runtime-библиотеки (
libpq5, неlibpq-dev). Копируем wheels черезCOPY --from=builder.pip install --no-index --find-links=/wheelsустанавливает локально без обращения к PyPI. Удаляем wheels после установки.
Результат:
my-app latest def456 220MB
Сократили с 1.1 ГБ до 220 МБ. 80% уходит.
Runtime libs vs dev libs
Часто в multi-stage путаются: какие пакеты в runtime ставить.
- libfoo-dev (с header files) нужен для компиляции. В builder.
- libfoo или libfoo5 (runtime library only) нужен для загрузки .so в Python. В runtime.
Примеры для DE:
| Пакет | Builder | Runtime |
|---|---|---|
| Postgres | libpq-dev | libpq5 |
| MySQL | default-libmysqlclient-dev | libmariadb3 (или ничего, если psycopg) |
| Kafka | librdkafka-dev | librdkafka1 |
| Изображения | libjpeg-dev, zlib1g-dev | libjpeg62-turbo, zlib1g |
| XML/JSON | libxml2-dev, libxslt-dev | libxml2, libxslt1.1 |
Если runtime fail с ImportError: libpq.so.5: cannot open shared object file — не доставили runtime-либу. apt-get install libpq5 в runtime стадии.
Альтернатива: .venv копирование
Вместо wheels можно копировать .venv целиком:
FROM python:3.13-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
RUN python -m venv /venv
ENV PATH=/venv/bin:$PATH
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.13-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /venv /venv
ENV PATH=/venv/bin:$PATH
WORKDIR /app
COPY . .
CMD ["python", "main.py"]
Сравнение wheels vs .venv:
| wheels | .venv | |
|---|---|---|
| Финальный образ | install из wheels на runtime | готовый /venv copy’нут |
| Время сборки runtime stage | ~30 сек (pip install) | ~5 сек (COPY) |
| Воспроизводимость | wheels — иммутабельные артефакты | .venv может иметь absolute paths |
| Сложность | две команды (wheel + install) | один venv |
Для большинства DE-проектов .venv проще: один COPY вместо wheel + install. Wheels оправданы, если хочешь wheels-as-artifact (например, использовать те же wheels в нескольких runtime).
С uv это ещё проще
uv делает multi-stage ещё чище:
# syntax=docker/dockerfile:1.7
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
ENV UV_PROJECT_ENVIRONMENT=/venv
RUN uv venv /venv
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --python /venv/bin/python -r requirements.txt
FROM python:3.13-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /venv /venv
ENV PATH=/venv/bin:$PATH
WORKDIR /app
COPY . .
CMD ["python", "main.py"]
Это идиоматичный 2026-pattern: BuildKit cache + uv + multi-stage venv.
Тестирование, что multi-stage не сломал ничего
Стандартный финальный тест: контейнер собран, давай проверим что он:
docker build -t my-app:multi .
# 1. Размер
docker images my-app:multi
# 220MB ОК
# 2. Запуск
docker run --rm my-app:multi python -c "import pandas; import psycopg; print('OK')"
# OK
# 3. Не падает на runtime libs
docker run --rm my-app:multi python -c "import psycopg; psycopg.connect('postgresql://test:test@localhost:5432/test')"
# OperationalError ОК (нет постгреса, но ImportError'а libpq.so.5 нет — а это что мы и проверяли)
Если ImportError: libpq.so.5: cannot open shared object file — не доставили libpq5 в runtime stage.
Реальный bench: 1.2 ГБ -> 180 МБ
Проект: airflow + pandas + sqlalchemy + psycopg + pyarrow + dbt-postgres.
Single-stage с gcc + dev-libs в финале: 1.2 ГБ.
Multi-stage с правильным разделением: 180 МБ.
Сокращение в 6.5x. Время CI pull/deploy сокращается пропорционально (180 МБ качается за 5 секунд, 1.2 ГБ — за 25 секунд на каждый node).
Размер по слоям
docker history my-app:multi
Видишь, сколько весит каждый слой. Для multi-stage финальный:
SIZE CREATED BY
30MB COPY . . # buildkit
180MB COPY --from=builder /venv /venv # buildkit ← главная масса
8MB RUN /bin/sh -c apt-get update ... libpq5 ... # buildkit
2MB RUN useradd ...
...
180 МБ в COPY —from=builder /venv — это site-packages со всеми зависимостями. Чтобы ещё уменьшить — нужно либо удалять unused (uv --exclude-unused), либо использовать distroless для runtime.
Multi-stage с distroless для maximum compression
# syntax=docker/dockerfile:1.7
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN apt-get update && apt-get install -y --no-install-recommends gcc libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
RUN uv venv /venv
ENV PATH=/venv/bin:$PATH
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install -r requirements.txt
FROM gcr.io/distroless/python3-debian12 AS runtime
COPY --from=builder /venv /venv
ENV PATH=/venv/bin:$PATH
WORKDIR /app
COPY . .
USER nonroot
ENTRYPOINT ["python", "main.py"]
Распределение:
- distroless python3-debian12: ~50 МБ.
- /venv с зависимостями: 180 МБ.
- Наш код: 1 МБ.
Итого: ~230 МБ. Немного больше чем slim-runtime (220 МБ), но огромная security premium.
Попробуй сам
- Возьми проект с
pip install psycopg pandas. Создай single-stage Dockerfile:
Замерь размер.FROM python:3.13-slim RUN apt-get update && apt-get install -y gcc libpq-dev && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "main.py"] - Переделай в multi-stage с wheels. Сравни размер.
- Переделай в multi-stage с venv copy. Сравни размер и время сборки.
- Проверь, что финальный образ работает:
docker run --rm <image> python -c "import psycopg; print('OK')". Если падает с libpq.so.5 — забылlibpq5в runtime stage. - Бонус: попробуй distroless runtime. Должно собраться и запуститься (минус дебаг).