0
Обложка: Dockerize Python: создаём образ Docker из приложения на Python

Dockerize Python: создаём образ Docker из приложения на Python

Андрей
Андрей
Noveo Developer

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

1. Article

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

1. Docker и Docker Compose (книга Docker Deep Dive рекомендуется к изучению).

2. Github Actions и Gitlab CI.

3. Task для локальных pipeline workflow.

4. Python frameworks: Django, FastAPI, Flask. Python ORMs + migrating libraries: Django ORM + SQLALchemy/Alembic.

5. Self hosted Github and Gitlab runners with available Docker in Docker.

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

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

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

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

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

Docker является фундаментальным кирпичиком, на основе которого идет дальнейшее построение веб-инфраструктур — Docker Сompose, AWS Elastic Beanstalk, AWS ECS, Kubernetes.

Системы контейнерного оркестрирования запускают множество контейнеров на разных машинах для горизонтального масштабирования нагрузки на сервера (увеличивать количество серверов вместо того, чтобы повышать мощность одного единственного), а также для обеспечения отказоустойчивости инфраструктуры.

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

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

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

Использование Docker или его аналогов в мире веб-разработки является хорошим шагом даже для самых простых случаев. Для примера, установка WordPress требует конфигурации SQL-базы данных MariaDB / MySQL, Apache Web Server в режиме PHP-сервера, который запустит WordPress. Незнакомый с этими технологиями человек может потратить несколько дней на то, чтобы разобраться, как их запустить все вместе. С использованием Docker Сompose это сводится к трем шагам:

1. Установить Docker.

2. Написать в файлик docker-compose.yml следующее:

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 минут.

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

  • база данных PostgreSQL,
  • веб сервер Gunicorn,
  • Celery Beat Producer — периодические задачи внутри Python через Message Queue,
  • Сelery Worker — обработчик задач,
  • 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

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

— poetry documentation,

— pipenv documentation,

— pip documentation.

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 это решается через django-cors-headers.

— Для FastAPI — через встроенную библиотеку CORSMiddleware.

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

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

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

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

— Django Deployment (Там есть и полезный checklist для деплоя),

— FastAPI Deployment,

— Flask deployment.

Для 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:

— для Github Actions,

— для Gitlab CI.dockerize python

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, то может как минимум понадобиться установка библиотек компиляции 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).