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

Docker Compose: приложение в контейнере

В предыдущем уроке systemd управлял WSGI-процессом напрямую. Теперь подготовим альтернативу: приложение находится в Docker-образе, а его запуск описан через Compose. Это другой способ управления тем же компонентом, поэтому два варианта не запускают одновременно на 127.0.0.1:8000. Статический release-lab и его старые страницы по-прежнему остаются самостоятельным сайтом.

Используем Compose V2 и команду docker compose. Наличие Docker на учебном VPS является отдельной предпосылкой: установка и права доступа к daemon согласуются владельцем стенда. При написании образ не собирался и контейнер не запускался. Наша задача — понять, где теперь находится артефакт и какие настройки сохраняются при пересоздании процесса.

Образ, контейнер и описание запуска

Образ содержит файловый результат приложения и зависимости. Контейнер является запущенным экземпляром этого результата с выбранными настройками. Compose описывает, какие экземпляры нужны, какие порты и каталоги подключены, как передаются переменные. Поэтому изменение Compose-файла и изменение образа — разные части выпуска. Тег образа удобен, но может быть переназначен; для точной истории сохраняют digest принятого образа.

Подготовим каталог app с app.py и release.json из прошлого урока. В нём нет .venv с ноутбука: зависимости устанавливаются внутри совместимой среды образа. Программа читает паспорт из своего рабочего каталога. Это позволяет сравнить ответ процесса с ожидаемым прикладным выпуском. Публичное статическое дерево мы не включаем в контейнер без необходимости: Nginx продолжает обслуживать его по собственной модели.

FROM python:3.12-slim
ARG GUNICORN_VERSION
RUN test -n "$GUNICORN_VERSION" \
    && pip install --no-cache-dir "gunicorn==$GUNICORN_VERSION"
RUN groupadd --gid 10001 app \
    && useradd --uid 10001 --gid 10001 --create-home app
WORKDIR /app
COPY --chown=10001:10001 app.py release.json ./
USER 10001:10001
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--access-logfile", "-", "--error-logfile", "-", "app:application"]

Переменная версии обязательна. Перед будущей сборкой читатель выбирает проверенную совместимую версию Gunicorn и передаёт её как GUNICORN_VERSION; статья не придумывает номер актуального пакета. Базовый тег Python также меняется со временем. Для строгой повторяемости фиксируют проверенный digest и зависимости, затем продвигают один собранный образ между окружениями. Подготовка нового образа перед каждой публикацией под прежним тегом разрушила бы нашу модель неизменяемого результата.

Локальный порт остаётся локальным

Внутри контейнера приложение слушает доступный интерфейс контейнера. Снаружи публикуем его только на loopback хоста. Это сохраняет Nginx входной точкой, где находятся домен и TLS. Если убрать 127.0.0.1 из описания порта, прикладной процесс может стать доступным извне напрямую в зависимости от сетевых правил. Внутренний адрес контейнера и адрес публикации на хосте нельзя считать одной настройкой.

name: release-lab-app
services:
  app:
    image: release-lab-app:app-r1
    build:
      context: ./app
      args:
        GUNICORN_VERSION: ${GUNICORN_VERSION:?select a tested version}
    ports:
      - "127.0.0.1:8000:8000"
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    stop_grace_period: 30s
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2).read()"]
      interval: 30s
      timeout: 3s
      retries: 3

Это полный минимальный Compose-файл для прикладного компонента; его Dockerfile находится в указанном контексте. Переменная версии задаётся в среде будущей сборки или непубличном файле параметров. Она не является секретом. Перед выполнением читатель останавливает альтернативную службу systemd либо выбирает иной порт и соответствующий upstream. Занятый порт нельзя исправлять случайным завершением всех процессов. Официальное описание сервисов Compose.

Что означает проверка здоровья

Встроенный запрос проверяет /health из контейнера. Он не проходит публичный DNS, TLS и Nginx, поэтому дополняется внешним монитором. Также он проверяет успешное HTTP-обращение, а не строгое содержание JSON. Для контроля номера читают тело либо добавляют соответствующий разбор. Нельзя считать состояние healthy доказательством полной работы приложения, если оно пока отвечает только маленьким индикатором.

Политика restart связана с жизненным циклом контейнера; отметка unhealthy сама по себе не означает, что Compose автоматически пересоздаст его. Поэтому план реакции на неудачную проверку описывают отдельно. Непрерывно перезапускающийся контейнер может скрывать ошибку конфигурации и создавать дополнительную нагрузку. Сначала читают журнал и причину завершения, затем меняют состояние.

Подготовка и обновление

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

docker compose config
docker compose build app
docker compose up -d app
docker compose ps
docker compose logs --tail 100 app

Команды не выполнялись. У обновления с единственным экземпляром возможен короткий перерыв: контейнер приходится пересоздать. Для отсутствия перерыва нужен дополнительный механизм двух экземпляров и переключения прокси, а не один флаг -d. В журнал выпуска записывают образ, Compose-конфигурацию и фактический номер ответа. Возврат выбирает прежний образ с совместимыми настройками; он не возвращает автоматически внешние данные.

При передаче образа на другой сервер он должен быть доступен там через согласованный registry или доверенный архив образа. Локальный тег на машине сборки не означает, что узел публикации уже имеет его содержимое. В журнале фиксируют способ доставки и проверенный digest. Если staging и production самостоятельно собирают Dockerfile, они могут получить разные базовые пакеты, несмотря на одинаковый код. Поэтому после принятия кандидата предпочтительнее продвинуть уже собранный образ, а не заново выполнить сборку. Проверка отвечает на прежний вопрос: те ли байты выбраны, которые просматривали?

Разбор Compose-файла также может показать подставленные значения переменных. Для учебного номера версии это удобно, но при добавлении секретов вывод не должен автоматически попадать в открытый отчёт или артефакт задания. Отдельно согласуют, какие параметры можно сохранять в паспорте. Описание запуска остаётся текстом под контролем версий, а реальные секреты передаются по правилам окружения. Так переносимость конфигурации не требует распространять доступ к базе вместе с образом.

Ограничение записи и будущие данные

read_only запрещает запись в файловый слой контейнера, а /tmp оставляет временное место для процесса. Наше приложение не сохраняет посетительские данные, поэтому этого достаточно. При добавлении загрузок выделяют постоянное хранилище с нужными правами. Убирать ограничение целиком ради одного каталога не требуется. Логи идут в стандартные потоки; их хранение и ротацию на стороне Docker также нужно определить, чтобы они не заполнили диск.

Контейнер упрощает доставку совместимого окружения, но не отменяет обязанности владельца наблюдать ресурс, сеть и копии. Он не является резервной копией данных и не делает любое приложение независимым от хоста. В следующем уроке появится PostgreSQL: тогда пересоздаваемый контейнер и сохраняемое состояние придётся разделить ещё внимательнее.