- PVSM.RU - https://www.pvsm.ru -

Разбор особенностей официального Docker-образа Python

Официальный Docker-образ Python весьма популярен. Кстати, я и сам рекомендовал [1] одну из его вариаций в качестве базового образа. Но многие программисты не вполне понимают того, как именно он работает. А это может привести к путанице и к возникновению различных проблем.

Разбор особенностей официального Docker-образа Python - 1 [2]

В этом материале я собираюсь поговорить о том, как создан этот образ, о том, какую он может принести пользу, о его правильном использовании и о его ограничениях. В частности, я разберу тут его вариант python:3.8-slim-buster (в состоянии, представленном файлом Dockerfile от 19 августа 2020 года [3]) и по ходу дела остановлюсь на самых важных деталях.

Читаем файл Dockerfile

▍Базовый образ

Начнём с базового образа:

FROM debian:buster-slim

Оказывается, что базовым образом для python:3.8-slim-buster является Debian GNU/Linux 10 — текущий стабильный релиз Debian, известный ещё как Buster (релизы Debian называют именами персонажей из «Истории игрушек»). Бастер [4] — это, если кому интересно, собака Энди.

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

▍Переменные среды

Теперь взглянем на переменные среды. Первая обеспечивает как можно более раннее добавление /usr/local/bin в $PATH.

# обеспечивает выбор локальной версии python, а не версии, входящей в состав дистрибутива
ENV PATH /usr/local/bin:$PATH

Образ устроен так, что установка Python выполняется в /usr/local. В результате данная конструкция обеспечивает то, что по умолчанию будут использоваться установленные исполняемые файлы.

Далее — взглянем на настройки языка:

# http://bugs.python.org/issue19846
# > В настоящий момент настройка "LANG=C" в Linux *полностью выводит из строя Python 3*, а это плохо.
ENV LANG C.UTF-8

Насколько я знаю, современный Python 3, по умолчанию, и без этой настройки, использует UTF-8. Поэтому я не уверен в том, что в наши дни в исследуемом Dockerfile нужна эта строка.

Здесь есть и переменная окружения, содержащая сведения о текущей версии Python:

ENV PYTHON_VERSION 3.8.5

В Dockerfile есть ещё переменная окружения с GPG-ключом, используемая для верификации загружаемого исходного кода Python.

▍Зависимости времени выполнения

Python, для работы, нужны некоторые дополнительные пакеты:

RUN apt-get update && apt-get install -y --no-install-recommends 
    ca-certificates 
    netbase 
  && rm -rf /var/lib/apt/lists/*

Первый пакет, ca-certificates, содержит список сертификатов стандартных центров сертификации. Нечто подобное используется браузером для проверки -адресов [5]. Это позволяет Python, wget и другим инструментам проверять сертификаты, предоставляемые серверами.

Второй пакет, netbase, выполняет установку в /etc нескольких файлов, необходимых для настройки соответствия определённых имён с некоторыми портами и протоколами. Например, /etc/services отвечает за настройку соответствия имён сервисов, вроде https, с номерами портов. В данном случае это 443/tcp.

▍Установка Python

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

RUN set -ex 
  
  && savedAptMark="$(apt-mark showmanual)" 
  && apt-get update && apt-get install -y --no-install-recommends 
    dpkg-dev 
    gcc 
    libbluetooth-dev 
    libbz2-dev 
    libc6-dev 
    libexpat1-dev 
    libffi-dev 
    libgdbm-dev 
    liblzma-dev 
    libncursesw5-dev 
    libreadline-dev 
    libsqlite3-dev 
    libssl-dev 
    make 
    tk-dev 
    uuid-dev 
    wget 
    xz-utils 
    zlib1g-dev 
# с релиза Stretch "gpg" больше по умолчанию в дистрибутив не входит
    $(command -v gpg > /dev/null || echo 'gnupg dirmngr') 
  
  && wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" 
  && wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" 
  && export GNUPGHOME="$(mktemp -d)" 
  && gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$GPG_KEY" 
  && gpg --batch --verify python.tar.xz.asc python.tar.xz 
  && { command -v gpgconf > /dev/null && gpgconf --kill all || :; } 
  && rm -rf "$GNUPGHOME" python.tar.xz.asc 
  && mkdir -p /usr/src/python 
  && tar -xJC /usr/src/python --strip-components=1 -f python.tar.xz 
  && rm python.tar.xz 
  
  && cd /usr/src/python 
  && gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" 
  && ./configure 
    --build="$gnuArch" 
    --enable-loadable-sqlite-extensions 
    --enable-optimizations 
    --enable-option-checking=fatal 
    --enable-shared 
    --with-system-expat 
    --with-system-ffi 
    --without-ensurepip 
  && make -j "$(nproc)" 
    LDFLAGS="-Wl,--strip-all" 
  && make install 
  && rm -rf /usr/src/python 
  
  && find /usr/local -depth 
    ( 
      ( -type d -a ( -name test -o -name tests -o -name idle_test ) ) 
      -o ( -type f -a ( -name '*.pyc' -o -name '*.pyo' -o -name '*.a' ) ) 
      -o ( -type f -a -name 'wininst-*.exe' ) 
    ) -exec rm -rf '{}' + 
  
  && ldconfig 
  
  && apt-mark auto '.*' > /dev/null 
  && apt-mark manual $savedAptMark 
  && find /usr/local -type f -executable -not ( -name '*tkinter*' ) -exec ldd '{}' ';' 
    | awk '/=>/ { print $(NF-1) }' 
    | sort -u 
    | xargs -r dpkg-query --search 
    | cut -d: -f1 
    | sort -u 
    | xargs -r apt-mark manual 
  && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false 
  && rm -rf /var/lib/apt/lists/* 
  
  && python3 --version

Тут происходит много всего, но самое важное — это следующее:

  1. Python устанавливается в /usr/local.
  2. Удаляются все .pyc-файлы.
  3. Пакеты, в частности — gcc и прочие подобные, которые были нужны для компиляции Python, удаляются после того, как необходимость в них пропадает.

Из-за того, что всё это происходит в единственной команде RUN, в итоге компилятор не сохраняется ни в одном из слоёв, что помогает поддерживать компактный размер образа.

Тут можно обратить внимание на то, что Python для компиляции нужна библиотека libbluetooth-dev. Мне это показалось необычным, поэтому я решил в этом разобраться. Как оказалось, Python может создавать Bluetooth-сокеты, но только в том случае, если он скомпилирован с использованием этой библиотеки.

▍Настройка символьных ссылок

На следующем шаге работы /usr/local/bin/python3 назначается символьная ссылка /usr/local/bin/python, что позволяет вызывать Python разными способами:

# создание некоторых полезных символьных ссылок, присутствие которых ожидается в системе
RUN cd /usr/local/bin 
  && ln -s idle3 idle 
  && ln -s pydoc3 pydoc 
  && ln -s python3 python 
  && ln -s python3-config python-config

▍Установка pip

У менеджера пакетов pip имеется собственный график выхода релизов, отличающийся от графика релизов Python. Например, в этом Dockerfile выполняется установка Python 3.8.5, выпущенного в июле 2020. А pip 20.2.2 вышел в августе, уже после выхода Python, но Dockerfile устроен так, чтобы была бы установлена свежая версия pip:

# если эту переменную назвать "PIP_VERSION", то pip выдаёт ошибку: "ValueError: invalid truth value '<VERSION>'"
ENV PYTHON_PIP_VERSION 20.2.2
# https://github.com/pypa/get-pip
ENV PYTHON_GET_PIP_URL https://github.com/pypa/get-pip/raw/5578af97f8b2b466f4cdbebe18a3ba2d48ad1434/get-pip.py
ENV PYTHON_GET_PIP_SHA256 d4d62a0850fe0c2e6325b2cc20d818c580563de5a2038f917e3cb0e25280b4d1

RUN set -ex; 
  
  savedAptMark="$(apt-mark showmanual)"; 
  apt-get update; 
  apt-get install -y --no-install-recommends wget; 
  
  wget -O get-pip.py "$PYTHON_GET_PIP_URL"; 
  echo "$PYTHON_GET_PIP_SHA256 *get-pip.py" | sha256sum --check --strict -; 
  
  apt-mark auto '.*' > /dev/null; 
  [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; 
  apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; 
  rm -rf /var/lib/apt/lists/*; 
  
  python get-pip.py 
    --disable-pip-version-check 
    --no-cache-dir 
    "pip==$PYTHON_PIP_VERSION" 
  ; 
  pip --version; 
  
  find /usr/local -depth 
    ( 
      ( -type d -a ( -name test -o -name tests -o -name idle_test ) ) 
      -o 
      ( -type f -a ( -name '*.pyc' -o -name '*.pyo' ) ) 
    ) -exec rm -rf '{}' +; 
  rm -f get-pip.py

После выполнения этих операций, как и прежде, удаляются все .pyc-файлы.

▍Точка входа в образ

В итоге в Dockerfile указывается точка входа в образ:

CMD ["python3"]

Используя CMD вместо ENTRYPOINT мы, запуская образ, по умолчанию получаем доступ к python:

$ docker run -it python:3.8-slim-buster
Python 3.8.5 (default, Aug  4 2020, 16:24:08)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Но, при необходимости, можно указывать при запуске образа и другие исполняемые файлы:

$ docker run -it python:3.8-slim-buster bash
root@280c9b73e8f9:/#

Итоги

Вот что мы узнали, разобрав Dockerfile официального Python-образа slim-buster.

▍В состав образа входит Python

Хотя это и может показаться очевидным, стоит обратить внимание на то, как именно Python включён в состав образа. А именно, сделано это путём его самостоятельной установки в /usr/local.

Программисты, использующие этот образ, порой совершают одну и ту же ошибку, которая заключается в повторной установке Debian-версии Python:

FROM python:3.8-slim-buster

# Делать этого не нужно:
RUN apt-get update && apt-get install python3-dev

При выполнении этой команды RUN Python будет установлен ещё раз, но в /usr, а не в /usr/local. И это, как правило, будет не та версия Python, которая установлена в /usr/local. А программисту, который воспользовался вышеприведённым Docker-файлом, вероятно, не нужны [6] две разные версии Python в одном и том же образе. Это, в основном, является причиной путаницы.

А если же кому-то и правда нужна Debian-версия Python, то лучше будет использовать в качестве базового образа debian:buster-slim.

▍В образ входит самая свежая версия pip

Например, самый свежий релиз Python 3.5 состоялся в ноябре 2019, но Docker-образ python:3.5-slim-buster включает в себя pip, который вышел в августе 2020. Это (обычно) хорошо, так как означает, что в нашем распоряжении оказываются самые свежие исправления ошибок и улучшения производительности. Это, кроме того, значит, что мы можем пользоваться поддержкой более новых вариантов «колёс».

▍Из образа удаляются все .pyc-файлы

Если хочется немного ускорить загрузку системы, то можно самостоятельно скомпилировать исходный код стандартной библиотеки в формат .pyc. Делается это с помощью модуля compileall [7].

▍Образ не выполняет установку обновлений безопасности Debian

Хотя базовые образы debian:buster-slim и python часто обновляются, имеется определённый промежуток между моментами выхода обновлений безопасности Debian и включением их в образы. Поэтому нужно самостоятельно устанавливать обновления безопасности для базового дистрибутива Linux.

Какими Docker-образами вы пользуетесь для выполнения Python-кода?

Автор: ru_vds

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/razrabotka/356342

Ссылки в тексте:

[1] рекомендовал: https://pythonspeed.com/articles/base-image-python-docker-images/

[2] Image: https://habr.com/ru/company/ruvds/blog/516310/

[3] 19 августа 2020 года: https://github.com/docker-library/python/blob/1b78ff417e41b6448d98d6dd6890a1f95b0ce4be/3.8/buster/slim/Dockerfile

[4] Бастер: https://toystorymovies.fandom.com/wiki/Buster

[5] -адресов: https://-%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%BE%D0%B2

[6] не нужны: https://pythonspeed.com/articles/importerror-docker/

[7] compileall: https://docs.python.org/3/library/compileall.html

[8] Источник: https://habr.com/ru/post/516310/?utm_source=habrahabr&utm_medium=rss&utm_campaign=516310