Dockerize Python#

  • Репозиторий с примерами кода для данной статьи: Github

1. Article#

1.0 Используемые технологии#

1.1 Вступление#

Docker - технология заменяющая виртуализацию. В рамках современной web разработки, можно сказать что Docker является аналогом компиляции приложению в один бинарный файл. Для таких программных языков как Javascript, Python, PHP, Ruby, зачастую это единственный способ заморозить библиотеки зависимостей для получения неизменного артефакта (одного файла) который мы можем поочередно запустить и протестировать на машине разработчика, в облачном тестировочной полигоне и тот же самый запустить на прод.

Docker пришел на смену виртуализации как более легковесный аналог, который можно запускать десятками на одной машине, из за того что он переиспользует Ядро Оси. Контейнеризация аналогично виртуализации служит для более полного использования ресурсов каждой машины, она позволяет нам запустить множество различных приложений на одной машине без конфликтов между друг другом.

Docker является фундаментальным кирпичиком, на основе которого идет дальнейшие построение веб инфраструктур. Docker-compose, AWS Elastic Beanstalk, AWS ECS, Kubernetes. Системы контейнерного оркестрирования запускают множество контейнеров на разных машинах, для горизонтально масштабирования нагрузки на сервера (увеличевать кол-во серверов вместо того чтобы повышать мощность одного единственного), а так же для обеспечения отказоустойчивости инфраструктуры. Концепция докера хороша зачастую тем, что все приложения в обычном случае не имеет сохраняемого состояния, и мы можем заменить одну версию приложения на другую без загрязнения операционной системы на которой запускаем контейнеры. Контейнер внутри содержит внутри файловую систему в достаточном количестве для имитации независимой операционной системы.

Использование контейнеризации так же приводить к тому что любые приложения становяться мультитенантными по умолчанию. То есть множество раз можно запустить одно и то же приложение безо всяких конфликтов, даже если они внутри контейнера используют совершенно одинаковые ресурсы файловой системы.

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

Использование Docker или его аналогов в мире веб разработке является хорошим шагом даже для самых простых случаев. Для примера, установка Wordpress, требует конфигурации SQL базы данных Mariadb / MySQL, Apache web server в режиме PHP сервера который запустит Wordpress. Человек новый к данным технологиям может потратить несколько дней пока худо бедно не разбереться как их запустить все вместе. С использованием Docker-compose это сводиться к трем шагам.

services:
  db:
    # We use a mariadb image which supports both amd64 & arm64 architecture
    image: mariadb:10.6.4-focal
    # If you really want to use MySQL, uncomment the following line
    #image: mysql:8.0.27
    command: '--default-authentication-plugin=mysql_native_password'
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=somewordpress
      - MYSQL_DATABASE=wordpress
      - MYSQL_USER=wordpress
      - MYSQL_PASSWORD=wordpress
    expose:
      - 3306
      - 33060
  wordpress:
    image: wordpress:latest
    ports:
      - 80:80
    restart: always
    environment:
      - WORDPRESS_DB_HOST=db
      - WORDPRESS_DB_USER=wordpress
      - WORDPRESS_DB_PASSWORD=wordpress
      - WORDPRESS_DB_NAME=wordpress
volumes:
  db_data:
  • 3) Написать команду docker-compose up -d (-d флаг запуска в фоновом режиме)

На все про все, и 5 минут может не уйти.

В рамках работы с Python web приложений, даже в тривиальном случае необходимо обычно множество запущенных процессов к приложению

  • 1) База данных Postgresql
  • 2) Веб сервер Gunicorn
  • 3) Celery Beat Producer - периодические задачи внутри Python через Message Queue
  • 4) Celery Worker - обработчик задач
  • 5) Redis - очереди для Celery задач, или в качестве Key-Value хранилища кеша быстрого доступа, который мы можем использовать из питона

Часто должны быть установлены какие нибудь дополнительные Linux библиотеки по компиляции C кода, или gettext для переводов. Все что надо установить и не упомнишь без какого либо средства автоматизации. Удобство контейнеризации Докер в том что... можно смело его использовать даже во время разработки на своей машине. Все равно все наши контейнеры можно остановить и удалить безо всяких загрязнений системы (к примеру эти команды полностью очистят машину ото всех работавших докер приложений (docker stop $(docker ps -q), docker system prune -a)

С учетом всех достоинств контейнеризации, можно смело заявить что она убирает такое количество нагрузки с плеч разработчика, что является таким же необходимым инструментом как файл с зависимостями к любому веб приложению (package.json / requirements.txt и тд). Зачастую Docker является единственным способом быстро вспомнить и настроить окружение как работать с каким либо веб проектом.

Если хотите попробовать контейнеризацию в самом щадящем варианте, изучите Docker Deep Dive, и попробуйте Docker и Docker-compose локально на машине. В рамках прод проектов, можно начать с использования Digital Ocean App Deployment поддерживающий деплой докер изображений, а так же AWS Lightsail Container Deployment и AWS Elastic Beanstalk являются самыми простыми вариантами в рамках AWS. Beanstalk решает так же из коробки горизонтальное масштабирование системы.

В связи с этим рассмотрим более подробно как контейнизировать веб приложения питона, и подготовить их к минимальному использованию не в режиме дев окружения. Подобная инструкция вам может особенно пригодиться если ваша роль Devops engineer, который отвечает лишь за инфраструктуру системы, но может быть мало знаком с отдельными языками программирования и особенностями его контейнеризации, такими как Python. А так же вам это пригодиться если вы просто Python Backend разработчик, и хотите настроить свое дев окружение в более удобном и задокументированном варианте, на уровне большем чем просто использование virtual venv (Ваш DevOps engineer или тот кто ответственен за деплой системы очень обрадуется увидев Docker файл к проекту).

1.2 Зависимости#

В Python наличествует как минимум 5 способов установки зависимостей к проекту

  • pip менеджер по умолчанию, установка файлов вида requirements.txt, requirements.dev.text, constraints.txt
  • Установка requirements.txt через venv
  • Установка зависимостей через Pipenv package manager
  • Установка зависимостей через Poetry package manager
  • Установка зависимостей через Conda (common for data science/machine learning projects)

4 из них продемонстрированы в Dockerfile для Flask простого проекта.

dep-poetry, dep-pipenv, dep-pip, dep-venv docker stages демонстируруют одноименные установки зависимостей

FROM python:3.10.5-slim-buster as base

# This flag is important to output python logs correctly in docker!
ENV PYTHONUNBUFFERED 1
# Flag to optimize container size a bit by removing runtime python cache
ENV PYTHONDONTWRITEBYTECODE 1
WORKDIR /code


FROM base as dep-poetry
ENV POETRY_HOME /opt/poetry
RUN python3 -m venv $POETRY_HOME
RUN $POETRY_HOME/bin/pip install poetry==1.2.2
ENV POETRY_BIN $POETRY_HOME/bin/poetry

COPY pyproject.toml poetry.lock ./
RUN $POETRY_BIN config --local virtualenvs.create false
RUN $POETRY_BIN install --no-root
COPY src src


FROM base as dep-pipenv
RUN pip install pipenv
COPY Pipfile Pipfile.lock ./
RUN pipenv install --system
COPY src src


FROM base as dep-pip
COPY requirements.txt constraints.txt ./
RUN pip install -r requirements.txt -c constraints.txt
COPY src src


FROM base as dep-venv
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY requirements.txt constraints.txt ./
RUN pip install -r requirements.txt -c constraints.txt
COPY src src

Альтернативные варианты установки и настроек см. по следующим адресам:

1.3 Веб серверы#

Помимо этого нужно учесть то что встроенные веб сервера в Flask/Django/FastAPI запускаемые через python3 entryfile.py по умолчанию подразумеваются для использования лишь в разработке.

Python web servers разделаются на две категории:

  • WSGI based веб сервера (вида Gunicorn), использование которого подразумевается для sync Python кода (не содержающего async инструкций).
  • и ASGI based веб сервера (вида Uvicorn) подразумеваемые к использванию для питон кода содержащего асинхронный код. Использование Uvicorn для прода с асинхронным кодом подразумевается в паре с Gunicorn, который имеет Uvicorn workers.

При этом дополнительный момент учесть... питон веб сервера не способны возвращать static assets css/js/jpeg and etc. Лучшей рекомендаций сегодня является использовать хотябы Nginx в режиме reverse proxy к питон веб серверу и настроенный так же возвращать static assets. Не в коем случае не стоит использовать библиотеки White noise, данное решение страшно глючит и медленно работает даже для одного пользователя.

1.3.1 Django (Sync)#

Django обычно достаточно настраивать для синхронного кода через WSGI. (Django Channels с веб сокетами для реализации живых чатов впрочем существует и ему нужно ASGI). Пример настроенного Django работающего через Gunicorn-WSGI с Nginx reverse proxy serving static assets. Для данной настройки достаточно установить gunicorn через текущий используемый package manager (pip/poetry/pipenv), и запустить веб сервер через gunicorn которому дали путь к WSGI..

В prod варианте деплоя Python мы как минимум всегда можем указывать количество workers парарелльно обратаывающие запросы gunicorn src.core.wsgi -b 0.0.0.0:8000 --workers 2.

P.S. Django с отключенным settings.Debug = False перестает показывать static assets даже в дев сервере работающим через python3 manage.py runserver

1.3.2 FastAPI (Async)#

Пример настроенного асинхронного проекта на FastAPI.

Запуск меняется на gunicorn src.core.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

Помимо смены веб сервера с WSGI на ASGI, асинхронные фреймворки питона требует так же асинхронно дружелюбные библиотеки и драйвера для Postgresql в том числе. Если для синхронного Django нам достаточно установить psycopg2-binary pip package (Обратите внимание что psycopg2-binary не требует установки C компилирующие зависимостей в отличии от psycopg2 библиотеки). То для асинхронного фреймворка нам нужно так же установить asyncpg для Postgresql)

Аналогично все библиотеки в асинхронном фреймворке должны использоваться async дружелюбные. aiohttp вместо requests к примеру для выполнения сетевых запросов.

1.4 CORS headers#

Самая частая последняя проблема при деплое CORS headers которые должны быть часто настроены.

Примеры проектов Django с Nginx в режиме CORS разрешающим все можно найти здесь.

И пример для FastAPI Здесь

1.5 Помимо прочего#

Каждый из питон фреймворков имеет богатую документацию деплоя с разными решения

Для Django, FastAPI (и минималистичный вариант под минималистичный Flask) в рамках данной статьи предоставлены варианты конфигурации контейнеризации с Docker-compose и настроенными выполнениями unit тестов создающими объекты в postgresql через Pytest framework.

Обратим внимание, что Django ORM миграции БД создаваемые через python3 manage.py makemigrations должны быть закомиттены в репозиторий, и могут применяться к БД через python3 manage.py migrate.

Для FastAPI и Flask частым решением используется SQLALchemy для ORM и Alembic для миграции БД. alembic -c src/alembic.ini revision --autogenerate -m "migration_name" для создание миграций БД, alembic -c src/alembic.ini upgrade head - для применения всех до последней миграций к бд

1.5.1 Static assets#

Фреймворку возможно еще будет нужно добавить параметр куда собирать static assets и во время сборки докер изображения их собрать туда

Django: ./manage.py collectstatic --noinput --clear (см. STATIC_URL / STATIC_ROOT в settings.py)

1.5.2 Translations#

Если в фреймворке используеться translations, понадобяться дополнительный шаг во время сборки Docker изображения для этого.

  • Django: python3 manage.py compilemessages
  • Flask: инструкции здесь если используеться Flask-Babel
  • FastAPI: Для FastAPI очевидных решений нет, возможно используется gettext
1.5.3 CI pipeline build && test#

Предоставлены примеры настройки CI agnostic pipeline workflows через task (которые можно исполнять локально!) и docker-compose:

2. FAQ#

Общее для проектов на Python#

Приложение является самостоятельным или это виртуальный хост для  веб-сервера?#

В общем случае Python веб приложения самостоятельны и деплояться через Gunicorn/Uvicorn/WSGI/ASGI и прочие веб серверы питона (см. полный список в ссылках документации по деплою фреймворков в главе 1.5)

Однако Питон веб сервера не могут возвращать static assets css/js/jpeg и тд. В этом случае их возвращают через Nginx работающим заодно в режиме reverse proxy к питон серверу.

Так же Nginx и прочие reverse proxy, используются для аугментации вида... регулирования headers, добавить client side или server side caching.

В мало популярном случае питон веб сервер можно напрямую сдеплоить через Apache mod_wsgi к примеру, но это является мало популярным решением. Если так хочеться feature rich возможностей, посмотрите в сторону uWSGI (Очень много фич).

И хоть и некоторые питон сервера могут TSL сертификаты прикрепить, все же проще через Nginx это сделать или иные внешние решения.

Приложение можно запустить в одном контейнере?#

Да, можно. Если в приложении нету никаких добавок вида Celery (Message Queue), и если static assets нет необходимости возвращать (Для REST API в общем случае не нужно, если только не Django приложение с используемым Django Admin интерфейсом)

и если для кеширования не нужен Redis / или Redis деплоиться где то отдельно.

Иначе для одного даже dev environment используется множество контейнеров, контейнер под postgresql, redis, основной web server, celery beat(cron like message queue task producer), celery worker (message queue worker), celery flower (message queue monitoring) + nginx (в качестве reverse proxy + serving static assets)

см. пример возможного большого колва контейнеров

Требуется ли приложению установка зависимостей?#
  • Да, читатайте статью от главы 1.2 Зависимости
  • Если проект применяет вставки C кода или какого либо вида golang вызывать в python. Может понадобиться установка библиотек компиляции C кода как минимум. Большие детали по разным вариантам описаны в книге Python Expert Programming 4th edition в главе C extensions
Best-practices по контейнеризации#
  • Те же что и везде, сжимать в один шаг установку и очистку кеша
  • Использовать multi staging... Который в основном не нужен так как компилируют веб приложения к бинарникам редко
  • Сначала установить зависимости, потом копировать остальной код
  • Нормально Python код иметь собранными в packages от root folder, чтобы PYTHONPATH хаков с обнаружением модулей и packages не потребовалось (root папка не должны иметь __init__.py файл, а каждая копируемая папка с python кодом должна иметь __init__.py на всех уровнях. И пути импорта прописаны по человечески абсолютные от root folder или относительные)
  • Использовать ENVIRONMENT variables, а не .env файлы (их можно разве что для локальной дев разработки)
  • Если настроят logging library вместо print, то совсем молодцы
  • флажок ENV PYTHONUNBUFFERED 1 нужен чтобы логи нормальны из контейнеры вылезали
  • флажок ENV PYTHONDONTWRITEBYTECODE 1 тоже можно поставить, все равно кеш питон кода в контейнере лишь место лишнее занимать будет
  • не забывать что assert синтаксис используеться лишь в тестировании, а для прода может быть и выключен. Так что лучше его не иметь в рабочем коде.
  • Если будете копировать venv папку с уже установленными зависимостями когда либо в контейнер, учтите что его абсолютный путь не должен меняться, либо он сломаеться. Но вообще его копировать это моветон, устанавливайте зависимости в контейнерах во время сборки xD
  • не копируйте мусор вида __pycache__ в контейнер, настройте .dockerignore
  • Как минимум настроить масштабирование количества процессов-workers (Для более feature rich вариантов можно посмотреть в сторону uWSGI)
Нужны ли дополнительные скрипты (bash etc) для сборки/запуска приложения?#

Для веб приложений обычно нет, однако c Makefile, или task или paver жить проще. Или просто делая скрипты с argparse встроенной библиотекой. Или через click. Все индивидуально для веб проектов.

Для приложений собираемых в бинарники файла вида setup.py от https://pypi.org/project/setuptools/, либо иные могут наличествовать чисто питон скрипты/библиотеки для сборки проектов. Список достаточно частых решений можно составить для данного случая. В рамках данной статьи сборку бинарников а так же публикацию библиотек на pypi мы не рассматриваем.

Что обычно кэшируется в CI/CD пайплайне ?#

при контейнеризации нам ничего кешировать и не нужно так то. Однако если бы этого не было, можно было бы закешировать устанавливаемые pip packages или используемый venv (под капотом он используеться для каждого из менеджеров почти (poetry/pipenv). ОДнако в зависимости от Package manager отличается путь где кешировать их зависимости)

Gitlab CI template for python

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

# Pip's cache doesn't store the python packages
# https://pip.pypa.io/en/stable/topics/caching/
#
# If you want to also cache the installed packages, you have to install
# them in a virtualenv and cache it as well.
cache:
  paths:
    - .cache/pip
    - venv/

Frameworks#

Существуют ли еще какие-либо предварительные процедуры для приложения, кроме установки зависимостей?

Django#

Да в статье в большинстве перечислено:

  • Смена dev сервера на боевой WSGI (для синхронного питона) или ASGI (для асинхронного питона)
  • Установка питона нужной версии (или использовании Docker image с нужной питон основой)
  • Установка pip если отсутствует (python3 -m ensurepip) для установки дальнейшей зависимостей
  • Установка используемого package manager (pipenv, poetry)
  • Установка зависимостей
  • Отключение дебага
  • Смена Django секрета на что нибудь другое из ENV
  • Настройка env переменных через os.environ или альтернативные решения
  • Настройка CORS headers
  • Настройка куда static assets собирать и скомпилировать их в одну папку (если используются html возможности Django)
  • Компиляции переводов если используется (python3 manage.py compilemessages), установка OS зависимостей вида gettext
  • При использовании библиотек питона с компиляцией через C, прочие дев инструменты компиляции должна быть установлены
  • Мигрировать бд потом через python3 manage.py migrate

См. пример докер файла для Django

FastAPI#

В основном повторяет Django шаги, некоторые вещи повторно не упоминаются:

  • Смена сервера на боевой асинхронный ASGI сервер (Для примера Gunicorn с Uvicorn workers), с увеличением колва workers
  • Настройка CORS headers
  • Установка используемого package manager (pipenv, poetry)
  • Установка зависимостей
  • Мигрировать бд потом для SQLALchemy через: alembic -c src/alembic.ini upgrade head (или иной используемый ORM)

См. пример докер файла для FastAPI

Flask#

В основном повторяет Django шаги, некоторые вещи повторно не упоминаются:

  • Смена dev сервера на боевой WSGI (для синхронного питона) или ASGI (для асинхронного питона)
  • Отключение дебага
  • Установка используемого package manager (pipenv, poetry)
  • Установка зависимостей
  • Настройка CORS headers
  • Настройка куда static assets собирать и скомпилировать их в одну папку (если используются html возможности Django)
  • Компиляции переводов если используется (Flask-Babel?), установка OS зависимостей вида gettext
  • Мигрировать бд потом для SQLALchemy через: alembic -c src/alembic.ini upgrade head (или иной используемый ORM)