Алоха! Всё, что написано ниже, является прямым продолжением предыдущего материала, и если ты его пропустил, то многое покажется тебе непонятным или неочевидным. Поэтому я рекомендую не торопиться и соблюдать последовательность. И ещё важный момент — если ты совсем не знаком с Docker, то прежде чем двигаться дальше, обязательно почитай про основы данной технологии, благо статей в интернете немало, например, вот. Или хотя бы ознакомься с моей памяткой (VK, Github). Просто чтобы понимать, о чём вообще речь. Docker — это платформа контейнеризации с открытым исходным кодом, с помощью которой можно автоматизировать создание приложений, их доставку и управление. Платформа позволяет быстрее тестировать и выкладывать приложения, запускать на одной машине требуемое количество контейнеров. Благодаря контейнеризации и использованию Docker, разработчики больше не задумываются о том, в какой среде будет функционировать их приложение и будут ли в этой в среде необходимые для тестирования опции и зависимости. Достаточно упаковать приложение со всеми зависимостями и процессами в контейнер, чтобы запускать в любых системах: Linux, Windows и macOS. Платформа Docker позволила отделить приложения от инфраструктуры. Контейнеры не зависят от базовой инфраструктуры, их можно легко перемещать между облачной и локальной инфраструктурами. Не переживай, супер глубокое понимание технологии в рамках данного материала не потребуется. Но если всё-таки очень хочется погрузиться, то на Хабре есть перевод серии статей от Джеффа Хейла. К сожалению, в python-мире до сих пор повсеместно применяется неизолированный запуск приложения и его инфраструктуры на личных устройствах. Боюсь, даже опытные специалисты неохотно используют контейнеризацию, хотя в действительности её плюсы неоспоримы. Во-первых, она позволяет при локальном запуске повторить среду продакшена, что может уберечь от многих неочевидных ошибок. А во-вторых, при переезде с компа на комп или при появлении нового разработчика не придётся в сотый раз корячиться с настройкой приложения и инфраструктуры. Конфигурация производится лишь однажды и в дальнейшем просто поддерживается в актуальном состоянии. Разумеется, «контейнерная» разработка, как и всё в нашем мире, имеет свои недостатки. Главный из них сопряжён с локальной отладкой приложения — при запуске через системный интерпретатор сделать это куда проще. Однако технологии тоже не стоят на месте, и такие популярные IDE, как PyCharm Professional и VS Code, уже способны справиться с данной задачей. Отдельной строкой следует упомянуть ещё один нюанс: при работе с докером на чипах M1 можно напороться на трудности. Не всегда очевидные, но решаемые, и ситуация постоянно улучшается. Чтобы понимать, к чему мы вообще стремимся, предлагаю тебе сначала установить Docker и Docker Compose. Затем стяни этот репозиторий и перейди в директорию текущего урока: В ней создай папку secrets и положи туда два файла: event_broker_password и service_db_password. В первый файл впиши python_garden, а во второй — postgres. Проверь, всё ли правильно получилось? (Команда cat выводит содержимое файла). Введи в терминале из корня проекта следующую команду: После того, как её выполнение закончится, введи: В результате у тебя должно получиться следующее: Если ты счастливый обладатель компа с чипом M1, то смотри README. Теперь, чтобы убедиться в правдивости напечатанных статусов, выведи логи любого сервиса при помощи команды docker compose logs <service> (имена в столбце SERVICE). Также можешь дёрнуть апишку из предыдущего урока обычным курлом: curl http://0.0.0.0:8000. При желании открой адрес http://0.0.0.0:8000 в браузере и убедись в наличии идентичного ответа. Ну и напоследок небольшой бонус. Зайди на http://0.0.0.0:15672, введи в оба поля python_garden и нажми «Login». Добро пожаловать в веб-интерфейс локального RabbitMQ! Вот так просто. Прежде чем продолжить, давай откатимся в самое начало и удалим всё, что мы создали на данном этапе. Введи поочерёдно следующие команды: Знакомство с контейнеризацией мы начнём с описания докер-файла (./Dockerfile), где будут перечислены инструкции для сборки нужного нам образа. 🔹 В самой первой строке мы ссылаемся на базовый образ из публичного репозитория. Можешь выбрать любой другой из списка, если тебя, скажем, не устраивает версия. Только прошу обратить внимание: лучше брать slim-образы, поскольку при работе с alpine ты неизбежно столкнёшься с трудностями во время установки некоторых библиотек. Также замечу: во всех уважающих себя компаниях образы хранятся в своих, закрытых репозиториях, так что навряд ли тебе придётся где-то, помимо собственных проектов, обращаться к Docker Hub напрямую. 🔹 Во второй строке указан ответственный за создание и поддержку образа. Обычно здесь оставляют name и email команды разработчиков. 🔹 В блоке «Сборка зависимостей» мы сначала объявляем переменную BUILD_DEPS, в которой перечисляем необходимые утилиты и функции ОС, затем запускаем обновление списков пакетов и устанавливаем перечисленные сущности. Инструкцию ARG вместе с переменной можешь опустить, объединив со следующей. Это лишь вопрос эстетики и удобства. Главное, никогда без веской причины не запускай в докер-файле команду apt-get upgrade, которая часто встречается в различных сниппетах рядом с apt-get update. Запомни разницу (подробности здесь): apt-get update — обновляет списки пакетов, т.е. получает информацию о новейших версиях и зависимостях; apt-get upgrade — на основе существующих списков (/etc/apt/sources.list) запускает обновление всех установленных в настоящее время пакетов. Допустим, мы ожидаем некий пакет версии «1.0», установленный в базовом образе, и ориентируемся на вполне конкретное поведение. А после апгрейда получаем, к примеру, версию «3.5» с совершенно иным поведением. Думаю, ты представляешь степень боли, которую нам предстоит испытать в подобном случае. 🔹 Далее принимаемся за установку poetry. Это модный и полезный менеджер зависимостей для Python. Если ты с ним ещё не знаком, то пока не заморачивайся — обсудим его как-нибудь отдельно. Просто пропускай обе инструкции. 🔹 В «Инициализации проекта» проводим подготовительные мероприятия, а именно: задаём рабочую директорию для всех последующих директив; указываем sh-скрипт с вариантами запуска нашего приложения (о них ниже); устанавливаем две переменные среды, которые очень важны. PYTHONUNBUFFERED отвечает за отключение буферизации вывода (output). То есть непустое значение данной переменной среды гарантирует, что мы можем видеть выходные данные нашего приложения в режиме реального времени. PYTHONDONTWRITEBYTECODE означает, что Python не будет пытаться создавать файлы .pyc. Однажды мне довелось столкнуться с результатами отсутствия этой переменной. Из-за невыясненного сбоя при сборке контейнера исходный код не скомпилировался заново в байт-код. В итоге получилось, что код внутри обновлённого контейнера и код локальный отличались, следовательно, отличалось и поведение. Было очень трудно выявить причину проблемы. Добавляй эти две переменные в Dockerfile всегда! Даже если пишешь на Go. Просто на всякий случай. 😁 🔹 В предпоследнем блоке устанавливаем зависимости. Для poetry, когда речь идёт о контейнере, отключаем создание виртуального окружения (virtualenvs.create false), интерактив (--no-interaction) и ANSI-output (--no-ansi). Если же от poetry ты решил держаться подальше, то запускай: 🔹 Последняя директива в дополнительных комментариях не нуждается: весь проект просто копируется внутрь контейнера, в WORKDIR. Итак, мы пробежались по всем инструкциям, указанным в нашем Dockerfile. Теперь давай сбилдим образ! Для этого запусти команду: Здесь -t означает имя нашего образа, а . указывает текущую директорию в качестве целевой. После успешной сборки ты можешь посмотреть список всех имеющихся образов при помощи команды docker images и найти там наш, свеженький. Далее приступаем к описанию sh-скрипта так называемой точки входа, она же entrypoint. Точка входа — это парадные ворота в наш сервис, которые мы обозначили в докер-файле командой ENTRYPOINT ["./docker-entrypoint.sh"]. А sh-скрипт, продолжая аналогию, что-то вроде табличек-указателей или ресепшена в отеле. Напомню: у нашего нового бэкенд-приложения из первого урока есть два режима работы — API и консьюмер. Явно опишем оба варианта в sh-скрипте, также размещённом в корне проекта (./docker-entrypoint.sh), и не забудем оставить возможность для запуска иных команд. На данном этапе у тебя может возникнуть вопрос: «А зачем такие сложности? Почему бы не указать команду запуска сервиса прямо в dockerfile через директиву CMD с возможностью переопределения?». Ты прав — если речь идёт о простом приложении с одним вариантом запуска, то смысла городить огород с точкой входа, разумеется, нет. Однако в природе такие приложения попадаются очень редко. Посуди сам, даже в Django, помимо основной команды запуска python manage.py runserver, у нас имеются следующие: создание новой миграции, применение миграций, запуск какого-нибудь вспомогательного скрипта, запуск тестов и так далее. Вспоминай первый урок и оставленную в тезисах для запоминания ссылку на принцип единственной ответственности. При использовании точки входа процесс создания образа отделяется от слоя запуска сервиса и сервисных функций. При изменении списка входных команд мы не будем трогать dockerfile, и как следствие, нам не придётся во время локальной работы пересобирать сам образ. Что касается продемонстрированного выше скрипта, то он, думаю, трудностей в понимании у тебя не вызовет: имеются две строго декларированные команды и отдельная ветка для всех остальных. Тут следует остановиться подробнее разве что на exec. По рекомендации разработчиков Docker, запуск процессов внутри контейнера лучше осуществлять через неё, ибо тогда вызываемая команда получит PID 1 и будет корректно работать с сигналами. Если же exec не указать, то PID 1 достанется процессу bash. Мы почти у цели! Осталось описать инфраструктуру приложения. Для этого воспользуемся инструментом под названием Docker Compose. Если говорить упрощённо, то данная технология позволяет с помощью простых команд контролировать несколько сервисов. Там же, в корне проекта, разместим файл docker-compose.yml, где будут храниться конфиги всех сервисов инфраструктуры. Принимаемся за настройку базы данных. Здесь указаны: имя контейнера; образ, на основе которого будет собран контейнер; соответствие портов на хост-машине портам в контейнере, то есть при подключении к порту № 1 мы будем попадать на порт № 2 в контейнере; и переменные окружения, по сути являющиеся кредами для доступа к БД. Для нашего примера — да и вообще для большинства локальных развёртываний PostgreSQL — этой конфигурации будет достаточно. Однако если хочешь, можешь покопаться в официальной доке и познакомиться с полным перечнем доступных настроек. Теперь давай обратим внимание на RabbitMQ. Как видишь, различий немного. Попробуем поднять нашу инфраструктуру. Для этого введи в терминале из корня проекта следующую команду: И посмотри результат: Если статусы у обоих сервисов — running, ты всё сделал правильно. Если нет, значит, я что-то упустил в своих объяснениях. Обязательно напиши об этом в комментариях! Ну что? Вот мы и добрались, наконец-то, до нашего псевдобэкенда. Давай опишем его. Новых ключей здесь немного. Начнём с build. Это раздел с настройками для сборки образа. То есть, если у тебя на момент запуска контейнера отсутствует образ python_garden, Docker Compose при помощи параметров в разделе build соберёт его сам. Параметр context: . в данном случае сообщает, что Dockerfile располагается в текущей директории. Ключ command переопределяет команду по умолчанию, указанную в директиве CMD. В нашем случае такая инструкция отсутствует, поэтому command её заменяет. Как ты помнишь, api и consumer у нас явно описаны в docker-entrypoint.sh. По поводу volumes смотри уже упомянутую памятку по Docker. Внешний том нам здесь нужен для того, чтобы после каждого изменения кода не приходилось пересобирать образ. Помнишь, при сборке задействовалась инструкция COPY? После её выполнения в образе сохраняется кодовая база определённой версии, а с помощью вольюмов мы обходим это ограничение. То есть при указанном volumes образ смотрит в папку с проектом и по команде docker compose restart <service> поднимает контейнер с обновлённой кодовой базой. Посмотреть файл с настройками Docker Compose целиком можно по ссылке. Дабы убедиться, что всё работает как надо, вернись в раздел «Тизер» и проделай описанные там действия. Согласись, это очень удобно, когда ты скачиваешь проект с гитхаба и одной командой можешь запустить его вкупе с зависимыми сервисами. Только не забывай поддерживать актуальность docker-compose.yml и docker-entrypoint.sh. Бывают случаи, когда могут понадобиться какие-то локальные доработки — например, имя контейнера не по душе или ты используешь устройство с процессором M1, тогда как коллеги сидят на православном Intel. Разработчики предусмотрели такую ситуацию, так что не вздумай ломать работающий файл docker-compose.yml! Вместо этого создай в корне проекта docker-compose.override.yml и изменяй существующие настройки, как тебе вздумается. Общий, совмещённый результат конфигурации ты можешь посмотреть с помощью команды: В этой статье намеренно отсутствует конкретика и не используется в качестве примера какой-нибудь полурабочий прототип, чтобы на выходе у тебя получился шаблон микросервиса. Забирай результат, правь под себя и пользуйся. Старайся использовать slim-образы Python, поскольку при работе с alpine ты неизбежно столкнёшься с трудностями во время установки некоторых библиотек. Не запускай в докер-файле команду apt-get upgrade. Всегда добавляй в Dockerfile переменные окружения ENV PYTHONDONTWRITEBYTECODE 1 и ENV PYTHONUNBUFFERED 1. По рекомендации разработчиков Docker, запуск процессов внутри контейнера лучше осуществлять через exec, ибо тогда вызываемая команда получит PID 1 и будет корректно работать с сигналами. Помни про volumes. Без указания внешнего тома тебе придётся постоянно пересобирать образ. Поднять в фоне все сервисы, описанные в конфигурации: Вывести список контейнеров: Вывести логи указанного сервиса (контейнера): Рестартануть сервис: Посмотреть итоговую конфигурацию контейнеров: Остановить все контейнеры из текущей конфигурации: Удалить все контейнеры из текущей конфигурации: Остановить и удалить все контейнеры из текущей конфигурации (последовательное выполнение двух предыдущих команд): Удалить все не запущенные контейнеры и все не использующиеся образы: Вывести список образов:  Python developer