Ох уж этот Docker, а с ним и Docker Compose… Хотя давайте все по порядку.
По моему плану, деплой Telegram бота на сервер должен быть осуществлен с помощью средств контейнеризации. На данный момент я более или менее знаком с Docker и Docker compose. Именно с помощью этих инструментов на учебе в Яндекс.Практикуме мы деплоили сайты, backend которых мы писали на Django, а frontend был написан на React. А раз так, это хороший повод повторить пройденный материал на практике и возможность узнать что-то новое.
Напоминаю, что код бота доступен в репозитории на GitHub. Если есть вопросы, критические замечания или предложения, пишите мне в Telegram, в Вконтакте или на почту bks2408@mail.ru
И еще, все публикации, посвященные разработке Telegram bot Work for everyone, вы можете найти в специальной рубрике Telegram bot
Содержание:
- Постановка задачи
- Dockerfile для образа Telegram бота
- Полный код Dockerfile
- Docker compose для Work for everyone
- Полный код файла docker-compose.yml
Постановка задачи
Задача такая: запустить Telegram bot Work for everyone так, чтобы он работал в Docker контейнерах и его легко было развернуть на любой системе, вне зависимости от окружения. Если разбить эту задачу на подзадачи, то мне нужно:
- написать Dockerfile для сборки образа Telegram бота;
- в docker-compose описать три сервиса: Telegram bot, PostgreSQL и Redis;
- на основе подготовленного docker-compose запустить контейнеры;
- убедится, что бот работает так, как этого требует ТЗ.
Задачи поставлены, вперед!
Dockerfile для образа Telegram бота
В директории bot создаю файл с именем Dockerfile. В Dockerfile будут находиться инструкции, на основе которых docker создаст образ. Как отмечено в документации к Docker, инструкции представляют собой команды, которые может ввести пользователь в командной строке.
Вот тут вы можете посмотреть, какие инструкции можно использовать в Dockerfile. К каждой инструкции есть описание.
Предлагаю пройтись по Dockerfile, который я написал, и поближе познакомиться с использованными инструкциями.
Инструкция FROM
Пример использования инструкции FROM
FROM python:3.11
Описание инструкции FROM
Инструкция FROM определяет базовый образ, поверх которого будет собран новый образ, с учетом всех остальных инструкций в Dockerfile.
Именно с инструкции FROM должен начинаться Dockerfile. Данную инструкцию можно использовать несколько раз при многослойной сборке образа.
Здесь остановлюсь, так как есть у меня вопросы к тому, что я написал в Dockerfile при первой попытке собрать образ. В качестве базового образа я использую образ python:3.11. Но кроме такого образа, есть образы с тегами slim, alpine, bookworm. Хотелось бы понять, в чем разница и что мне в итоге лучше использовать.
Итак, базовый образ, в котором в качестве тега используется только версия Python, например python:3.11, использует полную и стабильную версию Debian Linux. В настоящее время это версия 12 с кодовым обозначением bookworm. Это можно увидеть, если посмотреть подробную информацию об образе python:3.11.8 на Docker Hub. Для такого базового образа инструкция FROM будет содержать ссылку на образ debian:12.
Кроме основного базового образа, есть и другие версии образов, в тегах которых можно встретить слово slim. Например, образ python:3.11.8-slim.
Данный тег говорит нам о том, что в качестве базового образа для него был также использован дистрибутив Debian Linux версии 12 bookworm, но только с самыми необходимыми пакетами. Инструкция для сборки такого «тонкого» образа начинается с такой инструкции FROM:
FROM debian:bookworm-slim
При использовании таких образов рекомендуется тщательно протестировать свое приложение на предмет оценки достаточности пакетов в образе для работы приложения.
Считается, что slim образы наиболее безопасны, так как в них меньше мест, которые могут сломаться.
Slim-образы могут быть построены не только на slim версии Debian Linux bookworm, но и на основе других версий Debian: bullseye, buster, stretch.
Образы, в тегах которых присутствует упоминание Alpine (python:3.11-alpine), созданы на основе Alpine Linux – легкого, простого и эффективного, с точки зрения потребления ресурсов, дистрибутива Linux. Именно так написано на официальном сайте проекта. Alpine Linux был разработан специально для работы в контейнерах.
Про Alpine образы много встречал статей, в которых не рекомендуется использовать их в качестве базового слоя для собственных образов. Тут перепечатывать их не буду. Во-первых, это не совсем корректно, а во-вторых, там используются аргументы исключительно с точки зрения опытных разработчиков и они мне далеко не все понятны. Если интересно, почитайте вот эту статью на хабре и комментарии к ней, а также можно почитать вот здесь.
Подводя итог, можно сказать, что базовым параметром, который определяет разницу между образами Python, является операционная система, ее версия и конкретный дистрибутив, на основе которой эти образы построены.
Таким образом, моя инструкция FROM теперь будет выглядеть вот так:
FROM python:3.11-slim
Но не все так гладко, как написано выше. Первый запуск показал, что чего-то не хватает для построения образа. Процесс падает на шаге установки зависимостей. Точнее на установки psycopg2 – драйвера для работы с PostgreSQL.
В документации к psycopg2 написано, что для сборки модуля необходимо наличие в системе компилятора C и некоторых дополнительных пакетов (libpq-dev).
Надо, так надо, пойду искать, как это добавить.
Решение такое: в Dockerfile добавил еще одну команду RUN, которая состоит из:
- apt-get update – обновление индексов пакетов
- apt-get -y install libpq-dev gcc – установка пакетов libpq-dev и gcc (компилятор для языка C). Ключ –y используется для ускорения установки, чтобы система не спрашивала подтверждения.
Время сборки увеличилось, но образ собран и весит в два раза меньше, чем образ на основе образа python:3.11.
Перед деплоем на сервер отдельно соберу этот образ и отправлю на Docker Hub.
Инструкция ENV
Пример использования инструкции ENV
ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1
Описание инструкции ENV
ENV – это инструкция для установки переменных среды. Установленное таким образом значение будет присутствовать в среде для всех последующих инструкций на этапе сборки. Переменные среды, установленные с помощью инструкции ENV
, будут сохраняться при запуске контейнера из полученного образа.
В моем Dockerfile установлены две переменные: PYTHONUNBUFFERED и PYTHONDONTWRITEBYTECODE.
- PYTHONUNBUFFERED позволяет направлять потоки
stdout
иstderr
прямо на терминал без предварительной буферизации. Например, журнал работы запущенного контейнера. - PYTHONDONTWRITEBYTECODE не позволяет Python записывать файлы pyc. Файл PYC представляет собой скомпилированный выходной файл, созданный из исходного кода, написанного на языке программирования Python. Когда файл py запускается с использованием интерпретатора Python, он преобразуется в байт-код для выполнения. В то же время скомпилированный байт-код также сохраняется в виде файла.
Инструкция WORKDIR
Пример использования инструкции WORKDIR
WORKDIR /app
Описание инструкции WORKDIR
Инструкция WORKDIR
устанавливает рабочий каталог для любых инструкций RUN
, CMD
и ENTRYPOINT
, которые следуют за ней в Dockerfile.
Как указано в документации Docker, если директория не существует, она будет создана, даже если не будет использоваться ни в одной последующей инструкции Dockerfile.
В моем случае создается директория app, куда и будут в дальнейшем скопированы все файлы Telegram бота, а также файл с зависимостями. Именно из этой директории будет запускаться файл bot.py.
Инструкция COPY
Пример использования инструкции COPY
COPY requirements.txt . … COPY . .
Описание инструкции COPY
Инструкция COPY предназначена для копирования файлов в файловую систему контейнера. Копирование осуществляется с учетом источника контекста сборки, то есть места расположения Dockerfile.
В моем случае таких инструкций две. Эти инструкции работают послойно.
Инструкция RUN
Пример использования инструкции RUN
RUN apt-get update \ && apt-get -y install libpq-dev gcc RUN python -m pip install --upgrade pip RUN pip install -r requirements.txt --no-cache-dir
Описание инструкции RUN
Инструкция RUN позволяет выполнять любые команды для создания нового слоя в собираемом образе.
Первоначально я использовал только две инструкции RUN. Но, мне понадобилась еще одна инструкция RUN для установки в образ необходимых компонентов. Подробнее об этом смотрите выше, в разделе описания инструкции FROM.
Остальные инструкции достаточно стандартные для приложений на Python. Одна инструкция обновляет модуль pip, а вторая устанавливает необходимые зависимости из requirements.txt, которые используются приложением. Флаг —no-cache-dir используется для аннулирования кэша pip.
Инструкция CMD
Пример использования инструкции CMD
CMD ["python", "bot.py"]
Описание инструкции CMD
В этой инструкции указываются команды. То есть, команды, которые будут запускаться при старте контейнера из данного образа. В моем случае, это привычная для запуска команда: python bot.py.
Следует обратить внимание на то, что инструкция CMD может быть только одна.
На этом инструкции, которые я использовал в своем Dockerfile для создания образа Telegram бота, закончились. Уверен, что можно сделать лучше, но опыта пока не хватает. Обязательно вернусь к этому, когда будут появляться новые знания в этой области.
Полный код Dockerfile
FROM python:3.11-slim ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 WORKDIR /app RUN apt-get update \ && apt-get -y install libpq-dev gcc COPY requirements.txt . RUN python -m pip install --upgrade pip RUN pip install -r requirements.txt --no-cache-dir COPY . . CMD ["python", "bot.py"]
Dockerfile для создания образа Telegram бота готов, можно переходить к описанию Docker compose.
Docker compose для Work for everyone
Для работы Telegram бота Work for everyone необходимо три составляющих:
- Python приложение, собственно это и есть сам бот;
- база данных PostgreSQL – для хранения данных о вакансиях;
- Redis – используется как кэш для организации временного хранения данных.
Образ для Telegram бота я создаю на основе Dockerfile, описанного выше. Образы для PostgreSQL и Redis я беру готовые с официального сайта Docker Hub.
Для того, чтобы все это заработало вместе, необходимо использовать соответствующие инструменты оркестрации контейнеров. Docker Compose для этого подходит в самый раз.
В соответствии с документацией, Docker Compose – это инструмент для определения и запуска многоконтейнерных приложений.
В корне проекта создаю файл docker-compose.yml, в котором и буду прописывать все необходимые инструкции. Так же, как и с Dockerfile предлагаю пройтись по ним, дав краткую характеристику.
docker-compose.yml начинается с указания версии формата файла. В моем случае я использую version: ‘3.7’. Как я понял, при обработке файла docker-compose.yml проверяется, поддерживаются ли указанные в файле инструкции данной версией.
Подключение volumes
Volumes (тома) – представляют собой постоянные хранилища данных, реализованных механизмом контейнеров.
При объявлении volumes задаются их имена для последующего их использования с различными сервисами. Информация, которая хранится в volumes, не теряется при перезапуске контейнеров.
Я создаю два тома: один для базы данных, другой — для Redis.
volumes: pg_bot_data: redis_data:
Далее, при описании сервисов БД и Redis с помощью инструкции volumes происходит подключение именованных томов к соответствующему сервису. Это позволяет сервисам получать доступ к данным, которые хранятся в томах.
# Подключение тома к сервису с БД volumes: - pg_bot_data:/var/lib/postgresql/data # Подключение тома к сервису с Redis volumes: - redis_data:/var/redis_data
Описание services
В моем проекте три службы, поэтому описывать их я буду так: сначала опишу службы для PostgreSQL и Redis, а затем отдельно расскажу про Telegram бота.
Описание служб я начинаю с указания образов (image), которые будут использоваться для построения контейнеров:
image: postgres:16.1 # образ для БД PostgreSQL image: redis:6.0 # образ для Redis
Далее я задаю пользовательские имена контейнеров с помощью команды container_name:
container_name: db_container container_name: redis_container
Сборка контейнеров с базой данных и Redis не возможна без указания обязательных параметров, таких как пароль и имя пользователя. Держать их в файле docker-compose, который находится в репозитории, да еще и в публичном, это похоже на самоубийство.
Все секреты хранятся в файле .env. для того, чтобы Docker Compose узнал о том, что секретные данные нужно искать именно там, используется инструкция:
env_file: .env
env_file
добавляет переменные среды в контейнер на основе содержимого файла. Путь к файлу .env определяется относительно расположения файла docker-compose.yml.
Команда restart определяет политику работы при завершении работы контейнера. По умолчанию Docker Compose не перезапускает контейнер в случае его остановки.
Возможные значения команды restart:
- always – контейнер будет перезапускаться всегда;
- on-failure –перезапуск контейнера в случае, если контейнер завершит свою работу с ошибкой;
- unless-stopped – контейнер перезапускается вне зависимости от кода ошибки, но прекращает перезапускаться при остановке или удалении службы.
Для своих контейнеров с БД и Redis я выбрал политику перезапуска всегда (restart: always).
Затем следует описание портов (инструкция ports). Сначала указывается порт хоста, а потом порт контейнера.
# для PostgreSQL ports: - "5432:5432" # для Redis ports: - "6379:6379"
Проверка работоспособности контейнеров
Неожиданный для меня пункт, так как не планировал включать инструкцию healthcheck в свой docker-compose. Но без нее у меня все падало. Да что уж там, я о существовании такой инструкции и не знал.
Если кратко, то запущенный контейнер не равно работающий контейнер. Работающим контейнером, применительно к контейнеру с БД и Redis, я называю такой контейнер, который готов принимать запросы на создание БД, таблиц в БД и т.д.
Первый запуск моего проекта в контейнерах завершался неудачей, так как запросы к БД отклонялись из-за ее неготовности принимать их.
Одна из первых ссылок в поисковой выдаче привела меня к решению использовать healthcheck. И это помогло.
Healthcheck
объявляет проверку, которая выполняется, чтобы определить, являются ли контейнеры работоспособными.
В параметре test указывается команда, которая и будет выполнять проверку.
# test для PostgreSQL (pg_isready – утилита для проверки работоспособности БД): test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] # test для Redis: test: [ "CMD", "redis-cli", "-a", "${PASSWORD_REDIS}", "--raw", "incr", "ping" ]
В h
ealthcheck указываются также такие параметры, как:
- interval – задается интервал для проведения тестов;
- timeout – максимальное время ожидания ответа;
- retries – максимальное количество попыток выполнения теста, при достижении которого тест считается проваленным.
Результаты тестов используются в описании службы приложения в разделе depends_on , так как без старта и подтверждения работоспособности контейнеров с БД и Redis контейнер с Telegram ботом просто упадет с ошибкой.
Контейнер для Telegram бота
Начну описание с depends_on, так как только после выполнения этого пункта стартует контейнер с Telegram ботом.
Depends_on – позволяет настроить зависимости между службами.
depends_on: db: condition: service_healthy redis: condition: service_healthy
Как только значение condition будет service_healthy, то будет запущен контейнер с Telegram ботом.
В остальном описание службы приложения Work for everyone аналогично с описанными ранее службами. Отличие заключается в том, что нет проверки работоспособности самого контейнера с ботом, нет дополнительных команд, как в контейнере с Redis, где задается пароль для подключения, не подключаются никакие volumes.
Кроме того, вместо команды image используется команда build, которая собирает образ на основе Dockerfile. Позже эту часть я поменяю, потому что образ с telegram ботом будет уже собран и загружен на Docker hub и оттуда будет подтягиваться для создания контейнера.
Полный код файла docker-compose.yml
version: '3.7' volumes: pg_bot_data: redis_data: services: db: container_name: db_container image: postgres:16.1 env_file: .env restart: always volumes: - pg_bot_data:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] interval: 5s timeout: 3s retries: 3 redis: container_name: redis_container image: redis:6.0 env_file: .env restart: always volumes: - redis_data:/var/redis_data ports: - "6379:6379" command: redis-server --requirepass ${PASSWORD_REDIS} healthcheck: test: [ "CMD", "redis-cli", "-a", "${PASSWORD_REDIS}", "--raw", "incr", "ping" ] interval: 5s timeout: 3s retries: 3 bot: container_name: bot_container build: ./bot/ env_file: .env restart: always depends_on: db: condition: service_healthy redis: condition: service_healthy