Skip to content

Latest commit

 

History

History
700 lines (406 loc) · 57.5 KB

docker.md

File metadata and controls

700 lines (406 loc) · 57.5 KB

FastAPI и Docker-контейнеры

При развёртывании приложений FastAPI, часто начинают с создания образа контейнера на основе Linux. Обычно для этого используют Docker. Затем можно развернуть такой контейнер на сервере одним из нескольких способов.

Использование контейнеров на основе Linux имеет ряд преимуществ, включая безопасность, воспроизводимость, простоту и прочие.

!!! tip "Подсказка" Торопитесь или уже знакомы с этой технологией? Перепрыгните на раздел Создать Docker-образ для FastAPI 👇

Развернуть Dockerfile 👀
FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]

# Если используете прокси-сервер, такой как Nginx или Traefik, добавьте --proxy-headers
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]

Что такое "контейнер"

Контейнеризация - это легковесный способ упаковать приложение, включая все его зависимости и необходимые файлы, чтобы изолировать его от других контейнеров (других приложений и компонентов) работающих на этой же системе.

Контейнеры, основанные на Linux, запускаются используя ядро Linux хоста (машины, виртуальной машины, облачного сервера и т.п.). Это значит, что они очень легковесные (по сравнению с полноценными виртуальными машинами, полностью эмулирующими работу операционной системы).

Благодаря этому, контейнеры потребляют малое количество ресурсов, сравнимое с процессом запущенным напрямую (виртуальная машина потребует гораздо больше ресурсов).

Контейнеры также имеют собственные запущенные изолированные процессы (но часто только один процесс), файловую систему и сеть, что упрощает развёртывание, разработку, управление доступом и т.п.

Что такое "образ контейнера"

Для запуска контейнера нужен образ контейнера.

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

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

Когда контейнер запущен (на основании образа), он может создавать и изменять файлы, переменные окружения и т.д. Эти изменения будут существовать только внутри контейнера, но не будут сохраняться в образе контейнера (не будут сохранены на диск).

Образ контейнера можно сравнить с файлом, содержащем программу, например, как файл main.py.

И контейнер (в отличие от образа) - это на самом деле выполняемый экземпляр образа, примерно как процесс. По факту, контейнер запущен только когда запущены его процессы (чаще, всего один процесс) и остановлен, когда запущенных процессов нет.

Образы контейнеров

Docker является одним оз основных инструментов для создания образов и контейнеров и управления ими.

Существует общедоступный Docker Hub с подготовленными официальными образами многих инструментов, окружений, баз данных и приложений.

К примеру, есть официальный образ Python.

Также там представлены и другие полезные образы, такие как базы данных:

и т.п.

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

Таким образом, вы можете изучить, что такое контейнеризация и Docker, и использовать полученные знания с разными инструментами и компонентами.

Так, вы можете запустить одновременно множество контейнеров с базой данных, Python-приложением, веб-сервером, React-приложением и соединить их вместе через внутреннюю сеть.

Все системы управления контейнерами (такие, как Docker или Kubernetes) имеют встроенные возможности для организации такого сетевого взаимодействия.

Контейнеры и процессы

Обычно образ контейнера содержит метаданные предустановленной программы или команду, которую следует выполнить при запуске контейнера. Также он может содержать параметры, передаваемые предустановленной программе. Похоже на то, как если бы вы запускали такую программу через терминал.

Когда контейнер запущен, он будет выполнять прописанные в нём команды и программы. Но вы можете изменить его так, чтоб он выполнял другие команды и программы.

Контейнер буде работать до тех пор, пока выполняется его главный процесс (команда или программа).

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

Контейнер не считается запущенным, если в нём не выполняется хотя бы один процесс. Если главный процесс остановлен, значит и контейнер остановлен.

Создать Docker-образ для FastAPI

Что ж, давайте ужё создадим что-нибудь! 🚀

Я покажу Вам, как собирать Docker-образ для FastAPI с нуля, основываясь на официальном образе Python.

Такой подход сгодится для большинства случаев, например:

  • Использование с Kubernetes или аналогичным инструментом
  • Запуск в Raspberry Pi
  • Использование в облачных сервисах, запускающих образы контейнеров для вас и т.п.

Установить зависимости

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

На название и содержание такого файла влияет выбранный Вами инструмент установки этих библиотек (зависимостей).

Чаще всего это простой файл requirements.txt с построчным перечислением библиотек и их версий.

При этом Вы, для выбора версий, будете использовать те же идеи, что упомянуты на странице О версиях FastAPI{.internal-link target=_blank}.

Ваш файл requirements.txt может выглядеть как-то так:

fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0

Устанавливать зависимости проще всего с помощью pip:

$ pip install -r requirements.txt
---> 100%
Successfully installed fastapi pydantic uvicorn

!!! info "Информация" Существуют и другие инструменты управления зависимостями.

В этом же разделе, но позже, я покажу вам пример использования Poetry. 👇

Создать приложение FastAPI

  • Создайте директорию app и перейдите в неё.
  • Создайте пустой файл __init__.py.
  • Создайте файл main.py и заполните его:
from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

Dockerfile

В этой же директории создайте файл Dockerfile и заполните его:

# (1)
FROM python:3.9

# (2)
WORKDIR /code

# (3)
COPY ./requirements.txt /code/requirements.txt

# (4)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (5)
COPY ./app /code/app

# (6)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
  1. Начните с официального образа Python, который будет основой для образа приложения.

  2. Укажите, что в дальнейшем команды запускаемые в контейнере, будут выполняться в директории /code.

    Инструкция создаст эту директорию внутри контейнера и мы поместим в неё файл requirements.txt и директорию app.

  3. Скопируете файл с зависимостями из текущей директории в /code.

    Сначала копируйте только файл с зависимостями.

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

  4. Установите библиотеки перечисленные в файле с зависимостями.

    Опция --no-cache-dir указывает pip не сохранять загружаемые библиотеки на локальной машине для использования их в случае повторной загрузки. В контейнере, в случае пересборки этого шага, они всё равно будут удалены.

    !!! note "Заметка" Опция --no-cache-dir нужна только для pip, она никак не влияет на Docker или контейнеры.

    Опция --upgrade указывает pip обновить библиотеки, емли они уже установлены.

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

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

  5. Скопируйте директорию ./app внутрь директории /code (в контейнере).

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

  6. Укажите команду, запускающую сервер uvicorn.

    CMD принимает список строк, разделённых запятыми, но при выполнении объединит их через пробел, собрав из них одну команду, которую вы могли бы написать в терминале.

    Эта команда будет выполнена в текущей рабочей директории, а именно в директории /code, которая указана в команде WORKDIR /code.

    Так как команда выполняется внутри директории /code, в которую мы поместили папку ./app с приложением, то Uvicorn сможет найти и импортировать объект app из файла app.main.

!!! tip "Подсказка" Если ткнёте на кружок с плюсом, то увидите пояснения. 👆

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

.
├── app
│   ├── __init__.py
│   └── main.py
├── Dockerfile
└── requirements.txt

Использование прокси-сервера

Если вы запускаете контейнер за прокси-сервером завершения TLS (балансирующего нагрузку), таким как Nginx или Traefik, добавьте опцию --proxy-headers, которая укажет Uvicorn, что он работает позади прокси-сервера и может доверять заголовкам отправляемым им.

CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]

Кэш Docker'а

В нашем Dockerfile использована полезная хитрость, когда сначала копируется только файл с зависимостями, а не вся папка с кодом приложения.

COPY ./requirements.txt /code/requirements.txt

Docker и подобные ему инструменты создают образы контейнеров пошагово, добавляя один слой над другим, начиная с первой строки Dockerfile и добавляя файлы, создаваемые при выполнении каждой инструкции из Dockerfile.

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

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

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

Файл со списком зависимостей изменяется довольно редко. Так что выполнив команду копирования только этого файла, Docker сможет использовать кэш на этом шаге.

А затем использовать кэш и на следующем шаге, загружающем и устанавливающем зависимости. И вот тут-то мы и сэкономим много времени. ✨ ...а не будем томиться в тягостном ожидании. 😪😆

Для загрузки и установки необходимых библиотек может понадобиться несколько минут, но использование кэша занимает несколько секунд максимум.

И так как во время разработки вы будете часто пересобирать контейнер для проверки работоспособности внесённых изменений, то сэкономленные минуты сложатся в часы, а то и дни.

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

COPY ./app /code/app

Создать Docker-образ

Теперь, когда все файлы на своих местах, давайте создадим образ контейнера.

  • Перейдите в директорию проекта (в ту, где расположены Dockerfile и папка app с приложением).
  • Создай образ приложения FastAPI:
$ docker build -t myimage .

---> 100%

!!! tip "Подсказка" Обратите внимание, что в конце написана точка - ., это то же самое что и ./, тем самым мы указываем Docker директорию, из которой нужно выполнять сборку образа контейнера.

В данном случае это та же самая директория (`.`).

Запуск Docker-контейнера

  • Запустите контейнер, основанный на вашем образе:
$ docker run -d --name mycontainer -p 80:80 myimage

Проверка

Вы можете проверить, что Ваш Docker-контейнер работает перейдя по ссылке: http://192.168.99.100/items/5?q=somequery или http://127.0.0.1/items/5?q=somequery (или похожей, которую использует Ваш Docker-хост).

Там вы увидите:

{"item_id": 5, "q": "somequery"}

Интерактивная документация API

Теперь перейдите по ссылке http://192.168.99.100/docs или http://127.0.0.1/docs (или похожей, которую использует Ваш Docker-хост).

Здесь вы увидите автоматическую интерактивную документацию API (предоставляемую Swagger UI):

Swagger UI

Альтернативная документация API

Также вы можете перейти по ссылке http://192.168.99.100/redoc or http://127.0.0.1/redoc (или похожей, которую использует Ваш Docker-хост).

Здесь вы увидите альтернативную автоматическую документацию API (предоставляемую ReDoc):

ReDoc

Создание Docker-образа на основе однофайлового приложения FastAPI

Если ваше приложение FastAPI помещено в один файл, например, main.py и структура Ваших файлов похожа на эту:

.
├── Dockerfile
├── main.py
└── requirements.txt

Вам нужно изменить в Dockerfile соответствующие пути копирования файлов:

FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (1)
COPY ./main.py /code/

# (2)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
  1. Скопируйте непосредственно файл main.py в директорию /code (не указывайте ./app).

  2. При запуске Uvicorn укажите ему, что объект app нужно импортировать из файла main (вместо импортирования из app.main).

Настройте Uvicorn на использование main вместо app.main для импорта объекта app.

Концепции развёртывания

Давайте вспомним о Концепциях развёртывания{.internal-link target=_blank} и применим их к контейнерам.

Контейнеры - это, в основном, инструмент упрощающий сборку и развёртывание приложения и они не обязывают к применению какой-то определённой концепции развёртывания, а значит мы можем выбирать нужную стратегию.

Хорошая новость в том, что независимо от выбранной стратегии, мы всё равно можем покрыть все концепции развёртывания. 🎉

Рассмотрим эти концепции развёртывания применительно к контейнерам:

  • Использование более безопасного протокола HTTPS
  • Настройки запуска приложения
  • Перезагрузка приложения
  • Запуск нескольких экземпляров приложения
  • Управление памятью
  • Использование перечисленных функций перед запуском приложения

Использование более безопасного протокола HTTPS

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

Это может быть другой контейнер, в котором есть, например, Traefik, работающий с HTTPS и самостоятельно обновляющий сертификаты.

!!! tip "Подсказка" Traefik совместим с Docker, Kubernetes и им подобными инструментами. Он очень прост в установке и настройке использования HTTPS для Ваших контейнеров.

В качестве альтернативы, работу с HTTPS можно доверить облачному провайдеру, если он предоставляет такую услугу.

Настройки запуска и перезагрузки приложения

Обычно запуском контейнера с приложением занимается какой-то отдельный инструмент.

Это может быть сам Docker, Docker Compose, Kubernetes, облачный провайдер и т.п.

В большинстве случаев это простейшие настройки запуска и перезагрузки приложения (при падении). Например, команде запуска Docker-контейнера можно добавить опцию --restart.

Управление запуском и перезагрузкой приложений без использования контейнеров - весьма затруднительно. Но при работе с контейнерами - это всего лишь функционал доступный по умолчанию. ✨

Запуск нескольких экземпляров приложения - Указание количества процессов

Если у вас есть кластер машин под управлением Kubernetes, Docker Swarm Mode, Nomad или аналогичной сложной системой оркестрации контейнеров, скорее всего, вместо использования менеджера процессов (типа Gunicorn и его воркеры) в каждом контейнере, вы захотите управлять количеством запущенных экземпляров приложения на уровне кластера.

В любую из этих систем управления контейнерами обычно встроен способ управления количеством запущенных контейнеров для распределения нагрузки от входящих запросов на уровне кластера.

В такой ситуации Вы, вероятно, захотите создать образ Docker, как описано выше, с установленными зависимостями и запускающий один процесс Uvicorn вместо того, чтобы запускать Gunicorn управляющий несколькими воркерами Uvicorn.

Балансировщик нагрузки

Обычно при использовании контейнеров один компонент прослушивает главный порт. Это может быть контейнер содержащий прокси-сервер завершения работы TLS для работы с HTTPS или что-то подобное.

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

!!! tip "Подсказка" Прокси-сервер завершения работы TLS одновременно может быть балансировщиком нагрузки.

Система оркестрации, которую вы используете для запуска и управления контейнерами, имеет встроенный инструмент сетевого взаимодействия (например, для передачи HTTP-запросов) между контейнерами с Вашими приложениями и балансировщиком нагрузки (который также может быть прокси-сервером).

Один балансировщик - Множество контейнеров

При работе с Kubernetes или аналогичными системами оркестрации использование их внутренней сети позволяет иметь один балансировщик нагрузки, который прослушивает главный порт и передаёт запросы множеству запущенных контейнеров с Вашими приложениями.

В каждом из контейнеров обычно работает только один процесс (например, процесс Uvicorn управляющий Вашим приложением FastAPI). Контейнеры могут быть идентичными, запущенными на основе одного и того же образа, но у каждого будут свои отдельные процесс, память и т.п. Таким образом мы получаем преимущества распараллеливания работы по разным ядрам процессора или даже разным машинам.

Система управления контейнерами с балансировщиком нагрузки будет распределять запросы к контейнерам с приложениями по очереди. То есть каждый запрос будет обработан одним из множества одинаковых контейнеров с одним и тем же приложением.

Балансировщик нагрузки может обрабатывать запросы к разным приложениям, расположенным в вашем кластере (например, если у них разные домены или префиксы пути) и передавать запросы нужному контейнеру с требуемым приложением.

Один процесс на контейнер

В этом варианте в одном контейнере будет запущен только один процесс (Uvicorn), а управление изменением количества запущенных копий приложения происходит на уровне кластера.

Здесь не нужен менеджер процессов типа Gunicorn, управляющий процессами Uvicorn, или же Uvicorn, управляющий другими процессами Uvicorn. Достаточно только одного процесса Uvicorn на контейнер (но запуск нескольких процессов не запрещён).

Использование менеджера процессов (Gunicorn или Uvicorn) внутри контейнера только добавляет излишнее усложнение, так как управление следует осуществлять системой оркестрации.

Множество процессов внутри контейнера для особых случаев

Безусловно, бывают особые случаи, когда может понадобиться внутри контейнера запускать менеджер процессов Gunicorn, управляющий несколькими процессами Uvicorn.

Для таких случаев вы можете использовать официальный Docker-образ (прим. пер: - здесь и далее на этой странице, если вы встретите сочетание "официальный Docker-образ" без уточнений, то автор имеет в виду именно предоставляемый им образ), где в качестве менеджера процессов используется Gunicorn, запускающий несколько процессов Uvicorn и некоторые настройки по умолчанию, автоматически устанавливающие количество запущенных процессов в зависимости от количества ядер вашего процессора. Я расскажу вам об этом подробнее тут: Официальный Docker-образ со встроенными Gunicorn и Uvicorn.

Некоторые примеры подобных случаев:

Простое приложение

Вы можете использовать менеджер процессов внутри контейнера, если ваше приложение настолько простое, что у вас нет необходимости (по крайней мере, пока нет) в тщательных настройках количества процессов и вам достаточно имеющихся настроек по умолчанию (если используется официальный Docker-образ) для запуска приложения только на одном сервере, а не в кластере.

Docker Compose

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

В этом случае можно использовать менеджер процессов, управляющий несколькими процессами, внутри одного контейнера.

Prometheus и прочие причины

У вас могут быть и другие причины, когда использование множества процессов внутри одного контейнера будет проще, нежели запуск нескольких контейнеров с единственным процессом в каждом из них.

Например (в зависимости от конфигурации), у вас могут быть инструменты подобные экспортёру Prometheus, которые должны иметь доступ к каждому запросу приходящему в контейнер.

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

В таком случае может быть проще иметь один контейнер со множеством процессов, с нужным инструментом (таким как экспортёр Prometheus) в этом же контейнере и собирающем метрики со всех внутренних процессов этого контейнера.


Самое главное - ни одно из перечисленных правил не является высеченным на камне и вы не обязаны слепо их повторять. вы можете использовать эти идеи при рассмотрении вашего конкретного случая и самостоятельно решать, какая из концепции подходит лучше:

  • Использование более безопасного протокола HTTPS
  • Настройки запуска приложения
  • Перезагрузка приложения
  • Запуск нескольких экземпляров приложения
  • Управление памятью
  • Использование перечисленных функций перед запуском приложения

Управление памятью

При запуске одного процесса на контейнер вы получаете относительно понятный, стабильный и ограниченный объём памяти, потребляемый одним контейнером.

Вы можете установить аналогичные ограничения по памяти при конфигурировании своей системы управления контейнерами (например, Kubernetes). Таким образом система сможет изменять количество контейнеров на доступных ей машинах приводя в соответствие количество памяти нужной контейнерам с количеством памяти доступной в кластере (наборе доступных машин).

Если у вас простенькое приложение, вероятно у вас не будет необходимости устанавливать жёсткие ограничения на выделяемую ему память. Но если приложение использует много памяти (например, оно использует модели машинного обучения), вам следует проверить, как много памяти ему требуется и отрегулировать количество контейнеров запущенных на каждой машине (может быть даже добавить машин в кластер).

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

Подготовительные шаги при запуске контейнеров

Есть два основных подхода, которые вы можете использовать при запуске контейнеров (Docker, Kubernetes и т.п.).

Множество контейнеров

Когда вы запускаете множество контейнеров, в каждом из которых работает только один процесс (например, в кластере Kubernetes), может возникнуть необходимость иметь отдельный контейнер, который осуществит предварительные шаги перед запуском остальных контейнеров (например, применяет миграции к базе данных).

!!! info "Информация" При использовании Kubernetes, это может быть Инициализирующий контейнер.

При отсутствии такой необходимости (допустим, не нужно применять миграции к базе данных, а только проверить, что она готова принимать соединения), вы можете проводить такую проверку в каждом контейнере перед запуском его основного процесса и запускать все контейнеры одновременно.

Только один контейнер

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

Официальный Docker-образ с Gunicorn и Uvicorn

Я подготовил для вас Docker-образ, в который включён Gunicorn управляющий процессами (воркерами) Uvicorn, в соответствии с концепциями рассмотренными в предыдущей главе: Рабочие процессы сервера (воркеры) - Gunicorn совместно с Uvicorn{.internal-link target=_blank}.

Этот образ может быть полезен для ситуаций описанных тут: Множество процессов внутри контейнера для особых случаев.

!!! warning "Предупреждение" Скорее всего у вас нет необходимости в использовании этого образа или подобного ему и лучше создать свой образ с нуля как описано тут: Создать Docker-образ для FastAPI.

В этом образе есть автоматический механизм подстройки для запуска необходимого количества процессов в соответствии с доступным количеством ядер процессора.

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

Он также поддерживает прохождение Подготовительных шагов при запуске контейнеров при помощи скрипта.

!!! tip "Подсказка" Для просмотра всех возможных настроек перейдите на страницу этого Docker-образа: tiangolo/uvicorn-gunicorn-fastapi.

Количество процессов в официальном Docker-образе

Количество процессов в этом образе вычисляется автоматически и зависит от доступного количества ядер центрального процессора.

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

Но вы можете изменять и обновлять конфигурацию с помощью переменных окружения и т.п.

Поскольку количество процессов зависит от процессора, на котором работает контейнер, объём потребляемой памяти также будет зависеть от этого.

А значит, если вашему приложению требуется много оперативной памяти (например, оно использует модели машинного обучения) и Ваш сервер имеет центральный процессор с большим количеством ядер, но не слишком большим объёмом оперативной памяти, то может дойти до того, что контейнер попытается занять памяти больше, чем доступно, из-за чего будет падение производительности (или сервер вовсе упадёт). 🚨

Написание Dockerfile

Итак, теперь мы можем написать Dockerfile основанный на этом официальном Docker-образе:

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9

COPY ./requirements.txt /app/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY ./app /app

Большие приложения

Если вы успели ознакомиться с разделом Приложения содержащие много файлов{.internal-link target=_blank}, состоящие из множества файлов, Ваш Dockerfile может выглядеть так:

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9

COPY ./requirements.txt /app/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY ./app /app/app

Как им пользоваться

Если вы используете Kubernetes (или что-то вроде того), скорее всего вам не нужно использовать официальный Docker-образ (или другой похожий) в качестве основы, так как управление количеством запущенных контейнеров должно быть настроено на уровне кластера. В таком случае лучше создать образ с нуля, как описано в разделе Создать Docker-образ для FastAPI.

Официальный образ может быть полезен в отдельных случаях, описанных выше в разделе Множество процессов внутри контейнера для особых случаев. Например, если ваше приложение достаточно простое, не требует запуска в кластере и способно уместиться в один контейнер, то его настройки по умолчанию будут работать довольно хорошо. Или же вы развертываете его с помощью Docker Compose, работаете на одном сервере и т. д

Развёртывание образа контейнера

После создания образа контейнера существует несколько способов его развёртывания.

Например:

  • С использованием Docker Compose при развёртывании на одном сервере
  • С использованием Kubernetes в кластере
  • С использованием режима Docker Swarm в кластере
  • С использованием других инструментов, таких как Nomad
  • С использованием облачного сервиса, который будет управлять разворачиванием вашего контейнера

Docker-образ и Poetry

Если вы пользуетесь Poetry для управления зависимостями вашего проекта, то можете использовать многоэтапную сборку образа:

# (1)
FROM python:3.9 as requirements-stage

# (2)
WORKDIR /tmp

# (3)
RUN pip install poetry

# (4)
COPY ./pyproject.toml ./poetry.lock* /tmp/

# (5)
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes

# (6)
FROM python:3.9

# (7)
WORKDIR /code

# (8)
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt

# (9)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (10)
COPY ./app /code/app

# (11)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
  1. Это первый этап, которому мы дадим имя requirements-stage.

  2. Установите директорию /tmp в качестве рабочей директории.

    В ней будет создан файл requirements.txt

  3. На этом шаге установите Poetry.

  4. Скопируйте файлы pyproject.toml и poetry.lock в директорию /tmp.

    Поскольку название файла написано как ./poetry.lock** в конце), то ничего не сломается, если такой файл не будет найден.

  5. Создайте файл requirements.txt.

  6. Это второй (и последний) этап сборки, который и создаст окончательный образ контейнера.

  7. Установите директорию /code в качестве рабочей.

  8. Скопируйте файл requirements.txt в директорию /code.

    Этот файл находится в образе, созданном на предыдущем этапе, которому мы дали имя requirements-stage, потому при копировании нужно написать --from-requirements-stage.

  9. Установите зависимости, указанные в файле requirements.txt.

  10. Скопируйте папку app в папку /code.

  11. Запустите uvicorn, указав ему использовать объект app, расположенный в app.main.

!!! tip "Подсказка" Если ткнёте на кружок с плюсом, то увидите объяснения, что происходит в этой строке.

Этапы сборки Docker-образа являются частью Dockerfile и работают как временные образы контейнеров. Они нужны только для создания файлов, используемых в дальнейших этапах.

Первый этап был нужен только для установки Poetry и создания файла requirements.txt, в которым прописаны зависимости вашего проекта, взятые из файла pyproject.toml.

На следующем этапе pip будет использовать файл requirements.txt.

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

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

А на последнем этапе, придерживаясь описанных ранее правил, создаётся итоговый образ

Использование прокси-сервера завершения TLS и Poetry

И снова повторюсь, если используете прокси-сервер (балансировщик нагрузки), такой как Nginx или Traefik, добавьте в команду запуска опцию --proxy-headers:

CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]

Резюме

При помощи систем контейнеризации (таких, как Docker и Kubernetes), становится довольно просто обрабатывать все концепции развертывания:

  • Использование более безопасного протокола HTTPS
  • Настройки запуска приложения
  • Перезагрузка приложения
  • Запуск нескольких экземпляров приложения
  • Управление памятью
  • Использование перечисленных функций перед запуском приложения

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

Позаботившись о порядке написания инструкций в Dockerfile, вы сможете использовать кэш Docker'а, минимизировав время сборки, максимально повысив свою производительность (и не заскучать). 😎

В некоторых особых случаях вы можете использовать официальный образ Docker для FastAPI. 🤓