Совсем недавно произошёл релиз минималистичного Alpine Linux 3.8. Очень часто данный linux образ используют в докере, собирая очень компактные окружения для runtime.

Сегодняшняя статья будет рассмотрена в срезе использования runtime системы в докере для Python 3.6.X версий, с различным составом пакетов pip. А так же мы соберём самый новый Python 3.7 в Alpine.

В конце статьи будет представлен размер образа image, занимаемый на диске, в зависимости от состава пакетов pip и произведено сравнение между дистрибутивами Alpine 3.8, Debian 9, Fedora 28.

Итак, приступим: для тестирования дистрибутивы выбраны. Будем собирать следующие docker images:
  1. Система, ее обновление. И Python3 с обновлённым pip (10 версии)
  2. п.1 + tornado cython
  3. п.2 + numpy-scipy
  4. п.3 + pillow bokeh pandas websocket-client

В результате даных заливок, мы получим различные версии: Python без пакетов, Python с web сервером, Python с пакетами для обработки многопоточных математических вычислений, Python с «графическим» стеком и работы с данными.

Итак, результирующие файлы для Debian и Fedora будут выглядеть у нас так:
Debian
FROM debian
 
RUN apt-get update -y && apt-get install python3-pip -y && pip3 install pip --upgrade && apt-get clean
 
RUN pip3 install cython tornado websocket-client pytest numpy pandas scipy bokeh pillow


Fedora
FROM fedora
  
RUN dnf update -y && dnf install libstdc++ -y && dnf clean all && pip3 install --upgrade pip && python3 --version
  
RUN pip3 install cython tornado websocket-client pytest numpy pandas scipy bokeh pillow


А вот с Alpine 3.8 пока заминка. Официально на момент написания статьи он ещё на вышел, а посмотреть, то хочется:-). Поэтому нам понадобиться их образ системы:
dl-cdn.alpinelinux.org/alpine/v3.8/releases/x86_64
И мы соберём свой Alpine from Sratch:
github.com/gliderlabs/docker-alpine/tree/master/versions/library-3.8/x86_64

Выкачиваем файловую систему Alpine 3.8:
curl dl-cdn.alpinelinux.org/alpine/v3.8/releases/x86_64/alpine-minirootfs-3.8.0_rc10-x86_64.tar.gz >alpine3.8.tar.gz

Создаём свой докер файл:
FROM scratch
ADD alpine3.8.tar.gz /

ENV RELEASE="v3.8"
ENV MIRROR="http://dl-cdn.alpinelinux.org/alpine"
ENV PACKAGES="alpine-baselayout,busybox,alpine-keys,apk-tools,libc-utils"
ENV TAGS=(alpine:3.8)


Затем копипастим и добавляем в этот файл борку Python 3.6 со страницы github.com/docker-library/python/blob/master/3.6/alpine3.7/Dockerfile
не забыв удалить или закомментировать строку FROM alpine:3.7

И пробуем создать образ с Alpine 3.8 и Python на борту:

$docker build -t alpine3.8 .
#результат работы (последние строки)
(1/2) Purging .fetch-deps (0)
(2/2) Purging libressl (2.7.4-r0)
Executing busybox-1.28.4-r0.trigger
OK: 33 MiB in 45 packages
+ python get-pip.py --disable-pip-version-check --no-cache-dir 'pip==10.0.1'
Collecting pip==10.0.1
  Downloading https://files.pythonhosted.org/packages/0f/74/ecd13431bcc456ed390b44c8a6e917c1820365cbebcb6a8974d1cd045ab4/pip-10.0.1-py2.py3-none-any.whl (1.3MB)
Collecting setuptools
  Downloading https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl (567kB)
Collecting wheel
  Downloading https://files.pythonhosted.org/packages/81/30/e935244ca6165187ae8be876b6316ae201b71485538ffac1d718843025a9/wheel-0.31.1-py2.py3-none-any.whl (41kB)
Installing collected packages: pip, setuptools, wheel
Successfully installed pip-10.0.1 setuptools-39.2.0 wheel-0.31.1
+ pip --version
pip 10.0.1 from /usr/local/lib/python3.6/site-packages/pip (python 3.6)
+ find /usr/local -depth '(' '(' -type d -a '(' -name test -o -name tests ')' ')' -o '(' -type f -a '(' -name '*.pyc' -o -name '*.pyo' ')' ')' ')' -exec rm -rf '{}' +
+ rm -f get-pip.py
 ---> f7439dca5f31
Removing intermediate container e1fcd1c74873
Step 17/17 : CMD python3
 ---> Running in a4dc6dfa5184
 ---> 6924b206c6b9
Removing intermediate container a4dc6dfa5184
Successfully built 6924b206c6b9


Результаты первого шага установка только Python (docker images --all):
  1. Debian 9 / 513 MB
  2. Fedora 28 / 387 MB
  3. Alpine 3.8 / 82.2 MB


Шаг 2. Установка cython и tornado

Начинаем добавлять пакеты pip. Первым установим cython и tornado. Для Debian и Fedora пакеты ставятся без ошибок, а вот Alpine падает с ошибкой:
unable to execute 'gcc': No such file or directory
    error: command 'gcc' failed with exit status 1

Придется гуглить и потом уже добавлять библиотеки сборки в Alpine, чтобы pip успешно собрал их из исходного текста. Затем запускать сборку докера снова, затем опять искать зависимости, читать форумы stackoverflow и issues в github и ждать и ждать и ждать.


Поскольку в следующих шагах мы начнём добавлять математические и графические библиотеки в наш образ runtime Python, и чтобы слишком не увеличивать текст данной статьи, я приведу финальные зависимости для Alpine linux:
         apk add --no-cache --virtual .build-deps                  gfortran                 build-base                 openblas-dev                 bzip2-dev                 coreutils                 dpkg-dev dpkg                 expat-dev                 gcc                 gdbm-dev                 libc-dev                 libffi-dev                 libnsl-dev                 libressl                 libressl-dev                 libtirpc-dev                 linux-headers                 make                 ncurses-dev                 pax-utils                 readline-dev                 sqlite-dev                 tcl-dev                 tk                 tk-dev                 xz-dev                 zlib-dev                 libxml2-dev                 libxslt-dev                 musl-dev                 libgcc                 curl                 jpeg-dev                 zlib-dev                 freetype-dev                 lcms2-dev                 openjpeg-dev                 tiff-dev                 tk-dev                 tcl-dev         && ln -s /usr/include/locale.h /usr/include/xlocale.h

  1. Debian 9 / 534 MB
  2. Fedora 28 / 407 MB
  3. Alpine 3.8 / 144 MB


Шаг 3. Добавляем математику numpy scipy


  1. Debian 9 / 763 MB
  2. Fedora 28 / 626 MB
  3. Alpine 3.8 / 404 MB MB


Шаг 4. Добавляем графический стек websocket-client pytest pandas bokeh pillow



Dockerfile-Alpine

FROM scratch
ADD alpine3.8.tar.gz /
CMD ["/bin/sh"]

ENV RELEASE="v3.8"
ENV MIRROR="http://dl-cdn.alpinelinux.org/alpine"
ENV PACKAGES="alpine-baselayout,busybox,alpine-keys,apk-tools,libc-utils"
#ENV BUILD_OPTIONS=(-b -s -t UTC -r $RELEASE -m $MIRROR -p $PACKAGES)
ENV TAGS=(alpine:3.8)

#
# NOTE: THIS DOCKERFILE IS GENERATED VIA "update.sh"
#
# PLEASE DO NOT EDIT IT DIRECTLY.
#
#https://github.com/docker-library/python/blob/master/3.6/alpine3.7/Dockerfile
# ensure local python is preferred over distribution python
ENV PATH /usr/local/bin:$PATH

# http://bugs.python.org/issue19846
# > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK.
ENV LANG C.UTF-8

# install ca-certificates so that HTTPS works consistently
# the other runtime dependencies for Python are installed later
RUN apk add --no-cache ca-certificates

ENV GPG_KEY 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
ENV PYTHON_VERSION 3.6.6

RUN set -ex     && apk add --no-cache --virtual .fetch-deps 	gnupg 	libressl 	tar 	xz         && 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 --keyserver ha.pool.sks-keyservers.net --recv-keys "$GPG_KEY"     && gpg --batch --verify python.tar.xz.asc python.tar.xz     && 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         && apk add --no-cache --virtual .build-deps  	bzip2-dev 	coreutils 	dpkg-dev dpkg 	expat-dev 	gcc 	gdbm-dev 	libc-dev 	libffi-dev 	libnsl-dev 	libressl 	libressl-dev 	libtirpc-dev 	linux-headers 	make 	ncurses-dev 	pax-utils 	readline-dev 	sqlite-dev 	tcl-dev 	tk 	tk-dev 	xz-dev 	zlib-dev # add build deps before removing fetch deps in case there's overlap
    && apk del .fetch-deps         && cd /usr/src/python     && gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"     && ./configure 	--build="$gnuArch" 	--enable-loadable-sqlite-extensions 	--enable-shared 	--with-system-expat 	--with-system-ffi 	--without-ensurepip     && make -j "$(nproc)" # set thread stack size to 1MB so we don't segfault before we hit sys.getrecursionlimit()
# https://github.com/alpinelinux/aports/commit/2026e1259422d4e0cf92391ca2d3844356c649d0
	EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000"     && make install         && runDeps="$( 	scanelf --needed --nobanner --format '%n#p' --recursive /usr/local 	    | tr ',' '\n' 	    | sort -u 	    | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }'     )"     && apk add --virtual .python-rundeps $runDeps     && apk del .build-deps         && find /usr/local -depth 	\( 	    \( -type d -a \( -name test -o -name tests \) \) 	    -o 	    \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) 	\) -exec rm -rf '{}' +     && rm -rf /usr/src/python

# make some useful symlinks that are expected to exist
RUN cd /usr/local/bin     && ln -s idle3 idle     && ln -s pydoc3 pydoc     && ln -s python3 python     && ln -s python3-config python-config

# if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value '<VERSION>'"
ENV PYTHON_PIP_VERSION 10.0.1

RUN set -ex;         apk add --no-cache --virtual .fetch-deps libressl;         wget -O get-pip.py 'https://bootstrap.pypa.io/get-pip.py';         apk del .fetch-deps;         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 	    \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) 	\) -exec rm -rf '{}' +;     rm -f get-pip.py

         apk add --no-cache --virtual .build-deps                  gfortran                 build-base                 openblas-dev                 bzip2-dev                 coreutils                 dpkg-dev dpkg                 expat-dev                 gcc                 gdbm-dev                 libc-dev                 libffi-dev                 libnsl-dev                 libressl                 libressl-dev                 libtirpc-dev                 linux-headers                 make                 ncurses-dev                 pax-utils                 readline-dev                 sqlite-dev                 tcl-dev                 tk                 tk-dev                 xz-dev                 zlib-dev                 libxml2-dev                 libxslt-dev                 musl-dev                 libgcc                 curl                 jpeg-dev                 zlib-dev                 freetype-dev                 lcms2-dev                 openjpeg-dev                 tiff-dev                 tk-dev                 tcl-dev         && ln -s /usr/include/locale.h /usr/include/xlocale.h         && pip install cython tornado websocket-client pytest numpy pandas scipy bokeh pillow         && apk del .build-deps        && apk add --no-cache  libstdc++ openblas zlib jpeg openjpeg tiff tk tcl musl libxml2 libxslt xz zlib libstdc++ openblas     && pip list

CMD ["python3"]



  1. Debian 9 / 905 MB
  2. Fedora 28 / 760 MB
  3. Alpine 3.8 / 650 MB


В качестве бонуса, попробуем в Alpine 3.8 скомпилировать ещё не вышедший для докера Python 3.7.
Новая версия Python 3.7 представлена 27 июня 2018 года

Код компиляции возьмём из:
github.com/docker-library/python/tree/bbbc37fff3411a34deef30dd9b34dc938fe7b134/3.7-rc/alpine3.7

Завершение компиляции 3.7
Package          Version
---------------- -------
atomicwrites     1.1.5  
attrs            18.1.0 
bokeh            0.13.0 
Cython           0.28.3 
Jinja2           2.10   
MarkupSafe       1.0    
more-itertools   4.2.0  
numpy            1.14.5 
packaging        17.1   
pandas           0.23.1 
Pillow           5.1.0  
pip              10.0.1 
pluggy           0.6.0  
py               1.5.4  
pyparsing        2.2.0  
pytest           3.6.2  
python-dateutil  2.7.3  
pytz             2018.4 
PyYAML           4.1    
scipy            1.1.0  
setuptools       39.2.0 
six              1.11.0 
tornado          5.0.2  
websocket-client 0.48.0 
wheel            0.31.1 
 ---> 3d18b8c27cd9
Removing intermediate container f546e004b79f
Step 18/18 : CMD python3
 ---> Running in 23c5aea50a0d
 ---> d5385e425064
Removing intermediate container 23c5aea50a0d
Successfully built d5385e425064

real    41m17,619s


Размер Alpine 3.8 с Python 3.7 с текущим списком пакетов pip 656 MB

Итоги


Python
  • Debian 9 / больше в 6.24х / +430 Mb
  • Fedora 28 / больше в 4,7х / +304 Mb

Python tornado cython
  • Debian 9 / больше в 3,71х / +390 Mb
  • Fedora 28 / больше в 2,82x / +263 Mb


Python tornado cython numpy scipy
  • Debian 9 / больше в 1,88 раз / +359 Mb
  • Fedora 28 / больше в 1.54 раз / +222 Mb


Python tornado cython numpy scipy websocket-client pytest pandas bokeh pillow
  • Debian 9 / больше в 1,39 раз / +255 Mb
  • Fedora 28 / больше в 1.16 раз / +110 Mb

При использовании пустого runtime Python, дистрибутив Alpine linux лидер по минимальному размеру. При увеличени количества библиотек pip до tornado+cython+numpy+scipy Alpine все ещё дает заметную экономию в размере на жёстком диске. Одако как только в пакетах появляются графические утилиты для работы с данными для Python, разница практически исчезает.

При большом количестве графических пакетов, оптимальнее выбрать дистрибутив Fedora, чем заниматься компиляцией пакетов в Alpine (компиляция может длиться 1-2 часа), и в результате получить экономию в один или два десятка процентов места на жёстком диске.

UPDATE1: Тестирование проводилось на Fedora Atomic Host: release 28 (Twenty Eight), Version: 2018.5

UPDATE2: Проверка занимаемого места на диске, была проведена по данному билду hub.docker.com/r/flytrue/python-runtime-docker/tags
$docker pull flytrue/python-runtime-docker:alpine-full
$docker save flytrue/python-runtime-docker:alpine-full -o alpine-full.tar
$ls -lh alpine-full.tar 
-rw-------. 1 fedora fedora 631M июн 29 08:38 alpine-full.tar
$ docker images --all|grep alpine
docker.io/flytrue/python-runtime-docker   alpine-full         f37154658671        19 hours ago        650 MB

Комментарии (9)


  1. zloddey
    28.06.2018 15:40
    +1

    А если применить multi-stage build? Один образ Alpine для компиляции зависимостей, другой для рантайма. Конечно, это ещё больше возни, чем предложенный вариант, но экономия места всё же будет.


    1. Renatk Автор
      28.06.2018 15:45

      Согласен, ничто не мешает использовать данный способ. Но в данных примерах (официальные сборки Alpine 3.7 для Python 3.6) вычищают все установленные и временные зависимости. Поэтому цель была сравнить пустой Python и дополненный пакетами.


  1. kkirsanov2
    28.06.2018 15:53

    А почему вы не удаляете из сборки *-dev пактеы, которые ненужны после компиляции?


    Сам делаю вот так: разбиваю пакеты на две группы. перечисленные в dev удаляю после установки pip -r


    RUN apk update && \cat /opt/app/requirements/apk.txt | xargs apk add  --no-cache && \cat /opt/app/requirements/apk_dev.txt | xargs apk add  --no-cache && \python3 -m pip install -r /opt/app/requirements/python-requirements.txt && \cat /opt/app/requirements/apk_dev.txt | xargs apk del

    Удаление тяжелых тяжелых пакетов типа mariadb-dev сильно экономит место и заметно ускоряет деплой.


    1. Renatk Автор
      28.06.2018 16:07

      Именно так, как вы написали и происходит: установка сборочных инструментариев, сборка, удаление сборочных инструментариев.

       apk add --no-cache --virtual .build-deps
      ....
       apk del .build-deps 


      1. Jon7
        28.06.2018 18:52

        А Вы уверены что "apk del .build-deps" удаляет и фактически освобождает место? У меня возникли сомнения потому, что докер использует overlayfs2 за которым стоит union mount. https://docs.docker.com/glossary/?term=overlay%20storage%20driver
        То есть нужно точно знать что все действия развертыванию, компиляции с её временными файлами, деплою и удалению инструментария происходят в одном и том же обязательно последнем слое фс.
        В противном случае никакого фактического освобождения места может не оказаться.


  1. darkslave
    28.06.2018 20:09

    У Debian есть slim сборка, в которой вырезаны лишние зависимости и утилиты. Аналогичные сборки есть у дистрибутивов на основе debian.
    К примеру образ на alpine python:3.7-rc-alpine весит 94,5Мб, а образ на debian 9 slim python:3.7-rc-slim всего 143Мб, что не многим больше.


  1. Ipeacocks
    28.06.2018 20:12

    Ну в принципе вы не открыли Америку. Если много зависимостей, то Альпайн — это ещё та заноза!


  1. Stas911
    29.06.2018 16:43

    Список зависимостей внушает. Страшно представить сколько пришлось все это собирать.

    На тему размера — приятно, конечно, иметь образ поменьше, но во всем нужен баланс: если посчитать стоимость времени инженера и стоимость стораджа — может оказаться, что и фиг с ним, с местом.


    1. Renatk Автор
      29.06.2018 16:56

      У меня Alpine в облаке собирался приблизительно за один час.
      Бесплатный hub.docker.com собирает около 2х часов