# 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):

## Альтернативная документация API { #alternative-api-docs }
Также можно открыть http://192.168.99.100/redoc или http://127.0.0.1/redoc (или аналогичный URL вашего Docker-хоста).
Вы увидите альтернативную автоматическую документацию (на базе ReDoc):

## Собрать 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**, вы можете **минимизировать время сборки**, чтобы повысить продуктивность (и не скучать). 😎