Главная/Python внутри Container/Урок

6.1. Выбор base image

Цели

После этого материала вы сможете:

  • объяснить различие вариантов официального образа Python: полный, slim, alpine;
  • объяснить, что такое glibc и musl и как это влияет на установку Python-пакетов;
  • обосновать измерением, почему Alpine для Python часто оказывается хуже, а не лучше;
  • выбрать вариант образа под конкретный проект и защитить выбор;
  • оценить distroless и понять, за что вы платите его использованием;
  • выбрать версию Python и спланировать её обновление.

Предварительные знания

Ключевые термины

ТерминОбъяснение
glibcGNU C Library — стандартная библиотека C в Debian, Ubuntu, RHEL
muslАльтернативная реализация библиотеки C, используемая в Alpine
wheelГотовый к установке бинарный пакет Python, формат .whl
manylinuxСтандарт совместимости бинарных wheels для дистрибутивов на glibc
musllinuxАналогичный стандарт для систем на musl
sdistПакет с исходным кодом; требует компиляции при установке
distrolessОбраз без оболочки, пакетного менеджера и системных утилит

Теория

Варианты официального образа

Docker Hub предлагает несколько вариантов образа python, различающихся базой и составом.

ВариантБазаРазмерЧто внутри
python:3.13Debian + buildpack-deps~1.0 GBКомпиляторы, заголовки, git, curl, множество библиотек разработки
python:3.13-slimDebian, минимальный набор~126 MBТолько то, что нужно для работы Python
python:3.13-alpineAlpine Linux~55 MBPython на musl, BusyBox вместо GNU-утилит

Разница между полным вариантом и slim — почти 900 MB инструментов сборки. Они нужны, если вы компилируете пакеты из исходников, и не нужны, если ставите готовые wheels.

Разница между slim и alpine — 70 MB, и именно вокруг неё возникает самое частое заблуждение.

Почему Alpine соблазнителен

Аргумент выглядит убедительно: образ втрое меньше, значит быстрее загрузка, меньше место на диске, меньше поверхность атаки.

Для языков со статической линковкой — Go, Rust — это работает. Для Python обычно нет, и причина в библиотеке C.

glibc против musl

Python-пакеты с расширениями на C (numpy, pandas, psycopg2, cryptography, lxml, pillow) содержат скомпилированный код. Он линкуется с библиотекой C операционной системы.

Debian и Ubuntu используют glibc. Alpine использует musl — другую реализацию с тем же интерфейсом, но иным поведением в деталях.

Официальная документация образа Python формулирует это прямо:

it does use musl libc instead of glibc and friends, so software will often run into issues depending on the depth of their libc requirements/assumptions

Практическое следствие связано с форматом wheels:

text
   PyPI хранит для пакета:

   numpy-2.4.0-cp313-cp313-manylinux_2_17_x86_64.whl   ← для glibc
   numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl    ← для musl
   numpy-2.4.0.tar.gz                                   ← исходники

Стандарт manylinux появился в 2016 году и поддерживается практически всеми пакетами. Стандарт musllinux — в 2021, и покрытие у него заметно ниже.

Когда подходящего wheel нет, pip скачивает исходники и компилирует их на месте. Для этого нужны компилятор, заголовочные файлы Python и заголовки всех системных зависимостей.

Что происходит на практике

text
   python:3.13-slim + pip install pandas
   ───────────────────────────────────────
   1. pip находит manylinux-wheel
   2. скачивает 12 MB
   3. распаковывает
   ⏱ 8 секунд, образ +45 MB

   python:3.13-alpine + pip install pandas
   ───────────────────────────────────────
   1. musllinux-wheel может отсутствовать
   2. pip скачивает sdist
   3. нужны gcc, g++, musl-dev, python3-dev, ...  (+200 MB)
   4. компиляция C и C++ расширений
   ⏱ 4–15 минут, образ +250 MB

Итог: образ на Alpine оказывается больше и собирается в десятки раз дольше. Экономия 70 MB на базе перекрывается 200 MB инструментов сборки.

Даже если использовать multi-stage и выбросить компиляторы, время сборки остаётся, а скомпилированные под musl расширения обычно медленнее: аллокатор musl оптимизирован под малый расход памяти, а не под скорость.

Когда Alpine оправдан

Не всегда всё плохо — есть случаи, где Alpine выигрывает.

СитуацияОценка
Приложение на чистом Python без бинарных расширенийAlpine даёт реальную экономию
Все нужные пакеты имеют musllinux-wheelsРаботает; проверьте заранее
Жёсткое ограничение на размер образаМожет оправдать затраты на сборку
Есть numpy, pandas, scipyПочти всегда плохой выбор
Нужна максимальная скорость сборкиПлохой выбор
Команда не готова отлаживать проблемы muslПлохой выбор

Проверить наличие musllinux-wheel можно до принятия решения — способ показан в практической части.

Distroless

Образы без оболочки, пакетного менеджера и системных утилит. В них только интерпретатор Python и библиотеки.

slimdistroless
Размер~126 MB~50 MB
Оболочкаестьнет
Пакетный менеджерестьнет
docker exec ... shработаетне работает
Установка пакетов в runtimeвозможнаневозможна
Поверхность атакибольшеменьше
Отладкапривычнаятребует nsenter или временного container

Distroless — логичное завершение multi-stage: зависимости ставятся в стадии сборки, в финальный образ копируется только .venv.

Цена — отладка. Приёмы работы с container без оболочки разбирались в уроке 4.3: nsenter, --pid=container:, /proc/<pid>/root. Они работают, но требуют навыка.

Рекомендация курса: осваивайте distroless после того, как уверенно диагностируете обычные образы. Преждевременный переход превращает мелкую проблему в тупик.

Выбор версии Python

Курс использует 3.13, хотя на момент написания доступна 3.14 и именно она является тегом по умолчанию.

Обоснование — не консерватизм, а конкретный риск: пакеты с бинарными расширениями получают wheels для новой минорной версии с задержкой от недель до месяцев. Пока их нет, pip компилирует из исходников — со всеми последствиями, описанными выше, но уже на glibc.

Практическое правило:

ВерсияКогда выбирать
Текущая минус одна (3.13 при вышедшей 3.14)Значение по умолчанию для production
Текущая (3.14)Если проверили, что все зависимости имеют wheels
Минус две и старшеТолько при явном требовании совместимости

Всегда указывайте минорную версию явно: python:3.13-slim, а не python:3-slim и тем более не python:latest. Иначе минорная версия сменится незаметно.

Полное закрепление — тег плюс digest (урок 5.9).


Внутренний механизм

Как pip выбирает пакет

При установке pip определяет набор поддерживаемых тегов платформы и ищет соответствующий файл на PyPI.

Порядок предпочтения:

  1. Wheel, точно соответствующий версии Python, ABI и платформе.
  2. Wheel для совместимой платформы (manylinux или musllinux подходящей версии).
  3. Универсальный wheel (py3-none-any) — для пакетов на чистом Python.
  4. Исходники (sdist) — требуют компиляции.

Посмотреть теги своей платформы можно командой pip debug --verbose. Именно их сравнение с именами файлов на PyPI определяет, будет компиляция или нет.

Почему slim меньше полного варианта

Полный образ основан на buildpack-deps, который включает заголовочные файлы десятков библиотек, компиляторы, git, curl, mercurial и прочие инструменты. Это удобно для сборки чего угодно без доустановки.

slim содержит только пакеты, необходимые для запуска Python. Если пакету при установке потребуются заголовки — например, libpq-dev для psycopg2 — их придётся установить явно, и это правильно: вы контролируете состав образа.


Команды и примеры

Подготовка

bash
mkdir -p /tmp/base-images && cd /tmp/base-images

Сравнение размеров

bash
# docker pull принимает РОВНО один образ:
# `docker pull a b` отвечает «docker pull requires 1 argument»
for img in python:3.13 python:3.13-slim python:3.13-alpine; do docker pull -q "$img"; done
docker images python --format 'table {{.Tag}}\t{{.Size}}' | grep -E '3.13|TAG'
text
TAG          SIZE
3.13-alpine  70.1MB
3.13-slim    178MB
3.13         1.62GB

Это DISK USAGE — сколько образ занимает на узле. docker image inspect вернёт вчетверо меньшие числа: 17 MB, 41 MB и 394 MB, потому что считает сжатые blob'ы (урок 3.4). Соотношение между вариантами от этого не меняется, и решение принимается по нему.

Что внутри полного варианта сверх slim:

bash
docker run --rm python:3.13 sh -c 'command -v gcc g++ make git curl 2>/dev/null' | sed 's/^/  /'
echo "--- в slim ---"
docker run --rm python:3.13-slim sh -c 'command -v gcc g++ make git curl 2>/dev/null' | sed 's/^/  /' || echo "  ничего из перечисленного нет"
text
  /usr/bin/gcc
  /usr/bin/g++
  /usr/bin/make
  /usr/bin/git
  /usr/bin/curl
--- в slim ---
  ничего из перечисленного нет

Проверка библиотеки C

bash
echo "=== slim (Debian) ==="
docker run --rm python:3.13-slim sh -c 'ldd --version 2>&1 | head -1'
docker run --rm python:3.13-slim cat /etc/os-release | grep PRETTY

echo "=== alpine ==="
docker run --rm python:3.13-alpine sh -c 'ldd 2>&1 | head -1'
docker run --rm python:3.13-alpine cat /etc/os-release | grep PRETTY
text
=== slim (Debian) ===
ldd (Debian GLIBC 2.41-12) 2.41
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
=== alpine ===
musl libc (x86_64)
PRETTY_NAME="Alpine Linux v3.22"

Разные реализации библиотеки C — источник всех различий, разбираемых в этом уроке.

Какие теги платформы видит pip

bash
echo "=== slim ==="
docker run --rm python:3.13-slim pip debug --verbose 2>/dev/null \
    | grep -A4 'Compatible tags' | head -5

echo "=== alpine ==="
docker run --rm python:3.13-alpine pip debug --verbose 2>/dev/null \
    | grep -A4 'Compatible tags' | head -5
text
=== slim ===
Compatible tags: 42
  cp313-cp313-manylinux_2_41_x86_64
  cp313-cp313-manylinux_2_40_x86_64
  ...
=== alpine ===
Compatible tags: 30
  cp313-cp313-musllinux_1_2_x86_64
  cp313-cp313-musllinux_1_1_x86_64
  ...

Именно эти теги pip сопоставляет с именами файлов на PyPI.

Проверка наличия wheel до принятия решения

Полезный приём: узнать, есть ли готовый пакет для Alpine, не собирая образ.

bash
check_wheel() {
    local pkg="$1"
    python3 - "$pkg" <<'PY'
import json, sys, urllib.request

pkg = sys.argv[1]
with urllib.request.urlopen(f"https://pypi.org/pypi/{pkg}/json", timeout=10) as r:
    data = json.load(r)

version = data["info"]["version"]
files = data["releases"][version]

many = [f for f in files if "manylinux" in f["filename"] and "x86_64" in f["filename"]]
musl = [f for f in files if "musllinux" in f["filename"] and "x86_64" in f["filename"]]
pure = [f for f in files if f["filename"].endswith("py3-none-any.whl")]

print(f"  {pkg} {version}")
if pure:
    print("    чистый Python — работает везде одинаково")
else:
    print(f"    manylinux (Debian/slim): {'есть' if many else 'НЕТ'}")
    print(f"    musllinux (Alpine):      {'есть' if musl else 'НЕТ — будет компиляция'}")
PY
}

for p in fastapi pydantic numpy pandas psycopg2-binary; do check_wheel "$p"; done
text
  fastapi 0.141.1
    чистый Python — работает везде одинаково
  pydantic 2.13.4
    manylinux (Debian/slim): есть
    musllinux (Alpine):      есть
  numpy 2.4.0
    manylinux (Debian/slim): есть
    musllinux (Alpine):      есть
  pandas 2.3.4
    manylinux (Debian/slim): есть
    musllinux (Alpine):      есть
  psycopg2-binary 2.9.11
    manylinux (Debian/slim): есть
    musllinux (Alpine):      НЕТ — будет компиляция

Выполните проверку для своих зависимостей до выбора базового образа. Одна строка «НЕТ» может стоить пятнадцати минут сборки.

Измерение: slim против alpine на пакете с компиляцией

Возьмём psycopg2 — он собирается из исходников на обеих платформах, что делает сравнение честным.

bash
cat > requirements.txt <<'EOF'
psycopg2==2.9.11
EOF

cat > Dockerfile.slim <<'EOF'
FROM python:3.13-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends gcc libpq-dev python3-dev \
 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "-c", "import psycopg2; print('ok')"]
EOF

cat > Dockerfile.alpine <<'EOF'
FROM python:3.13-alpine
RUN apk add --no-cache gcc musl-dev postgresql-dev python3-dev
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "-c", "import psycopg2; print('ok')"]
EOF

for v in slim alpine; do
    s="$(date +%s)"
    docker build -q --no-cache -f "Dockerfile.$v" -t base:"$v" . > /dev/null
    e="$(date +%s)"
    printf '%-8s время: %3s c   размер: %s\n' "$v" "$((e - s))" \
        "$(docker images base:"$v" --format '{{.Size}}')"
done
text
slim     время:  47 c   размер: 289MB
alpine   время:  93 c   размер: 218MB

Здесь Alpine дал меньший образ ценой вдвое большего времени сборки. Но образ содержит компиляторы — уберём их через multi-stage:

bash
cat > Dockerfile.slim-ms <<'EOF'
# syntax=docker/dockerfile:1
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/*
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.13-slim
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 \
 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
CMD ["python", "-c", "import psycopg2; print('ok')"]
EOF

cat > Dockerfile.alpine-ms <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-alpine AS builder
RUN apk add --no-cache gcc musl-dev postgresql-dev python3-dev
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.13-alpine
RUN apk add --no-cache libpq
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
CMD ["python", "-c", "import psycopg2; print('ok')"]
EOF

for v in slim-ms alpine-ms; do
    docker build -q -f "Dockerfile.$v" -t base:"$v" . > /dev/null
    printf '%-12s %s\n' "$v" "$(docker images base:"$v" --format '{{.Size}}')"
done
docker run --rm base:slim-ms
docker run --rm base:alpine-ms
text
slim-ms      146MB
alpine-ms    73.1MB
ok
ok

С multi-stage Alpine выигрывает вдвое по размеру. Это честный результат — и он показывает, что вопрос не в «Alpine хуже», а в том, что именно вы измеряете.

Случай, где Alpine проигрывает

Теперь пакет без musllinux-wheel:

bash
cat > requirements2.txt <<'EOF'
psycopg2-binary==2.9.11
EOF

cat > Dockerfile.slim2 <<'EOF'
FROM python:3.13-slim
COPY requirements2.txt .
RUN pip install --no-cache-dir -r requirements2.txt
CMD ["python", "-c", "import psycopg2; print('ok')"]
EOF

cat > Dockerfile.alpine2 <<'EOF'
FROM python:3.13-alpine
RUN apk add --no-cache gcc musl-dev postgresql-dev python3-dev
COPY requirements2.txt .
RUN pip install --no-cache-dir -r requirements2.txt
CMD ["python", "-c", "import psycopg2; print('ok')"]
EOF

for v in slim2 alpine2; do
    s="$(date +%s)"
    docker build -q --no-cache -f "Dockerfile.$v" -t base:"$v" . > /dev/null 2>&1
    e="$(date +%s)"
    printf '%-8s время: %3s c   размер: %s\n' "$v" "$((e - s))" \
        "$(docker images base:"$v" --format '{{.Size}}')"
done
text
slim2    время:   9 c   размер: 148MB
alpine2  время: 118 c   размер: 271MB

Вот проблема в чистом виде: у psycopg2-binary есть manylinux-wheel и нет musllinux. На slim — 9 секунд и готовый бинарник. На Alpine — почти две минуты компиляции и образ, который вдвое больше.

Разница в 13 раз по времени. В CI, где сборка идёт при каждом коммите, это ощутимо.

Distroless

bash
cat > app.py <<'PY'
import sys

print(f"Python {sys.version.split()[0]}")
print("работает в distroless")
PY

cat > Dockerfile.distroless <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS builder
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt* /tmp/
RUN pip install --no-cache-dir fastapi==0.141.1

FROM gcr.io/distroless/python3-debian12
COPY --from=builder /opt/venv/lib/python3.11/site-packages /app/site-packages
COPY app.py /app/app.py
ENV PYTHONPATH=/app/site-packages
WORKDIR /app
CMD ["app.py"]
EOF

echo "Образ distroless (сборка может не пройти при несовпадении версий Python):"
docker build -q -f Dockerfile.distroless -t base:distroless . > /dev/null 2>&1 \
    && docker images base:distroless --format '  размер: {{.Size}}' \
    || echo "  пропущено: версия Python в distroless отличается от builder"

Ключевое ограничение distroless проверяется мгновенно:

bash
docker run --rm --entrypoint sh python:3.13-slim -c 'echo "в slim оболочка есть"'
docker run --rm --entrypoint sh gcr.io/distroless/python3-debian12 -c 'echo x' 2>&1 | tail -1
text
в slim оболочка есть
docker: Error response from daemon: failed to create task for container: ... exec: "sh": executable file not found in $PATH

Оболочки нет — docker exec ... sh работать не будет. Диагностика возможна только приёмами из урока 4.3.

Distroless для Python требует внимания к версии интерпретатора: путь site-packages привязан к минорной версии, и она должна совпадать между стадией сборки и distroless-образом. Это одна из причин, по которой distroless сложнее в поддержке.

Проверка выбранного образа

bash
docker run --rm python:3.13-slim python -VV
docker run --rm python:3.13-slim sh -c 'cat /etc/os-release | grep VERSION='
text
Python 3.13.11 (main, ...) [GCC 14.2.0]
VERSION="13 (trixie)"

Флаг -VV показывает полную версию с компилятором — полезно при разборе проблем с бинарными расширениями.

Уборка

bash
cd /tmp
docker rmi -f $(docker images -q --filter 'reference=base:*') 2>/dev/null || true
docker rmi python:3.13 python:3.13-alpine 2>/dev/null || true
rm -rf /tmp/base-images

Практическое упражнение

Задание. Напишите скрипт choose-base.sh, который по файлу зависимостей рекомендует базовый образ.

Скрипт должен:

  1. Прочитать requirements.txt и извлечь имена пакетов.
  2. Для каждого пакета запросить PyPI и определить: чистый Python, есть ли manylinux, есть ли musllinux.
  3. Выдать рекомендацию: alpine подходит, slim предпочтителен, или Alpine противопоказан.
  4. Перечислить пакеты, которые будут компилироваться на Alpine.
  5. Оценить ожидаемое влияние на время сборки.

Проверьте скрипт на двух наборах: чистый Python и набор с бинарными расширениями.

Подсказки

Подсказка 1

JSON API PyPI: https://pypi.org/pypi/<package>/json. Список файлов релиза — в releases[version].

Подсказка 2

Универсальный wheel имеет имя, оканчивающееся на py3-none-any.whl. Такой пакет одинаково работает везде.

Подсказка 3

Имя пакета в requirements.txt нужно отделить от версии и extras: uvicorn[standard]==0.52.0uvicorn.

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/usr/bin/env bash
# choose-base.sh — рекомендация базового образа по файлу зависимостей.
set -uo pipefail

REQ="${1:-requirements.txt}"
[ -r "$REQ" ] || { echo "файл не найден: $REQ" >&2; exit 2; }

python3 - "$REQ" <<'PY'
import json
import re
import sys
import urllib.error
import urllib.request

req_path = sys.argv[1]

# Извлекаем имена пакетов: отбрасываем версии, extras, комментарии
names = []
for line in open(req_path, encoding="utf-8"):
    line = line.strip()
    if not line or line.startswith(("#", "-")):
        continue
    name = re.split(r"[<>=!~\[;]", line)[0].strip()
    if name:
        names.append(name)

if not names:
    print("в файле нет пакетов")
    raise SystemExit(0)

pure, both, glibc_only, unknown = [], [], [], []

for name in names:
    try:
        with urllib.request.urlopen(
            f"https://pypi.org/pypi/{name}/json", timeout=15
        ) as resp:
            data = json.load(resp)
    except (urllib.error.URLError, json.JSONDecodeError, TimeoutError):
        unknown.append(name)
        continue

    version = data["info"]["version"]
    files = data["releases"].get(version, [])
    fnames = [f["filename"] for f in files]

    if any(f.endswith("py3-none-any.whl") or f.endswith("-any.whl") for f in fnames):
        pure.append((name, version))
        continue

    has_many = any("manylinux" in f and ("x86_64" in f or "aarch64" in f) for f in fnames)
    has_musl = any("musllinux" in f and ("x86_64" in f or "aarch64" in f) for f in fnames)

    if has_many and has_musl:
        both.append((name, version))
    elif has_many:
        glibc_only.append((name, version))
    else:
        unknown.append(name)

print(f"═══ Анализ {req_path}: {len(names)} пакетов ═══\n")

if pure:
    print(f"Чистый Python ({len(pure)}) — работают везде одинаково:")
    for n, v in pure:
        print(f"  {n}=={v}")
    print()

if both:
    print(f"Бинарные, wheels для обеих платформ ({len(both)}):")
    for n, v in both:
        print(f"  {n}=={v}")
    print()

if glibc_only:
    print(f"Бинарные, ТОЛЬКО manylinux ({len(glibc_only)}):")
    for n, v in glibc_only:
        print(f"  {n}=={v}   ← на Alpine будет компилироваться")
    print()

if unknown:
    print(f"Не удалось определить ({len(unknown)}):")
    for n in unknown:
        print(f"  {n}")
    print()

print("═══ Рекомендация ═══\n")

binary_count = len(both) + len(glibc_only)

if glibc_only:
    print("  python:3.13-slim")
    print()
    print(f"  Причина: {len(glibc_only)} пакетов не имеют musllinux-wheel.")
    print("  На Alpine они будут компилироваться из исходников:")
    print("    - время сборки вырастет на 2-15 минут на пакет;")
    print("    - потребуются gcc, musl-dev и заголовки (+200 MB в стадии сборки);")
    print("    - возможны ошибки компиляции, специфичные для musl.")
elif binary_count == 0:
    print("  python:3.13-alpine подойдёт")
    print()
    print("  Причина: все зависимости — чистый Python.")
    print("  Экономия около 70 MB без риска компиляции.")
    print("  python:3.13-slim тоже корректен, если размер некритичен.")
else:
    print("  python:3.13-slim (по умолчанию) или python:3.13-alpine")
    print()
    print(f"  Причина: все {binary_count} бинарных пакетов имеют wheels для обеих платформ.")
    print("  Alpine даст меньший образ; slim — быстрее и привычнее в отладке.")
    print("  Решайте измерением на своём проекте.")

print()
print("  Проверьте выбор фактической сборкой обоих вариантов:")
print("    docker build -f Dockerfile.slim -t app:slim .")
print("    docker build -f Dockerfile.alpine -t app:alpine .")
PY

Проверка на двух наборах:

bash
chmod +x choose-base.sh

# набор 1: чистый Python
cat > pure.txt <<'EOF'
fastapi==0.141.1
httpx==0.28.1
EOF
./choose-base.sh pure.txt

echo
echo "════════════════════════════════"
echo

# набор 2: с бинарными расширениями
cat > binary.txt <<'EOF'
fastapi==0.141.1
psycopg2-binary==2.9.11
pandas==2.3.4
EOF
./choose-base.sh binary.txt

Ожидаемый вывод для первого набора:

text
═══ Анализ pure.txt: 2 пакетов ═══

Чистый Python (2) — работают везде одинаково:
  fastapi==0.141.1
  httpx==0.28.1

═══ Рекомендация ═══

  python:3.13-alpine подойдёт

  Причина: все зависимости — чистый Python.
  Экономия около 70 MB без риска компиляции.
  python:3.13-slim тоже корректен, если размер некритичен.

Для второго:

text
═══ Анализ binary.txt: 3 пакетов ═══

Чистый Python (1) — работают везде одинаково:
  fastapi==0.141.1

Бинарные, wheels для обеих платформ (1):
  pandas==2.3.4

Бинарные, ТОЛЬКО manylinux (1):
  psycopg2-binary==2.9.11   ← на Alpine будет компилироваться

═══ Рекомендация ═══

  python:3.13-slim

  Причина: 1 пакетов не имеют musllinux-wheel.
  На Alpine они будут компилироваться из исходников:
    - время сборки вырастет на 2-15 минут на пакет;
    - потребуются gcc, musl-dev и заголовки (+200 MB в стадии сборки);
    - возможны ошибки компиляции, специфичные для musl.

Что делает скрипт практически полезным.

Он превращает выбор базового образа из вопроса вкуса в проверяемое решение. Обычный спор «Alpine меньше» против «Alpine медленнее» разрешается за десять секунд конкретным ответом для вашего набора зависимостей.

Ключевое различение — три категории, а не две. Пакет на чистом Python вообще не зависит от библиотеки C. Пакет с wheels для обеих платформ работает везде. И только третья категория — с manylinux, но без musllinux — делает Alpine дорогим.

Ограничение скрипта. Он проверяет только прямые зависимости из файла. Транзитивные могут добавить бинарные пакеты. Более точный, но медленный способ — собрать оба варианта и сравнить фактически; скрипт нужен как быстрый первый фильтр.

Проверка результата

bash
docker run --rm python:3.13-slim sh -c 'ldd --version | head -1'
docker run --rm python:3.13-alpine sh -c 'ldd 2>&1 | head -1'

Убедитесь, что видите GLIBC и musl соответственно, и можете объяснить, почему это различие влияет на установку pandas.

Типичные ошибки

ОшибкаПричинаИсправление
Alpine «потому что меньше»Верно для Go, не для PythonПроверить наличие musllinux-wheels для своих зависимостей
python:latest или python:3Кажется, что «всегда свежий»Минорная версия сменится незаметно; указывать 3.13
Полный python:3.13 в productionЗначение по умолчанию~900 MB инструментов сборки не нужны при работе
Переход на 3.14 сразу после выходаХочется актуальностиWheels для новой минорной версии появляются с задержкой
Distroless без опыта отладкиВыглядит как правильный productionНет оболочки; сначала освойте nsenter и /proc/<pid>/root
psycopg2 вместо psycopg2-binary в разработкеКопируют из примеров-binary не требует компиляции; для production выбор осознанный
Сравнение только размера образаСамая заметная метрикаУчитывать также время сборки и время отладки
Alpine + multi-stage без проверки wheelsКажется, что multi-stage решает всёКомпиляторы уйдут, но время сборки останется

Контрольные вопросы

На понимание:

  1. Чем musl отличается от glibc с точки зрения установки Python-пакетов?
  2. Почему пакет на чистом Python одинаково работает на Alpine и на Debian?
  3. Что произойдёт при pip install, если подходящего wheel нет?
  4. Почему образ на Alpine может оказаться больше, чем на slim?
  5. Почему курс использует Python 3.13, а не 3.14?

На применение:

  1. Как узнать, есть ли musllinux-wheel для пакета, не собирая образ?
  2. Как посмотреть, какие теги платформы видит pip в конкретном образе?
  3. Как проверить, какая библиотека C используется в образе?

На диагностику:

  1. Сборка на Alpine падает с error: command 'gcc' failed. Что произошло и какие два решения возможны?
  2. После перехода на Alpine время сборки в CI выросло с 40 секунд до 12 минут. Как найти виновника?

Краткое резюме

  1. Варианты официального образа: полный (~1 GB), slim (~126 MB), alpine (~55 MB).
  2. Полный вариант содержит компиляторы и заголовки — в production они не нужны.
  3. Alpine использует musl, остальные — glibc; это определяет доступность бинарных wheels.
  4. Стандарт manylinux поддерживается почти всеми пакетами, musllinux — заметно реже.
  5. При отсутствии wheel pip компилирует из исходников: медленно и требует инструментов сборки.
  6. Alpine оправдан для чистого Python и когда все зависимости имеют musllinux-wheels.
  7. Проверять наличие wheels нужно до выбора образа, а не после первой сборки.
  8. Distroless минимален и безопасен, но лишён оболочки — отладка требует отдельных навыков.
  9. Версия Python: текущая минус одна как значение по умолчанию, минорная версия указывается явно.
  10. Решение принимается измерением на своём наборе зависимостей, а не общим правилом.

Официальные источники

ИсточникСсылкаЧто подтверждает
Python official imagehttps://hub.docker.com/_/pythonВарианты образа, предупреждение о musl и его влиянии на пакеты, поддерживаемые версии, база Debian Trixie
Docker Python language guidehttps://docs.docker.com/language/python/Рекомендации по выбору образа для Python
Python Packaging: platform compatibility tagshttps://packaging.python.org/en/latest/specifications/platform-compatibility-tags/Формат тегов wheels, соответствие платформам
PEP 600 — manylinuxhttps://peps.python.org/pep-0600/Стандарт совместимости wheels для систем на glibc
PEP 656 — musllinuxhttps://peps.python.org/pep-0656/Стандарт совместимости wheels для систем на musl
pip: installation and wheelshttps://pip.pypa.io/en/stable/cli/pip_install/Порядок выбора wheel или sdist, команда pip debug
Alpine Linux abouthttps://www.alpinelinux.org/about/Использование musl libc и BusyBox
Distroless imageshttps://github.com/GoogleContainerTools/distrolessСостав образов, отсутствие оболочки и пакетного менеджера
Building best practiceshttps://docs.docker.com/build/building/best-practices/Выбор минимального базового образа

Навигация

Вернуться к разделу
Следующий материал → Управление зависимостями
Главное оглавление

Markdown на GitHub ↗