Разверните OpenClaw в облаке в один клик
Вход/ Регистрация

Установка Python-пакетов при создании Docker-контейнера на базе Debian/Ubuntu

3
13 минут чтения
Средний рейтинг статьи: 5

Люди запускают Python-скрипты в Docker, потому что хотят предсказуемости. Нужно, чтобы код работал одинаково и на ноутбуке, и на сервере, и у коллеги, у которого вообще непонятно, какое окружение установлено. 

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

С Docker всё очень схоже с ситуацией на кухне. 

В локальной среде Python-проект может запускаться без проблем, потому что в операционной системе уже присутствуют компиляторы, системные библиотеки, header-файлы и ранее установленные зависимости. Docker-контейнер, напротив, представляет собой изолированное и минималистичное окружение — в нем отсутствует всё, что не добавлено явно. Поэтому если Python-пакету требуются системные библиотеки, их необходимо устанавливать вручную.

Кроме того, порядок инструкций в Dockerfile критичен: при некорректной последовательности шагов сборка будет выполняться значительно дольше. Если же не контролировать состав зависимостей, итоговый образ может существенно увеличиться в размере.

В этой статье мы разберем, как правильно подготовить кухню для Python в Docker: какие базовые образы выбирать, когда нужны системные зависимости и как их устанавливать, почему одни пакеты ставятся мгновенно, а другие компилируются.

С чего начать? Выбор базового образа

Выбрать базовый образ — это как выбрать кухню перед готовкой. Можно взять полностью оборудованную, но заплатить за это, в нашем случае, ресурсами системы. А можно выбрать пустую комнату с раковиной и газовой трубой и самому поставить сюда только то, что потребуется.

Образ python:3.11-slim

Самый простой вариант — взять готовый образ вроде:

    
FROM python:3.11-slim

Внутри этого образа уже установлен Python нужной версии. Вам не нужно думать, где взять интерпретатор, как его собрать: можно сразу перейти к установке зависимостей. Для большинства проектов этого достаточно.

Вместо 3.11 можно использовать любую другую подходящую версию, но мы будем рассматривать работу на примере этой. 

Чем отличаются образы slim от обычных?

Коротко: python:3.11— это полноценный образ с большим набором предустановленных системных пакетов, а python:3.11-slim — его облегченная версия с минимальным набором зависимостей, где всё лишнее нужно устанавливать вручную.

python:3.11 — это полноценный образ на базе Debian. Проще говоря, это кухня с запасом. В шкафах уже лежат инструменты, которыми вы, возможно, никогда не воспользуетесь, но если понадобится — они под рукой. Когда вы используете его, ошибки из-за отсутствующих системных библиотек возникают реже. Но за это приходится платить: больше размер образа, больше «лишнего» в продакшене, а еще повышается потенциальная уязвимость всего приложения. 

python:3.11-slim — это минимальный Debian с установленным Python. Это кухня без лишних шкафов. Основное есть, а всё остальное вы добавляете сами. Из минусов: чаще приходится явно добавлять системные зависимости, а при сборке пакетов с C-расширениями почти гарантированно понадобится build-essential и dev-библиотеки. 

Если вы собираете продакшен-сервис и контролируете зависимости — лучше использовать slim-версию и добавлять только то, что действительно нужно.

А что насчет Alpine?

Помимо python:3.11 и python:3.11-slim, часто встречается еще один вариант — Alpine.

Например:

    
FROM python:3.11-alpine

Alpine — это очень легкий дистрибутив Linux. Образ на его основе заметно меньше по размеру. 

Но Alpine использует библиотеку musl вместо glibc, которая стоит в Debian и Ubuntu. Из-за этого некоторые Python-пакеты либо собираются сложнее, либо могут вести себя иначе. Особенно те, что завязаны на C-расширения.

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

Alpine имеет смысл, если:

  • вы точно понимаете, какие зависимости используются;
  • проект небольшой;
  • нет тяжелых C-библиотек;
  • вы готовы разбираться с нюансами musl.

Для большинства продакшен-сервисов проще и безопаснее использовать slim-образ на базе Debian.

Полный образ Debian или Ubuntu

Например, если взять обычный Ubuntu:

    
FROM ubuntu:22.04

И дальше самостоятельно установить Python:

    
RUN apt update && apt install -y python3 python3-pip

В такой версии у вас появляется больше контроля. Можно выбрать конкретную версию системы, добавить свои репозитории, тонко настроить окружение. Такой подход используют, когда проект живет долго, когда нужно строгое соответствие инфраструктуре или когда Python — лишь часть общей системы.

Но чем чище базовый образ, тем больше работы вам предстоит. В ubuntu нет даже pip по умолчанию. Нет вообще ничего лишнего. И это одновременно плюс и дополнительная ответственность.

Поэтому для большинства задач правило простое:

  • если это типичный Python-сервис — берите python:X.Y-slim;

  • если нужна тонкая системная настройка — начинайте с Debian или Ubuntu;

  • не гонитесь за минимальным размером образа в ущерб стабильности.

Почему pip install в контейнере ведет себя иначе, чем на вашем компьютере

Допустим, вы выбрали базовый образ и написали в Dockerfile привычную строку:

    
RUN pip install -r requirements.txt

Эта команда устанавливает зависимости из файла в ваше текущее окружение. Это нужно, когда требуется передать свою версию приложения кому-то еще или загрузить на сервер. 

Вне Docker-контейнера эта команда редко когда возвращает ошибку, только если pip не может найти нужный Python-пакет. Но бывает, что под Docker сборка внезапно начинает что-то компилировать, появляются десятки строк с gcc, а иногда и ошибки.

Чтобы понять, что происходит, вернемся к нашей кухне. Когда вы готовите дома, часть ингредиентов уже лежит в шкафах. Вы об этом даже не думаете. Просто начинаете готовить. А в контейнере ничего этого нет. Он чистый.

Когда pip устанавливает пакет, он делает одно из двух:

  • либо скачивает готовый бинарный файл (wheel) и просто кладет его в систему,

  • либо скачивает исходники и начинает собирать пакет прямо внутри контейнера.

Первый вариант быстр и незаметен, а второй — шумный и требовательный.

Если для вашей версии Python и вашей системы уже есть готовый wheel, то установка проходит за секунды. Если нет, pip начинает компиляцию. А для компиляции нужны инструменты: компилятор, заголовочные файлы, системные библиотеки. Если их нет, то сборка упадет. 

Отсюда берутся ошибки вроде:

    
error: command 'gcc' failed fatal error: libpq-fe.h: No such file or directory

Особенно часто это происходит с пакетами, которые работают с базами данных, изображениями, криптографией, математикой. Например:

  • psycopg2 — требует библиотеку PostgreSQL;

  • Pillow — зависит от системных библиотек обработки изображений;

  • lxml — использует libxml и libxslt;

  • numpy — может собираться из исходников, если нет подходящего wheel.

На обычном образе python:3.11 часть зависимостей может оказаться уже установленной. В slim почти ничего лишнего нет. Поэтому разница особенно заметна именно там.

Когда требуются системные зависимости?

Не каждый Python-пакет — это просто набор .py-файлов. Иногда внутри есть C- или C++-код, а иногда для вашей платформы просто нет готового бинарного файла, и тогда pip начинает сборку из исходников.

Если возвращаться к кухне: вы готовите по рецепту и вдруг понимаете, что нужно не просто смешать ингредиенты, а, например, перемолоть мясо или запечь что-то в духовке. Без мясорубки и духовки ничего не выйдет. В Python-мире такой мясорубкой становится компилятор, а духовкой — системные библиотеки.

Системные зависимости требуются, когда библиотека работает не только с самим Python, а взаимодействует с внешним миром. Например, psycopg2 нужен доступ к библиотекам PostgreSQL, Pillow — к библиотекам обработки изображений, lxml — к libxml и libxslt. Если в контейнере их нет, сборка остановится с ошибкой.

А бывают и противоположные случаи. Если в проекте только FastAPI, requests, pydantic или другие библиотеки, то никаких системных пакетов может не понадобиться вообще. pip просто скачает готовые wheel-файлы, и установка пройдет быстро и тихо.

Понять, что понадобятся дополнительные зависимости, можно еще до ошибки. Если во время установки появляется строка Building wheel, значит пакет собирается из исходников. Если процесс занимает минуты и в логе мелькает gcc, почти наверняка не хватает инструментов сборки.

Именно в таких ситуациях и требуются системные зависимости. Не потому что Docker сложный, а потому что некоторые библиотеки работают глубже, чем просто на уровне Python-кода.

Порядок инструкций в Dockerfile

В Docker важен не только список команд, но и их порядок. Это как готовить ужин: если вы сначала всё подготовили, а потом начали готовить — процесс идет быстро. Если каждый раз бегаете за солью и заново моете доску — времени уходит больше.

Docker работает слоями. Каждая инструкция в Dockerfile создает отдельный слой. И если слой не изменился, Docker берет его из кэша и не пересобирает. Это сильно ускоряет сборку, но только если вы правильно расположили команды.

Посмотрим на частую ошибку:

    
FROM python:3.11-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt

Здесь мы сначала копируем весь проект, а потом устанавливаем зависимости. Любое изменение в коде, и даже правка одной строки, ломает кэш слоя COPY . .. Docker считает, что всё изменилось, и заново запускает pip install, даже если requirements.txt не трогали. В итоге зависимости пересобираются каждый раз. Сборка становится медленной, особенно если есть пакеты с компиляцией.

Правильнее сделать так:

    
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . .

Теперь логика такая: если вы меняете код, то пересобирается только последний слой. А если вы меняете зависимости — пересобирается слой с pip install. Сборка становится предсказуемой и быстрой.

Как не раздувать образ при установке системных зависимостей?

Когда вы устанавливаете системные зависимости через apt, внутри образа остается кэш пакетов. Он нужен во время установки, но не нужен после. Если его не удалить, образ становится тяжелее.

Поэтому после установки обычно добавляют очистку:

    
RUN apt update && apt install -y --no-install-recommends \     build-essential \     libpq-dev \     && rm -rf /var/lib/apt/lists/*

Флаг --no-install-recommends тоже важен. По умолчанию apt может подтянуть дополнительные пакеты, но они не всегда нужны. Этот флаг позволяет ставить только то, что действительно требуется.

Есть более серьезный источник лишнего веса — dev-зависимости. Сюда входят компилятор, заголовочные файлы и инструменты сборки. После установки они часто больше не используются. Если оставить build-essential в продакшен-образе, приложение от этого быстрее не станет, зато образ увеличится и расширится поверхность атаки: внутри контейнера останется полноценный набор инструментов разработки, которые в случае компрометации облегчают выполнение вредоносного кода. Поэтому в финальном образе лучше оставлять только runtime-зависимости. 

Полный пример: как установить Python-пакеты в Docker

Соберем простой сервис с зависимостями, которым нужны системные библиотеки. Например, FastAPI, psycopg2 для PostgreSQL и Pillow для работы с изображениями.

В requirements.txt:

    
fastapi==0.129.0 uvicorn==0.41.0 psycopg2==3.3.2 Pillow==12.1.1

psycopg2 почти всегда требует установки C-библиотек, потому что это обертка над нативной библиотекой PostgreSQL. Pillow работает через системные библиотеки обработки изображений и зависит от libjpeg-dev, zlib1g-dev

Если на локальном компьютере вы работали с Python через виртуальное окружение, то можно автоматически создать файл requirements.txt. Для этого, находясь в виртуальном окружении, введите команду в терминал: 

    
pip freeze > requirements.txt

Теперь составим Dockerfile:

    
FROM python:3.11-slim # Рабочая директория WORKDIR /app # Устанавливаем системные зависимости RUN apt update && apt install -y --no-install-recommends \     build-essential \     libpq-dev \     libjpeg-dev \     zlib1g-dev \     python3-dev \     && rm -rf /var/lib/apt/lists/* # Копируем зависимости отдельно (почему так — см. в разделе Правильный порядок инструкций в Dockerfile) COPY requirements.txt . # Устанавливаем Python-пакеты RUN pip install --no-cache-dir -r requirements.txt # Копируем проект COPY . . # Запускаем приложение CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Здесь важно:

  • Базовый образ — python:3.11-slim.

  • Системные зависимости устанавливаются до pip install.

  • Кэш apt удаляется сразу.

  • requirements.txt копируется отдельно.

Если приложение не требует системных зависимостей, блок с apt install можно убрать. Если появляются новые C-библиотеки — добавляем соответствующие lib*-dev-пакеты.

Продакшен-вариант: multi-stage-сборка

Multi-stage-сборка — это способ собирать Docker-образ в несколько этапов. На одном этапе вы что-то собираете, компилируете, подготавливаете, а в финальный контейнер переносите только результат. Всё лишнее — компиляторы, заголовочные файлы, инструменты разработки — остается в промежуточном слое и в продакшен не попадает.

В нашем примере выше мы устанавливали build-essential, python3-dev и dev-библиотеки прямо в продакшен-образ. Это работает, но внутри контейнера остается полноценный набор инструментов разработки. Образ становится тяжелее, а поверхность атаки шире. Multi-stage решает эту проблему: сборка происходит отдельно, а в финальном образе остаются только runtime-зависимости.

Посмотрим, как это выглядит на практике:

    
# Этап 1: сборка зависимостей FROM python:3.11-slim AS builder WORKDIR /app # Устанавливаем инструменты сборки RUN apt update && apt install -y --no-install-recommends \     build-essential \     python3-dev \     libpq-dev \     libjpeg-dev \     zlib1g-dev \     && rm -rf /var/lib/apt/lists/* COPY requirements.txt . # Собираем wheel-файлы RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt # Этап 2: финальный продакшен-образ FROM python:3.11-slim WORKDIR /app # Устанавливаем только runtime-библиотеки RUN apt update && apt install -y --no-install-recommends \     libpq5 \     libjpeg62-turbo \     zlib1g \     && rm -rf /var/lib/apt/lists/* # Копируем собранные wheel COPY --from=builder /wheels /wheels # Устанавливаем зависимости без компиляции RUN pip install --no-cache-dir /wheels/* # Копируем код приложения COPY . . CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

В первой стадии, которую мы назвали builder, устанавливаются компилятор и все dev-зависимости. Они нужны только для одного — собрать Python-пакеты, которым требуется компиляция. Здесь из requirements.txt формируются готовые wheel-файлы. По сути, на этом этапе мы выполняем всю «грязную работу»: подключаем инструменты сборки, заголовочные файлы и системные dev-библиотеки.

После этого начинается вторая стадия. Мы снова берем чистый python:3.11-slim, но уже без компилятора и инструментов разработки. Это новый, аккуратный контейнер, в котором нет ничего лишнего — только базовая система и Python.

В этот финальный образ копируются собранные wheel-файлы из стадии builder. Пакеты устанавливаются без компиляции, потому что всё уже собрано заранее. Затем добавляется код приложения и указывается команда запуска.

В результате финальный контейнер содержит только то, что нужно для работы сервиса. Компиляторы, dev-зависимости и промежуточные файлы остаются на этапе сборки и не попадают в продакшен.

Заключение

Запускать Python‑скрипты в Docker — всё равно что готовить по четкому рецепту на оборудованной кухне: вы точно знаете, какие инструменты и ингредиенты понадобятся, в каком порядке их использовать и чего ожидать в итоге. 

В итоге главное правило остается неизменным: не просто заставить всё собраться, а выстроить устойчивый и предсказуемый процесс. Когда каждый контейнер запускается как по отработанному рецепту, вы экономите время, снижаете риск ошибок в продакшене и делаете жизнь своей команды чуточку проще.

3
13 минут чтения
Средний рейтинг статьи: 5
Хотите внести свой вклад?
Участвуйте в нашей контент-программе за
вознаграждение или запросите нужную вам инструкцию
img-server
Пока нет комментариев