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

Docker и воспроизводимый запуск

Шаг 1

До этого момента мы запускали наш проект непосредственно на своём компьютере. Создавали виртуальное окружение, устанавливали зависимости из requirements.txt, заполняли .env и выполняли команду FastAPI. Такой способ удобен во время разработки, пока приложение работает в уже подготовленном окружении.

Но представим, что проект нужно передать другому разработчику или запустить на сервере. Одних файлов с Python-кодом недостаточно. На новом компьютере придётся отдельно установить подходящую версию Python, создать виртуальное окружение, установить библиотеки и правильно запустить приложение.

На каждом из этих этапов могут появиться отличия:

  • на нашем компьютере используется Python 3.13, а на сервере установлен Python 3.11
  • часть библиотек уже была установлена вручную, но случайно не попала в requirements.txt
  • системные пакеты и настройки отличаются между macOS, Windows и Linux
  • новый разработчик запускает приложение другой командой или из неправильного каталога
  • локальный проект продолжает работать благодаря старому кэшу или ранее установленной зависимости, которой нет в чистом окружении

В результате возникает знакомая ситуация: «На моём компьютере всё работает, а на другом — нет». Причина может находиться не в самом Python-коде, а в различиях среды, где он выполняется.

В нашем проекте есть ещё одна практическая проблема. В предыдущем разделе Prometheus и Grafana уже запускались в контейнерах, а FastAPI продолжал работать отдельно — в локальном виртуальном окружении. Поэтому перед началом работы нам приходилось:

  1. активировать .venv
  2. отдельно запустить FastAPI
  3. отдельной командой запустить Prometheus и Grafana
  4. связать контейнер Prometheus с приложением на компьютере через host.docker.internal

Как Docker решает эту проблему?

Docker позволяет упаковать приложение в заранее описанное окружение. Мы явно укажем, какая версия Python нужна, какие зависимости установить, какие файлы скопировать и какой командой запустить FastAPI. После этого Docker сможет повторить эту последовательность на другом компьютере.

Для работы важно различать три понятия:

  • Dockerfile это текстовая инструкция, по которой собирается окружение приложения
  • Docker image, или образ, это уже собранный неизменяемый шаблон с Python, зависимостями и кодом
  • Docker container, или контейнер, это запущенный экземпляр образа

Dockerfile можно сравнить с рецептом, образ — с заранее приготовленной заготовкой, а контейнер — с запущенным приложением, созданным из этой заготовки. Из одного образа можно запускать несколько контейнеров. Они используют одинаковые файлы и зависимости, но являются разными процессами.

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

Для пользователя наш API почти не изменится. После запуска по-прежнему откроются /health, Swagger UI, чат и анализ файлов. Разница находится в способе запуска:

Docker решит несколько конкретных задач нашего проекта:

  • зафиксирует базовую версию Python
  • установит зависимости по одному и тому же requirements.txt
  • задаст единый рабочий каталог и команду запуска
  • отделит код приложения от локального виртуального окружения и настроек IDE
  • позволит позднее соединить FastAPI, Prometheus и Grafana во внутренней сети Docker Compose

При этом Docker не делает приложение автоматически готовым к любой production-нагрузке. Он не создаёт базу данных, не добавляет аутентификацию, не настраивает HTTPS, не выдаёт доменное имя и не скрывает архитектурные ошибки Python-кода. Docker решает более узкую задачу, он делает среду сборки и запуска приложения понятнее и воспроизводимее.

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

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

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

  • .venv содержит локальное виртуальное окружение для нашей операционной системы
  • .idea хранит настройки PyCharm
  • tests и evals нужны разработчику, но не серверному процессу
  • sample_files содержит учебные примеры
  • __pycache__ и .pytest_cache создаются автоматически
  • .env содержит секретный API-ключ

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

В корне проекта создайте файл .dockerignore:

.git
.idea
.venv
**/__pycache__
**/*.pyc
.pytest_cache
.env
tests
evals
sample_files
monitoring
document_ai_demo.py
main.py

Файл .dockerignore сообщает Docker, какие пути не нужно включать в контекст сборки. По сути он похож на .gitignore, но используется не Git, а Docker. Давайте разберем основные файлы, которые мы исключаем. Первый файл, это корневой main.py, который запускает консольный интерфейс, а будущий контейнер предназначен для FastAPI. Поэтому консольную точку входа не копируем. document_ai_demo.py также не требуется: его сценарии уже доступны через основной сервис и HTTP API.

Каталог monitoring не нужен внутри образа API. Docker Compose прочитает его конфигурацию на компьютере до начала сборки, а Prometheus и Grafana будут запущены как отдельные контейнеры.

Особенно обратите внимание на .env. Мы исключаем его из образа, но не отказываемся от настроек. При запуске Docker передаст значения как переменные окружения. Благодаря этому один и тот же образ можно использовать с разными ключами, моделями и ограничениями, не пересобирая его.

Создаём Dockerfile

Теперь в корне проекта создайте файл Dockerfile без расширения:

FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

RUN useradd --create-home --shell /usr/sbin/nologin appuser

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser app ./app

USER appuser

EXPOSE 8000

CMD ["fastapi", "run", "app/api/main.py", "--host", "0.0.0.0", "--port", "8000"]

Docker выполняет инструкции сверху вниз. Результат большинства шагов сохраняется как отдельный слой образа. Если при следующей сборке инструкция и используемые ею файлы не изменились, Docker может взять готовый слой из кэша. Поэтому важен не только набор команд, но и их порядок.

Первая строка задаёт основу:

FROM python:3.13-slim

FROM сообщает, на базе какого существующего образа мы строим свой. Образ python:3.13-slim уже содержит Python 3.13 и минимальное Linux-окружение. Нам не придётся устанавливать интерпретатор вручную.

Слово slim означает, что из базового образа убрана значительная часть дополнительных системных программ. Это уменьшает размер, но если библиотеке когда-нибудь понадобится отдельный системный пакет, его придётся явно установить в Dockerfile. Текущим зависимостям проекта достаточно готовых Python-пакетов из requirements.txt.

Далее задаются две переменные окружения самого Python:

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

PYTHONDONTWRITEBYTECODE=1 запрещает Python создавать файлы .pyc. В контейнере они нам не нужны, потому что код не редактируется во время работы.

PYTHONUNBUFFERED=1 отключает буферизацию стандартного вывода. Без этой настройки Python способен некоторое время накапливать строки перед выводом. Для серверного приложения важнее сразу видеть новые JSON-логи через docker compose logs.

Следующая инструкция задаёт рабочий каталог:

WORKDIR /app

Docker создаст каталог /app внутри образа и сделает его текущим. Последующие относительные пути будут отсчитываться от него. Это означает, что COPY requirements.txt . поместит файл в /app/requirements.txt, а команда запуска будет выполняться из /app.

Затем создаём отдельного пользователя:

RUN useradd --create-home --shell /usr/sbin/nologin appuser

RUN выполняет команду во время сборки образа. Здесь Linux-команда useradd создаёт пользователя appuser. Параметр --create-home создаёт домашний каталог, а --shell /usr/sbin/nologin показывает, что пользователь предназначен для запуска процесса, а не для обычного интерактивного входа.

По умолчанию многие контейнеры запускают процесс с правами root. Нашему FastAPI-приложению не нужны административные полномочия. Мы создаём ограниченного пользователя заранее, а переключимся на него после установки зависимостей и копирования файлов.

Следующие две строки устанавливают библиотеки:

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY переносит requirements.txt из контекста сборки в текущий каталог образа. После этого pip устанавливает перечисленные зависимости. Параметр --no-cache-dir запрещает сохранять загруженные установочные архивы, которые после установки только увеличивали бы образ.

Мы намеренно копируем requirements.txt раньше Python-кода. Зависимости меняются реже, чем файлы внутри app. Если мы исправим один метод, Docker сможет использовать закэшированный слой с уже установленными библиотеками. А если изменится requirements.txt, слой установки будет создан заново.

После зависимостей копируем само приложение:

COPY --chown=appuser:appuser app ./app

Первый app — каталог в нашем проекте, а ./app — путь внутри рабочего каталога контейнера. В результате пакет окажется в /app/app.

Параметр --chown=appuser:appuser сразу назначает владельцем скопированных файлов созданного пользователя. Приложению в основном достаточно права чтения, но явный владелец делает границу полномочий понятнее.

Теперь переключаем все последующие действия и будущий процесс на этого пользователя:

USER appuser

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

Далее документируем используемый порт:

EXPOSE 8000

EXPOSE показывает, что процесс внутри контейнера слушает порт 8000. Эта инструкция не открывает порт на нашем компьютере автоматически. При запуске мы отдельно свяжем внутренний порт контейнера с портом основной системы.

Последняя инструкция задаёт команду запуска:

CMD ["fastapi", "run", "app/api/main.py", "--host", "0.0.0.0", "--port", "8000"]

CMD выполняется не во время сборки, а при каждом запуске контейнера. Команда fastapi run запускает объект app из app/api/main.py в обычном серверном режиме.

Здесь не используется fastapi dev. Режим разработки следит за изменениями файлов и перезапускает процесс, но внутри готового образа мы не редактируем код. Если код изменился на компьютере, образ нужно пересобрать.

Параметр --host 0.0.0.0 особенно важен. Адрес 127.0.0.1 внутри контейнера был бы доступен только самому контейнеру. Значение 0.0.0.0 разрешает принимать соединения через контейнерную сеть. --port 8000 должен совпадать с портом, указанным в EXPOSE и будущей конфигурации Docker Compose.

В Dockerfile нет API-ключа, адреса ProxyAPI, модели и тарифов. Это намеренно. Dockerfile описывает общее окружение, а конкретные настройки будут передаваться контейнеру при запуске. Благодаря этому при изменении ключа или модели не нужно создавать новый образ.

Давайте перейдем к практике, перейдите в корень проекта и выполните:

docker build -t ai-support-desk .

Команда docker build читает Dockerfile и последовательно создаёт образ. Параметр -t ai-support-desk задаёт ему понятное имя. Точка в конце выбирает текущий каталог как контекст сборки, поэтому команду нужно выполнять именно из корня проекта.

При первом запуске Docker загрузит базовый образ и установит все зависимости. А повторная сборка обычно проходит быстрее благодаря слоям кэша.

После успешной сборки запустите контейнер:

docker run --rm --env-file .env -e APP_ENV=production -p 8000:8000 ai-support-desk

Разберём и эту команду:

  • --rm удалит остановленный контейнер, чтобы он не оставался в списке как ненужный экземпляр
  • --env-file .env передаст настройки из локального файла как переменные окружения, не копируя сам файл в образ
  • -e APP_ENV=production заменит только режим приложения
  • -p 8000:8000 соединит порт 8000 компьютера с портом 8000 контейнера
  • ai-support-desk указывает образ, из которого создаётся контейнер

В записи -p 8000:8000 левый порт относится к нашему компьютеру, а правый — к контейнеру. Поэтому запрос к http://127.0.0.1:8000 попадёт в FastAPI внутри Docker.

Если порт уже занят процессом FastAPI, запущенным из IDE или терминала, сначала остановите тот процесс. Два приложения не смогут одновременно использовать один порт основной системы.

Откройте:

http://127.0.0.1:8000/health

В ответ ожидаем:

{
  "status": "ok"
}

Затем перейдите по адресу http://127.0.0.1:8000/docs и выполните один AI-запрос. Если получен ответ, значит внутри контейнера работают FastAPI, наши сервисы, LLMClient и подключение к ProxyAPI.

В терминале должны появиться структурированные логи приложения:

Остановите контейнер сочетанием Control+C. Параметр --rm удалит этот запущенный экземпляр, но собранный образ ai-support-desk останется и сможет использоваться снова.

В этом шаге мы решили первую часть задачи и теперь AI-бэкенд больше не зависит от локального виртуального окружения во время запуска. Однако FastAPI, Prometheus и Grafana пока всё ещё запускаются разными командами. В следующем шаге добавим образ API в Docker Compose, объединим три контейнера общей сетью и поднимем всю систему одной командой.

Шаг 2

В предыдущем шаге мы создали Docker-образ AI-бэкенда и убедились, что FastAPI успешно запускается внутри отдельного контейнера. Для этого использовали команду docker run. Prometheus и Grafana при этом продолжают запускаться другой командой — через Docker Compose, который мы подготовили в прошлом разделе.

Получается, что сейчас одна система разделена на две части. Контейнер с FastAPI мы запускаем и останавливаем вручную, а контейнерами Prometheus и Grafana управляет Docker Compose. Разработчику приходится помнить несколько команд и отдельно следить за состоянием каждой части. Если нужно остановить или заново запустить весь проект, сделать это одним действием пока нельзя.

Есть и особенность сетевого взаимодействия. Раньше FastAPI работал непосредственно на нашем компьютере, а Prometheus находился внутри контейнера. Поэтому в prometheus.yml мы использовали адрес host.docker.internal:8000. Имя host.docker.internal позволяло контейнеру Prometheus выйти за пределы своей сети и обратиться к порту 8000 основной системы, где был запущен FastAPI.

Теперь AI-бэкенд тоже работает внутри контейнера, поэтому обращаться к основной системе больше не требуется. Мы можем поместить FastAPI, Prometheus и Grafana в одну сеть Docker Compose. Внутри неё сервисы будут находить друг друга по именам: Prometheus обратится к api:8000, а Grafana — к prometheus:9090.

В этой лекции мы добавим контейнер AI-бэкенда в уже существующий docker-compose.yml, обновим адрес в конфигурации Prometheus и настроим порядок запуска. В результате FastAPI, Prometheus и Grafana будут подниматься и останавливаться одной командой, а всё их взаимодействие будет происходить внутри общей сети Docker.

Соберём единую среду из трёх сервисов:

При запуске Docker Compose автоматически создаст общую внутреннюю сеть и подключит к ней все три контейнера. В этой сети каждый сервис можно найти по его имени из docker-compose.yml. Поэтому Prometheus сможет обращаться к AI-бэкенду по адресу api:8000, а Grafana — к Prometheus по адресу prometheus:9090.

Эти адреса используются только для взаимодействия контейнеров друг с другом. На нашем компьютере сервисы по-прежнему будут доступны через localhost: Swagger UI — на порту 8000, интерфейс Prometheus — на 9090, а Grafana — на 3000.

И перед тем, как переходить к практике, давайте обсудим один момент. В прошлом разделе файл docker-compose.yml находился внутри каталога monitoring. Тогда он описывал только Prometheus и Grafana, поэтому такое расположение было логичным. Но теперь Docker Compose будет управлять всем приложением, включая AI-бэкенд. Конфигурация больше не относится только к мониторингу, поэтому перенесём её в корень проекта.

Если у вас контейнеры Prometheus и Grafana из прошлого раздела продолжают работать, сначала остановите их командой:

docker compose -f monitoring/docker-compose.yml down

После этого переместите monitoring/docker-compose.yml в корень проекта. Файл prometheus.yml оставьте внутри monitoring, поскольку он содержит настройки только одного сервиса.

Теперь откройте monitoring/prometheus.yml. Замените адрес источника метрик host.docker.internal:8000 на api:8000. Полностью файл будет выглядеть так:

global:
  scrape_interval: 5s

scrape_configs:
  - job_name: ai-support-desk
    metrics_path: /metrics
    static_configs:
      - targets:
          - api:8000

Обратите внимание, api это не доменное имя в интернете. Так будет называться сервис FastAPI внутри docker-compose.yml. Docker добавит это имя во внутреннюю систему адресов своей сети, и Prometheus сможет найти нужный контейнер.

Теперь откройте перенесённый в корень docker-compose.yml и полностью замените его содержимое:

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ai-support-api
    env_file:
      - .env
    environment:
      APP_ENV: production
    ports:
      - "8000:8000"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s

  prometheus:
    image: prom/prometheus:latest
    container_name: ai-support-prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    depends_on:
      api:
        condition: service_healthy

  grafana:
    image: grafana/grafana:latest
    container_name: ai-support-grafana
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - prometheus

volumes:
  prometheus_data:
    external: true
    name: monitoring_prometheus_data
  grafana_data:
    external: true
    name: monitoring_grafana_data

Рассмотрим, что изменилось. Первым появился сервис api. В отличие от Prometheus и Grafana, для него не указан готовый образ из интернета. Вместо этого используется блок build:

build:
  context: .
  dockerfile: Dockerfile

context: . указывает корень проекта как контекст сборки, а dockerfile сообщает имя инструкции сборки, которую мы создали в предыдущей лекции.

Настройки приложения передаются отдельно:

env_file:
  - .env
environment:
  APP_ENV: production

env_file читает переменные из корневого .env и передаёт их процессу FastAPI. Сам файл не копируется в Docker-образ. Блок environment заменяет только APP_ENV и явно указывает, что контейнер запускается не в режиме разработки.

Для локального учебного проекта этого достаточно. В реальной инфраструктуре секреты обычно передаются из защищённого хранилища выбранной платформы. Значение API-ключа по-прежнему нельзя добавлять в Dockerfile или docker-compose.yml.

Связь портов задаётся уже знакомой записью:

ports:
  - "8000:8000"

Левый порт относится к нашему компьютеру, а правый к контейнеру. Поэтому Swagger UI будет доступен по адресу http://localhost:8000/docs, хотя сам процесс FastAPI находится внутри Docker.

Блок healthcheck проверяет, действительно ли приложение готово отвечать:

healthcheck:
  test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
  interval: 10s
  timeout: 3s
  retries: 5
  start_period: 10s

Запущенный контейнер ещё не обязательно означает готовое приложение. FastAPI требуется небольшое время, чтобы импортировать модули, создать зависимости и выполнить lifespan. Проверка запускается внутри контейнера и обращается к /health стандартным модулем Python urllib. Устанавливать отдельный curl в образ не нужно.

start_period даёт приложению время на начальный запуск. Затем проверка выполняется каждые десять секунд, ждёт ответ до трёх секунд и допускает пять последовательных неудач. После успешного ответа контейнер получает состояние healthy.

Эта проверка подтверждает, что FastAPI запущен, но намеренно не обращается к ProxyAPI. Healthcheck должен быть быстрым и не должен расходовать токены при каждом повторении.

Prometheus теперь зависит от готовности API:

depends_on:
  api:
    condition: service_healthy

Docker Compose сначала запускает контейнер api и ждёт успешный healthcheck. Только после этого запускается Prometheus. Так первые обращения к /metrics не отправляются в ещё не подготовленное приложение.

Путь к конфигурации Prometheus тоже изменился:

- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro

Теперь путь отсчитывается от корня, где находится Compose-файл. Левая часть указывает локальный файл, правая — место внутри контейнера Prometheus. Окончание :ro подключает конфигурацию только для чтения.

В конце описаны именованные тома:

volumes:
  prometheus_data:
    external: true
    name: monitoring_prometheus_data
  grafana_data:
    external: true
    name: monitoring_grafana_data

В прошлом разделе Compose-файл находился в каталоге monitoring, поэтому Docker автоматически использовал этот каталог как часть имён томов. После переноса файла в корень автоматические имена могли бы измениться, и Grafana открылась бы без ранее созданного дашборда.

Значение name указывает точное прежнее имя тома, а external: true сообщает новой конфигурации, что этот том уже был создан другим Compose-проектом. Docker не станет пытаться создать его заново или считать своей собственностью, а просто подключит существующие данные.

На этом конфигурация общей среды готова. Мы перенесли Compose-файл в корень, добавили в него AI-бэкенд, связали сервисы через внутреннюю сеть, настроили healthcheck и сохранили существующие тома Prometheus и Grafana.

Пока мы намеренно не запускаем новую конфигурацию. В следующем шаге мы поднимем все три контейнера одной командой, проверим их состояние и выполним запросы через Swagger UI. Затем убедимся, что события появились в логах, Prometheus получил новые метрики, а Grafana отобразила их на дашборде.

Шаг 3

В предыдущих двух лекциях мы подготовили всё необходимое для воспроизводимого запуска. Сначала создали Dockerfile и собрали отдельный образ AI-бэкенда. Затем перенесли docker-compose.yml в корень проекта и описали в нём взаимодействие FastAPI, Prometheus и Grafana.

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

Перед запуском остановите контейнер из первой лекции, если он всё ещё работает, и убедитесь, что локальный FastAPI больше не занимает порт 8000. Затем перейдите в корень проекта и выполните:

docker compose up --build -d

Параметр --build просит Docker Compose собрать актуальный образ AI-бэкенда по Dockerfile. Параметр -d запускает контейнеры в фоне, поэтому после старта терминал снова будет готов принимать команды. В результате Docker Compose должен сообщить об успешной подготовке всех компонентов:

Строка Built означает, что образ AI-бэкенда успешно собран по Dockerfile. Статус Started показывает, что контейнеры Prometheus и Grafana запущены. Статус Healthy у ai-support-api означает, что контейнер не просто начал работать, но и успешно ответил на запрос к /health.

Если у вас Docker сообщает, что имена ai-support-prometheus или ai-support-grafana уже используются, значит контейнеры не были остановлены перед переносом Compose-файла. Сначала удалите частично созданные контейнеры новой конфигурации:

docker compose down

Затем удалите только старые контейнеры мониторинга:

docker rm -f ai-support-prometheus ai-support-grafana

Эта команда не удаляет именованные тома, поэтому история Prometheus и дашборд Grafana сохранятся. После этого повторите запуск.

Посмотрите состояние сервисов:

docker compose ps

Сервис api должен стать healthy, а Prometheus и Grafana — находиться в запущенном состоянии.

Если healthcheck ещё выполняется, подождите несколько секунд и повторите команду.

Теперь давайте протестируем наш проект. Откройте Swagger UI:

http://localhost:8000/docs

Создайте чат-сессию через POST /chat/sessions и скопируйте полученный session_id. После этого откройте POST /chat/sessions/{session_id}/messages, вставьте идентификатор в параметр пути и отправьте:

{
  "message": "Где сейчас заказ 8041?"
}

Модель должна выбрать инструмент get_order_status, приложение выполнит зарегистрированную Python-функцию и передаст её результат модели для подготовки финального ответа.

Напомню, что один пользовательский запрос может создать два LLM-вызова: выбор инструмента и формирование итоговой реплики.

Теперь посмотрим, что происходило внутри контейнера. Выполните:

docker compose logs --tail=30 api

В выводе должны появиться JSON-события HTTP-запроса и обращений к модели.

Среди логов будут встречаться не только созданные нами JSON-события, но и обычные служебные строки Uvicorn. Это нормально: JSON-формат мы настроили для модулей нашего приложения, а встроенное логирование веб-сервера не изменяли.

Кроме событий llm_call_completed и http_request_completed, вы увидите регулярные запросы GET /metrics и GET /health. Первый выполняет Prometheus, который периодически забирает метрики приложения, а второй отправляет Docker для проверки работоспособности контейнера.

Через несколько секунд Prometheus заберёт обновлённые значения. Перейдите по адресу http://localhost:9090/query и выполните выражение:

ai_support_llm_calls_total

В результате мы получим:

А значит Prometheus видит наш FastAPI проект. После этого откройте дашборд AI Support Desk в Grafana по адресу http://localhost:3000. В результате мы увидим:

А значит мы проверили всю цепочку: сетевой доступ к контейнеру, настройки ProxyAPI, вызов функций, структурированные логи, экспорт метрик, сбор Prometheus и отображение Grafana.

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

Чтобы пересобрать образ и пересоздать изменившийся сервис, из корня проекта снова выполните:

docker compose up --build -d

Docker сравнит инструкции и использованные файлы с предыдущей сборкой. Неизменившиеся слои будут взяты из кэша. Например если изменился только Python-код, зависимости из requirements.txt обычно не устанавливаются повторно.

Если изменился сам requirements.txt, Docker заново выполнит слой с pip install. Так новая библиотека попадёт в чистое окружение образа.

Для завершения всей системы выполните из корня:

docker compose down

Docker Compose остановит и удалит три контейнера и созданную для них сеть. Собранный образ AI-бэкенда останется, поэтому его можно будет использовать при следующем запуске.

Именованные тома monitoring_prometheus_data и monitoring_grafana_data тоже сохранятся. Поэтому следующий docker compose up -d снова подключит историю Prometheus, источник данных Grafana и созданный дашборд.

Не добавляйте параметр -v, если хотите сохранить эти данные:

docker compose down -v

Эта команда дополнительно удаляет тома. Она пригодится только тогда, когда нужно намеренно сбросить Prometheus и Grafana и начать с полностью чистого состояния.

И подводя итоги этого раздела, мы смело можем сказать, что теперь версия Python, зависимости, файлы приложения и команда запуска описаны явно. Другой разработчик сможет получить проект, подготовить собственный .env и запустить всю систему одинаковой командой. Именно это мы называем воспроизводимым запуском.

Однако Docker не решает автоматически все задачи публикации. Он не выдаёт доменное имя, не настраивает HTTPS, не создаёт резервные копии, не обновляет секреты и не определяет необходимое количество экземпляров API. Эти возможности зависят от сервера или выбранной облачной платформы.

В заключительном разделе рассмотрим итоговую структуру проекта, подготовим конфигурацию для другого разработчика и подведём итоги всего курса.

Шаг 4

Почему requirements.txt копируют и устанавливают до копирования каталога app?

Правильный ответ
  • ✅ Изменения кода не сбрасывают Docker-кеш слоя с зависимостями

Шаг 5

Какие пути следует исключить из контекста сборки Docker?

Выберите все правильные варианты.

Правильный ответ
  • ✅ .env
  • ✅ Каталоги pycache
  • ✅ .pytest_cache
  • ✅ .venv

Шаг 6

Зачем контейнер запускает приложение от пользователя appuser, а не от root?

Правильный ответ
  • ✅ Это уменьшает возможный ущерб при уязвимости приложения

Шаг 7

Что означает зависимость Prometheus от API с условием condition: service_healthy?

Правильный ответ
  • ✅ Prometheus запускается после успешной проверки состояния API

Шаг 8

Содержимое файла .dockerignore

Docker отправляет сборщику содержимое каталога проекта. В контекст не должны попадать секреты, виртуальное окружение, кеши Python и локальные результаты проверок. В этом задании поле решения представляет содержимое настоящего файла .dockerignore.

Задача

Введите в поле решения правила для файла .dockerignore. Каждое правило запишите на отдельной строке.

Требования

  • Добавьте правила .git, .idea, .venv, **/__pycache__, **/*.pyc, .pytest_cache и .env.
  • Добавьте каталоги tests, evals, sample_files и monitoring.
  • Добавьте файлы document_ai_demo.py и main.py.
  • Не исключайте app, requirements.txt или Dockerfile.
  • Порядок правил не влияет на проверку.
  • Не создавайте переменную и не используйте синтаксис Python: нужны только строки файла .dockerignore.

Пример результата

.git
.venv
**/__pycache__
**/*.pyc
.env
tests
# остальные правила
Правильный ответ
.git
.idea
.venv
**/__pycache__
**/*.pyc
.pytest_cache
.env
tests
evals
sample_files
monitoring
document_ai_demo.py
main.py

Шаг 9

Dockerfile для AI-бэкенда

Dockerfile должен воспроизводимо установить зависимости, скопировать только код приложения и запустить FastAPI не от пользователя root. На учебной площадке содержимое Dockerfile хранится в многострочной строке.

Задача

Введите в поле решения содержимое файла Dockerfile.

Каждую инструкцию Dockerfile записывайте как обычную строку Dockerfile. Не создавайте переменные Python и не заключайте содержимое в строку.

Требования

  • Начните с FROM python:3.13-slim.
  • Установите PYTHONDONTWRITEBYTECODE=1 и PYTHONUNBUFFERED=1.
  • Задайте WORKDIR /app.
  • Создайте пользователя командой RUN useradd --create-home --shell /usr/sbin/nologin appuser.
  • Скопируйте requirements.txt до каталога app и выполните pip install --no-cache-dir -r requirements.txt.
  • Скопируйте приложение командой COPY --chown=appuser:appuser app ./app.
  • Переключитесь на USER appuser и объявите EXPOSE 8000.
  • Добавьте команду запуска CMD ["fastapi", "run", "app/api/main.py", "--host", "0.0.0.0", "--port", "8000"].

Пример формата:

FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
Правильный ответ
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN useradd --create-home --shell /usr/sbin/nologin appuser
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appuser app ./app
USER appuser
EXPOSE 8000
CMD ["fastapi", "run", "app/api/main.py", "--host", "0.0.0.0", "--port", "8000"]