Learning Platform
Глоссарий Troubleshooting
Урок 17.04 · 26 мин
Средний
dockerpythonmulti-stage

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 может:

  1. Найти wheel под твою платформу (psycopg-3.2.3-cp313-cp313-manylinux_2_28_x86_64.whl) — устанавливает за секунды. Native code уже скомпилирован.
  2. Не найти 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"]

Что происходит:

  1. builder: с gcc, libpq-dev, python3-dev. pip wheel --wheel-dir /wheels -r requirements.txt собирает (или скачивает готовые) wheels для всех зависимостей в /wheels.
  2. 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% уходит.

Multi-stage Python: builder + runtime
Stage 1: builderpython:3.13-slim + gcc + libpq-dev + python3-dev. Тяжёлый ~600 МБ
pip wheel /wheelsСкачиваем или собираем wheels для всех зависимостей. Native code компилируется здесь, если нужно
Stage 2: runtimepython:3.13-slim + libpq5 (только runtime libs). 150 МБ база
COPY --from
pip install --no-index /wheelsЛокальная установка без обращения к PyPI. После установки удаляем /wheels — они уже распакованы в site-packages
финальный образТолько runtime libs + site-packages + код. gcc, libpq-dev, headers — не попадают

Runtime libs vs dev libs

Часто в multi-stage путаются: какие пакеты в runtime ставить.

  • libfoo-dev (с header files) нужен для компиляции. В builder.
  • libfoo или libfoo5 (runtime library only) нужен для загрузки .so в Python. В runtime.

Примеры для DE:

ПакетBuilderRuntime
Postgreslibpq-devlibpq5
MySQLdefault-libmysqlclient-devlibmariadb3 (или ничего, если psycopg)
Kafkalibrdkafka-devlibrdkafka1
Изображенияlibjpeg-dev, zlib1g-devlibjpeg62-turbo, zlib1g
XML/JSONlibxml2-dev, libxslt-devlibxml2, 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.

Размеры по подходам
single-stageBuilder и runtime в одном — gcc, dev-libs, кеш всё в финале
multi-stage slimBuilder stage с gcc, runtime stage только с libpq5. Стандартный паттерн
multi-stage + distrolessBuilder slim, runtime distroless. Security premium минус 50 МБ
distroless + uvС uv и оптимизированной .venv. Может ужаться ещё на 30-50 МБ через --exclude-unused

Попробуй сам

  1. Возьми проект с 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"]
    Замерь размер.
  2. Переделай в multi-stage с wheels. Сравни размер.
  3. Переделай в multi-stage с venv copy. Сравни размер и время сборки.
  4. Проверь, что финальный образ работает: docker run --rm <image> python -c "import psycopg; print('OK')". Если падает с libpq.so.5 — забыл libpq5 в runtime stage.
  5. Бонус: попробуй distroless runtime. Должно собраться и запуститься (минус дебаг).

Проверка знанийKnowledge check
Объясни, как multi-stage Dockerfile для Python ETL уменьшает размер образа с 1.2 ГБ до 180 МБ — какие части НЕ попадают в финальный образ и почему?
ОтветAnswer
Single-stage Dockerfile хранит в финальном образе всё, что нужно ТОЛЬКО для сборки: - gcc, g++ (300 МБ) — компилятор C/C++, нужен для сборки native extensions (psycopg, numpy) - libpq-dev, python3-dev (50 МБ) — header files для линковки - build-essential, make (50 МБ) - pip caches в /root/.cache/pip — десятки МБ wheel-файлов - apt-кеши в /var/lib/apt/lists/ — десятки МБ метаданных Эти build-time deps НЕ нужны для запуска приложения. psycopg, numpy уже скомпилированы и установлены в site-packages — gcc и header'ы могут быть удалены без потери функциональности. Multi-stage Pattern: Stage 1 (builder): - python:3.13-slim + gcc + libpq-dev + python3-dev - pip wheel --wheel-dir /wheels -r requirements.txt (или установка в /venv) - Здесь происходит вся «грязная» работа: компиляция native code, сборка wheels Stage 2 (runtime): - python:3.13-slim + libpq5 (runtime-only, БЕЗ -dev) - COPY --from=builder /wheels /wheels (или /venv) - pip install --no-index /wheels/*.whl (или просто ENV PATH=/venv/bin:$PATH) Что не попадает в финальный образ: - gcc и build-essentials (-300 МБ) - libpq-dev header files (заменены на runtime-only libpq5 -50 МБ) - pip caches (-30 МБ) - intermediate build files Финальный 180-220 МБ содержит: Python interpreter + runtime libs (libpq5, libssl) + site-packages с уже-скомпилированными native extensions + наш код. Этого достаточно для работы. Бонус: COPY --from=builder /venv /venv — один COPY переносит все зависимости одним слоем, что лучше для layer cache и distribution.

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. Single-stage Dockerfile с pandas + psycopg весит 1.2 ГБ. После multi-stage с разделением builder/runtime — 220 МБ. Что НЕ попадает в финальный образ и почему?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5