Перейти к содержанию

Docker и Docker Compose для бота

Ох уж этот Docker, а с ним и Docker Compose… Хотя давайте все по порядку.

По моему плану, деплой Telegram бота на сервер должен быть осуществлен с помощью  средств контейнеризации. На данный момент я более или менее знаком с Docker и Docker compose. Именно с помощью этих инструментов на учебе в Яндекс.Практикуме мы деплоили сайты, backend которых мы писали на Django, а frontend был написан на React.  А раз так, это хороший повод повторить пройденный материал на практике и возможность узнать что-то новое.

Напоминаю, что код бота доступен в репозитории на GitHub. Если есть вопросы, критические замечания или предложения, пишите мне в Telegram, в Вконтакте или на почту bks2408@mail.ru

И еще, все публикации, посвященные разработке Telegram bot Work for everyone, вы можете найти в специальной рубрике Telegram bot

Содержание:

Постановка задачи

Задача такая: запустить Telegram bot Work for everyone так, чтобы он работал в Docker контейнерах и его легко было развернуть на любой системе, вне зависимости от окружения. Если разбить эту задачу на подзадачи, то мне нужно:

  1. написать Dockerfile для сборки образа Telegram бота;
  2. в docker-compose описать три сервиса: Telegram bot, PostgreSQL и Redis;
  3. на основе подготовленного docker-compose запустить контейнеры;
  4. убедится, что бот работает так, как этого требует ТЗ.

Задачи поставлены, вперед!

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 устанавливает рабочий каталог для любых инструкций RUNCMD и 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" ]

В healthcheck указываются также такие параметры, как:

  • 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