# FastAPI в контейнерах — Docker { #fastapi-in-containers-docker } При развёртывании приложений FastAPI распространённый подход — собирать **образ контейнера на Linux**. Обычно это делают с помощью **Docker**. Затем такой образ контейнера можно развернуть несколькими способами. Использование Linux-контейнеров даёт ряд преимуществ: **безопасность**, **воспроизводимость**, **простоту** и другие. /// tip | Подсказка Нет времени и вы уже знакомы с этим? Перейдите к [`Dockerfile` ниже 👇](#build-a-docker-image-for-fastapi). ///
Предпросмотр Dockerfile 👀 ```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 ["fastapi", "run", "app/main.py", "--port", "80"] # Если запускаете за прокси, например Nginx или Traefik, добавьте --proxy-headers # CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] ```
## Что такое контейнер { #what-is-a-container } Контейнеры (в основном Linux-контейнеры) — это очень **легковесный** способ упаковать приложения вместе со всеми их зависимостями и необходимыми файлами, изолировав их от других контейнеров (других приложений или компонентов) в той же системе. Linux-контейнеры запускаются, используя то же ядро Linux хоста (машины, виртуальной машины, облачного сервера и т.п.). Это означает, что они очень легковесные (по сравнению с полноценными виртуальными машинами, эмулирующими целую операционную систему). Таким образом, контейнеры потребляют **малое количество ресурсов**, сопоставимое с запуском процессов напрямую (виртуальная машина потребовала бы намного больше ресурсов). У контейнеров также есть собственные **изолированные** выполняемые процессы (обычно всего один процесс), файловая система и сеть, что упрощает развёртывание, безопасность, разработку и т.д. ## Что такое образ контейнера { #what-is-a-container-image } **Контейнер** запускается из **образа контейнера**. Образ контейнера — это **статическая** версия всех файлов, переменных окружения и команды/программы по умолчанию, которые должны присутствовать в контейнере. Здесь **статическая** означает, что **образ** не запущен, он не выполняется — это только упакованные файлы и метаданные. В противоположность «**образу контейнера**» (хранящему статическое содержимое), «**контейнер**» обычно означает запущенный экземпляр, то, что **выполняется**. Когда **контейнер** запущен (на основе **образа контейнера**), он может создавать или изменять файлы, переменные окружения и т.д.. Эти изменения существуют только внутри контейнера и не сохраняются в исходном образе контейнера (не записываются на диск). Образ контейнера можно сравнить с **файлами программы**, например `python` и каким-то файлом `main.py`. А сам **контейнер** (в отличие от **образа контейнера**) — это фактически запущенный экземпляр образа, сопоставимый с **процессом**. По сути, контейнер работает только тогда, когда в нём есть **запущенный процесс** (и обычно это один процесс). Контейнер останавливается, когда в нём не остаётся запущенных процессов. ## Образы контейнеров { #container-images } Docker — один из основных инструментов для создания и управления **образами контейнеров** и **контейнерами**. Существует публичный Docker Hub с готовыми **официальными образами** для многих инструментов, окружений, баз данных и приложений. Например, есть официальный образ Python. А также множество образов для разных вещей, например баз данных: * PostgreSQL * MySQL * MongoDB * Redis, и т.д. Используя готовые образы, очень легко **комбинировать** разные инструменты и использовать их. Например, чтобы попробовать новую базу данных. В большинстве случаев можно воспользоваться **официальными образами** и просто настроить их через переменные окружения. Таким образом, во многих случаях вы можете изучить контейнеры и Docker и переиспользовать эти знания с множеством различных инструментов и компонентов. Например, вы можете запустить **несколько контейнеров**: с базой данных, Python-приложением, веб-сервером с фронтендом на React и связать их через внутреннюю сеть. Все системы управления контейнерами (такие как Docker или Kubernetes) имеют интегрированные возможности для такого сетевого взаимодействия. ## Контейнеры и процессы { #containers-and-processes } **Образ контейнера** обычно включает в свои метаданные программу или команду по умолчанию, которую следует запускать при старте **контейнера**, а также параметры, передаваемые этой программе. Это очень похоже на запуск команды в терминале. Когда **контейнер** стартует, он выполняет указанную команду/программу (хотя вы можете переопределить это и запустить другую команду/программу). Контейнер работает до тех пор, пока работает его **главный процесс** (команда или программа). Обычно в контейнере есть **один процесс**, но главный процесс может запускать подпроцессы, и тогда в том же контейнере будет **несколько процессов**. Нельзя иметь работающий контейнер без **хотя бы одного запущенного процесса**. Если главный процесс останавливается, контейнер останавливается. ## Создать Docker-образ для FastAPI { #build-a-docker-image-for-fastapi } Итак, давайте что-нибудь соберём! 🚀 Я покажу, как собрать **Docker-образ** для FastAPI **с нуля** на основе **официального образа Python**. Именно так стоит делать в **большинстве случаев**, например: * При использовании **Kubernetes** или похожих инструментов * При запуске на **Raspberry Pi** * При использовании облачного сервиса, который запускает для вас образ контейнера и т.п. ### Зависимости пакетов { #package-requirements } Обычно **зависимости** вашего приложения описаны в каком-то файле. Конкретный формат зависит в основном от инструмента, которым вы **устанавливаете** эти зависимости. Чаще всего используется файл `requirements.txt` с именами пакетов и их версиями по одному на строку. Разумеется, вы будете придерживаться тех же идей, что описаны здесь: [О версиях FastAPI](versions.md){.internal-link target=_blank}, чтобы задать диапазоны версий. Например, ваш `requirements.txt` может выглядеть так: ``` fastapi[standard]>=0.113.0,<0.114.0 pydantic>=2.7.0,<3.0.0 ``` И обычно вы установите эти зависимости командой `pip`, например:
```console $ pip install -r requirements.txt ---> 100% Successfully installed fastapi pydantic ```
/// info | Информация Существуют и другие форматы и инструменты для описания и установки зависимостей. /// ### Создать код **FastAPI** { #create-the-fastapi-code } * Создайте директорию `app` и перейдите в неё. * Создайте пустой файл `__init__.py`. * Создайте файл `main.py` со следующим содержимым: ```Python 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 } Теперь в той же директории проекта создайте файл `Dockerfile`: ```{ .dockerfile .annotate } # (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 ["fastapi", "run", "app/main.py", "--port", "80"] ``` 1. Начинаем с официального базового образа Python. 2. Устанавливаем текущую рабочую директорию в `/code`. Здесь мы разместим файл `requirements.txt` и директорию `app`. 3. Копируем файл с зависимостями в директорию `/code`. Сначала копируйте **только** файл с зависимостями, не остальной код. Так как этот файл **меняется нечасто**, Docker определит это и использует **кэш** на этом шаге, что позволит использовать кэш и на следующем шаге. 4. Устанавливаем зависимости из файла с требованиями. Опция `--no-cache-dir` указывает `pip` не сохранять загруженные пакеты локально, т.к. это нужно только если `pip` будет запускаться снова для установки тех же пакетов, а при работе с контейнерами это обычно не требуется. /// note | Заметка `--no-cache-dir` относится только к `pip` и не имеет отношения к Docker или контейнерам. /// Опция `--upgrade` указывает `pip` обновлять пакеты, если они уже установлены. Поскольку предыдущий шаг с копированием файла может быть обработан **кэшем Docker**, этот шаг также **использует кэш Docker**, когда это возможно. Использование кэша на этом шаге **сэкономит** вам много **времени** при повторных сборках образа во время разработки, вместо того чтобы **загружать и устанавливать** все зависимости **каждый раз**. 5. Копируем директорию `./app` внутрь директории `/code`. Так как здесь весь код, который **меняется чаще всего**, кэш Docker **вряд ли** будет использоваться для этого шагa или **последующих шагов**. Поэтому важно разместить этот шаг **ближе к концу** `Dockerfile`, чтобы оптимизировать время сборки образа контейнера. 6. Указываем **команду** для запуска `fastapi run`, под капотом используется Uvicorn. `CMD` принимает список строк, каждая из которых — это то, что вы бы ввели в командной строке, разделяя пробелами. Эта команда будет выполнена из **текущей рабочей директории**, той самой `/code`, которую вы задали выше `WORKDIR /code`. /// tip | Подсказка Посмотрите, что делает каждая строка, кликнув по номеру рядом со строкой. 👆 /// /// warning | Предупреждение Всегда используйте **exec-форму** инструкции `CMD`, как описано ниже. /// #### Используйте `CMD` — exec-форма { #use-cmd-exec-form } Инструкцию Docker `CMD` можно писать в двух формах: ✅ **Exec**-форма: ```Dockerfile # ✅ Делайте так CMD ["fastapi", "run", "app/main.py", "--port", "80"] ``` ⛔️ **Shell**-форма: ```Dockerfile # ⛔️ Не делайте так CMD fastapi run app/main.py --port 80 ``` Обязательно используйте **exec**-форму, чтобы FastAPI мог корректно завершаться и чтобы срабатывали [события lifespan](../advanced/events.md){.internal-link target=_blank}. Подробнее об этом читайте в документации Docker о shell- и exec-формах. Это особенно заметно при использовании `docker compose`. См. раздел FAQ Docker Compose с техническими подробностями: Почему мои сервисы пересоздаются или останавливаются 10 секунд?. #### Структура директорий { #directory-structure } Теперь у вас должна быть такая структура: ``` . ├── app │   ├── __init__.py │ └── main.py ├── Dockerfile └── requirements.txt ``` #### За прокси-сервером TLS терминации { #behind-a-tls-termination-proxy } Если вы запускаете контейнер за прокси-сервером завершения TLS (балансировщиком нагрузки), таким как Nginx или Traefik, добавьте опцию `--proxy-headers`. Это сообщит Uvicorn (через FastAPI CLI), что приложение работает за HTTPS и можно доверять соответствующим заголовкам. ```Dockerfile CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] ``` #### Кэш Docker { #docker-cache } В этом `Dockerfile` есть важная хитрость: мы сначала копируем **только файл с зависимостями**, а не весь код. Вот зачем. ```Dockerfile COPY ./requirements.txt /code/requirements.txt ``` Docker и подобные инструменты **строят** образы контейнеров **инкрементально**, добавляя **слой за слоем**, начиная с первой строки `Dockerfile` и добавляя любые файлы, создаваемые каждой инструкцией `Dockerfile`. Docker и подобные инструменты также используют **внутренний кэш** при сборке образа: если файл не изменился с момента предыдущей сборки, будет **переиспользован слой**, созданный в прошлый раз, вместо повторного копирования файла и создания нового слоя с нуля. Само по себе избегание копирования всех файлов не всегда даёт много, но благодаря использованию кэша на этом шаге Docker сможет **использовать кэш и на следующем шаге**. Например, на шаге установки зависимостей: ```Dockerfile RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt ``` Файл с зависимостями **меняется нечасто**. Поэтому, копируя только его, Docker сможет **использовать кэш** для этого шага. А затем Docker сможет **использовать кэш и на следующем шаге**, где скачиваются и устанавливаются зависимости. Здесь мы как раз **экономим много времени**. ✨ ...и не скучаем в ожидании. 😪😆 Скачивание и установка зависимостей **может занять минуты**, но использование **кэша** — **секунды**. Поскольку во время разработки вы будете пересобирать образ снова и снова, чтобы проверить изменения в коде, суммарно это сэкономит немало времени. Затем, ближе к концу `Dockerfile`, мы копируем весь код. Так как он **меняется чаще всего**, мы ставим этот шаг в конец, потому что почти всегда всё, что после него, уже не сможет использовать кэш. ```Dockerfile COPY ./app /code/app ``` ### Собрать Docker-образ { #build-the-docker-image } Теперь, когда все файлы на месте, соберём образ контейнера. * Перейдите в директорию проекта (где ваш `Dockerfile` и директория `app`). * Соберите образ FastAPI:
```console $ docker build -t myimage . ---> 100% ```
/// tip | Подсказка Обратите внимание на точку `.` в конце — это то же самое, что `./`. Так мы указываем Docker, из какой директории собирать образ контейнера. В данном случае это текущая директория (`.`). /// ### Запустить Docker-контейнер { #start-the-docker-container } * Запустите контейнер на основе вашего образа:
```console $ docker run -d --name mycontainer -p 80:80 myimage ```
## Проверка { #check-it } Проверьте работу по адресу вашего Docker-хоста, например: http://192.168.99.100/items/5?q=somequery или http://127.0.0.1/items/5?q=somequery (или аналогичный URL вашего Docker-хоста). Вы увидите что-то вроде: ```JSON {"item_id": 5, "q": "somequery"} ``` ## Интерактивная документация API { #interactive-api-docs } Теперь зайдите на http://192.168.99.100/docs или http://127.0.0.1/docs (или аналогичный URL вашего Docker-хоста). Вы увидите автоматическую интерактивную документацию API (на базе Swagger UI): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) ## Альтернативная документация API { #alternative-api-docs } Также можно открыть http://192.168.99.100/redoc или http://127.0.0.1/redoc (или аналогичный URL вашего Docker-хоста). Вы увидите альтернативную автоматическую документацию (на базе ReDoc): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) ## Собрать Docker-образ для однофайлового FastAPI { #build-a-docker-image-with-a-single-file-fastapi } Если ваше приложение FastAPI — один файл, например `main.py` без директории `./app`, структура файлов может быть такой: ``` . ├── Dockerfile ├── main.py └── requirements.txt ``` Тогда в `Dockerfile` нужно изменить пути копирования: ```{ .dockerfile .annotate hl_lines="10 13" } 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 ["fastapi", "run", "main.py", "--port", "80"] ``` 1. Копируем файл `main.py` напрямую в `/code` (без директории `./app`). 2. Используем `fastapi run` для запуска приложения из одного файла `main.py`. Когда вы передаёте файл в `fastapi run`, он автоматически определит, что это одиночный файл, а не часть пакета, и поймёт, как его импортировать и запустить ваше FastAPI-приложение. 😎 ## Концепции развертывания { #deployment-concepts } Ещё раз рассмотрим [концепции развертывания](concepts.md){.internal-link target=_blank} применительно к контейнерам. Контейнеры главным образом упрощают **сборку и развёртывание** приложения, но не навязывают конкретный подход к этим **концепциям развертывания**, и существует несколько стратегий. **Хорошая новость** в том, что при любой стратегии есть способ охватить все концепции развертывания. 🎉 Рассмотрим эти **концепции развертывания** в терминах контейнеров: * HTTPS * Запуск при старте * Перезапуски * Репликация (количество запущенных процессов) * Память * Предварительные шаги перед запуском ## HTTPS { #https } Если мы рассматриваем только **образ контейнера** для приложения FastAPI (и далее запущенный **контейнер**), то HTTPS обычно обрабатывается **внешним** инструментом. Это может быть другой контейнер, например с Traefik, который берёт на себя **HTTPS** и **автоматическое** получение **сертификатов**. /// tip | Подсказка У Traefik есть интеграции с Docker, Kubernetes и другими, поэтому очень легко настроить и сконфигурировать HTTPS для ваших контейнеров. /// В качестве альтернативы HTTPS может быть реализован как сервис облачного провайдера (при этом приложение всё равно работает в контейнере). ## Запуск при старте и перезапуски { #running-on-startup-and-restarts } Обычно есть другой инструмент, отвечающий за **запуск и работу** вашего контейнера. Это может быть сам **Docker**, **Docker Compose**, **Kubernetes**, **облачный сервис** и т.п. В большинстве (или во всех) случаев есть простая опция, чтобы включить запуск контейнера при старте системы и перезапуски при сбоях. Например, в Docker это опция командной строки `--restart`. Без контейнеров обеспечить запуск при старте и перезапуски может быть сложно. Но при **работе с контейнерами** в большинстве случаев этот функционал доступен по умолчанию. ✨ ## Репликация — количество процессов { #replication-number-of-processes } Если у вас есть кластер машин с **Kubernetes**, Docker Swarm Mode, Nomad или другой похожей системой для управления распределёнными контейнерами на нескольких машинах, скорее всего вы будете **управлять репликацией** на **уровне кластера**, а не использовать **менеджер процессов** (например, Uvicorn с воркерами) в каждом контейнере. Одна из таких систем управления распределёнными контейнерами, как Kubernetes, обычно имеет встроенный способ управлять **репликацией контейнеров**, поддерживая **балансировку нагрузки** для входящих запросов — всё это на **уровне кластера**. В таких случаях вы, скорее всего, захотите собрать **Docker-образ с нуля**, как [описано выше](#dockerfile), установить зависимости и запускать **один процесс Uvicorn** вместо множества воркеров Uvicorn. ### Балансировщик нагрузки { #load-balancer } При использовании контейнеров обычно есть компонент, **слушающий главный порт**. Это может быть другой контейнер — **прокси завершения TLS** для обработки **HTTPS** или похожий инструмент. Поскольку этот компонент принимает **нагрузку** запросов и распределяет её между воркерами **сбалансированно**, его часто называют **балансировщиком нагрузки**. /// tip | Подсказка Тот же компонент **прокси завершения TLS**, который обрабатывает HTTPS, скорее всего также будет **балансировщиком нагрузки**. /// При работе с контейнерами система, которую вы используете для запуска и управления ими, уже имеет внутренние средства для передачи **сетевого взаимодействия** (например, HTTP-запросов) от **балансировщика нагрузки** (который также может быть **прокси завершения TLS**) к контейнеру(-ам) с вашим приложением. ### Один балансировщик — несколько контейнеров-воркеров { #one-load-balancer-multiple-worker-containers } При работе с **Kubernetes** или похожими системами управления распределёнными контейнерами их внутренние механизмы сети позволяют одному **балансировщику нагрузки**, слушающему главный **порт**, передавать запросы в **несколько контейнеров**, где запущено ваше приложение. Каждый такой контейнер с вашим приложением обычно имеет **только один процесс** (например, процесс Uvicorn с вашим приложением FastAPI). Все они — **одинаковые контейнеры**, запускающие одно и то же, но у каждого свой процесс, память и т.п. Так вы используете **параллелизм** по **разным ядрам** CPU или даже **разным машинам**. Система распределённых контейнеров с **балансировщиком нагрузки** будет **распределять запросы** между контейнерами с вашим приложением **по очереди**. То есть каждый запрос может обрабатываться одним из нескольких **реплицированных контейнеров**. Обычно такой **балансировщик нагрузки** может также обрабатывать запросы к *другим* приложениям в вашем кластере (например, к другому домену или под другим префиксом пути URL) и направлять их к нужным контейнерам этого *другого* приложения. ### Один процесс на контейнер { #one-process-per-container } В таком сценарии, скорее всего, вы захотите иметь **один (Uvicorn) процесс на контейнер**, так как репликация уже управляется на уровне кластера. Поэтому в контейнере **не нужно** поднимать несколько воркеров, например через опцию командной строки `--workers`. Нужен **один процесс Uvicorn** на контейнер (но, возможно, несколько контейнеров). Наличие отдельного менеджера процессов внутри контейнера (как при нескольких воркерах) только добавит **лишнюю сложность**, которую, вероятно, уже берёт на себя ваша кластерная система. ### Контейнеры с несколькими процессами и особые случаи { #containers-with-multiple-processes-and-special-cases } Конечно, есть **особые случаи**, когда может понадобиться **контейнер** с несколькими **воркерами Uvicorn** внутри. В таких случаях вы можете использовать опцию командной строки `--workers`, чтобы указать нужное количество воркеров: ```{ .dockerfile .annotate } 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 # (1)! CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] ``` 1. Здесь мы используем опцию `--workers`, чтобы установить число воркеров равным 4. Примеры, когда это может быть уместно: #### Простое приложение { #a-simple-app } Вам может понадобиться менеджер процессов в контейнере, если приложение **достаточно простое**, чтобы запускаться на **одном сервере**, а не в кластере. #### Docker Compose { #docker-compose } Вы можете развёртывать на **одном сервере** (не кластере) с **Docker Compose**, и у вас не будет простого способа управлять репликацией контейнеров (в Docker Compose), сохраняя общую сеть и **балансировку нагрузки**. Тогда вы можете захотеть **один контейнер** с **менеджером процессов**, который запускает **несколько воркеров** внутри. --- Главное — **ни одно** из этих правил не является **строго обязательным**. Используйте эти идеи, чтобы **оценить свой конкретный случай** и решить, какой подход лучше для вашей системы, учитывая: * Безопасность — HTTPS * Запуск при старте * Перезапуски * Репликацию (количество запущенных процессов) * Память * Предварительные шаги перед запуском ## Память { #memory } Если вы запускаете **один процесс на контейнер**, у каждого контейнера будет более-менее чётко определённый, стабильный и ограниченный объём потребляемой памяти (контейнеров может быть несколько при репликации). Затем вы можете задать такие же лимиты и требования по памяти в конфигурации вашей системы управления контейнерами (например, в **Kubernetes**). Так система сможет **реплицировать контейнеры** на **доступных машинах**, учитывая объём необходимой памяти и доступной памяти в машинах кластера. Если приложение **простое**, это, вероятно, **не будет проблемой**, и жёсткие лимиты памяти можно не указывать. Но если вы **используете много памяти** (например, с моделями **Машинного обучения**), проверьте, сколько памяти потребляется, и отрегулируйте **число контейнеров** на **каждой машине** (и, возможно, добавьте машины в кластер). Если вы запускаете **несколько процессов в контейнере**, нужно убедиться, что их суммарное потребление **не превысит доступную память**. ## Предварительные шаги перед запуском и контейнеры { #previous-steps-before-starting-and-containers } Если вы используете контейнеры (например, Docker, Kubernetes), есть два основных подхода. ### Несколько контейнеров { #multiple-containers } Если у вас **несколько контейнеров**, и, вероятно, каждый запускает **один процесс** (например, в кластере **Kubernetes**), то вы, скорее всего, захотите иметь **отдельный контейнер**, выполняющий **предварительные шаги** в одном контейнере и одном процессе **до** запуска реплицированных контейнеров-воркеров. /// info | Информация Если вы используете Kubernetes, это, вероятно, будет Init Container. /// Если в вашем случае нет проблемы с тем, чтобы выполнять эти предварительные шаги **многократно и параллельно** (например, вы не запускаете миграции БД, а только проверяете готовность БД), вы можете просто выполнить их в каждом контейнере прямо перед стартом основного процесса. ### Один контейнер { #single-container } Если у вас простая схема с **одним контейнером**, который затем запускает несколько **воркеров** (или один процесс), можно выполнить подготовительные шаги в этом же контейнере непосредственно перед запуском процесса с приложением. ### Базовый Docker-образ { #base-docker-image } Ранее существовал официальный Docker-образ FastAPI: tiangolo/uvicorn-gunicorn-fastapi. Сейчас он помечен как устаревший. ⛔️ Скорее всего, вам **не стоит** использовать этот базовый образ (или какой-либо аналогичный). Если вы используете **Kubernetes** (или другое) и уже настраиваете **репликацию** на уровне кластера через несколько **контейнеров**, в этих случаях лучше **собрать образ с нуля**, как описано выше: [Создать Docker-образ для FastAPI](#build-a-docker-image-for-fastapi). А если вам нужны несколько воркеров, просто используйте опцию командной строки `--workers`. /// note | Технические подробности Этот Docker-образ был создан в то время, когда Uvicorn не умел управлять и перезапускать «упавших» воркеров, и приходилось использовать Gunicorn вместе с Uvicorn, что добавляло заметную сложность, лишь бы Gunicorn управлял и перезапускал воркеров Uvicorn. Но теперь, когда Uvicorn (и команда `fastapi`) поддерживают `--workers`, нет причин использовать базовый Docker-образ вместо сборки своего (кода получается примерно столько же 😅). /// ## Развёртывание образа контейнера { #deploy-the-container-image } После того как у вас есть образ контейнера (Docker), его можно развёртывать несколькими способами. Например: * С **Docker Compose** на одном сервере * В кластере **Kubernetes** * В кластере Docker Swarm Mode * С другим инструментом, например Nomad * С облачным сервисом, который принимает ваш образ контейнера и разворачивает его ## Docker-образ с `uv` { #docker-image-with-uv } Если вы используете uv для установки и управления проектом, следуйте их руководству по Docker для uv. ## Резюме { #recap } Используя системы контейнеризации (например, **Docker** и **Kubernetes**), довольно просто закрыть все **концепции развертывания**: * HTTPS * Запуск при старте * Перезапуски * Репликация (количество запущенных процессов) * Память * Предварительные шаги перед запуском В большинстве случаев вы, вероятно, не захотите использовать какой-либо базовый образ, а вместо этого **соберёте образ контейнера с нуля** на основе официального Docker-образа Python. Заботясь о **порядке** инструкций в `Dockerfile`и используя **кэш Docker**, вы можете **минимизировать время сборки**, чтобы повысить продуктивность (и не скучать). 😎