# FastAPI в контейнерах — Docker { #fastapi-in-containers-docker }

При развёртывании приложений FastAPI распространённый подход — собирать **образ контейнера на Linux**. Обычно это делают с помощью [**Docker**](https://www.docker.com/). Затем такой образ контейнера можно развернуть несколькими способами.

Использование Linux-контейнеров даёт ряд преимуществ: **безопасность**, **воспроизводимость**, **простоту** и другие.

/// tip | Подсказка

Нет времени и вы уже знакомы с этим? Перейдите к [`Dockerfile` ниже 👇](#build-a-docker-image-for-fastapi).

///

<details>
<summary>Предпросмотр Dockerfile 👀</summary>

```Dockerfile
FROM python:3.14

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"]
```

</details>

## Что такое контейнер { #what-is-a-container }

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

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

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

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

## Что такое образ контейнера { #what-is-a-container-image }

**Контейнер** запускается из **образа контейнера**.

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

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

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

Образ контейнера можно сравнить с **файлами программы**, например `python` и каким-то файлом `main.py`.

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

## Образы контейнеров { #container-images }

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

Существует публичный [Docker Hub](https://hub.docker.com/) с готовыми **официальными образами** для многих инструментов, окружений, баз данных и приложений.

Например, есть официальный [образ Python](https://hub.docker.com/_/python).

А также множество образов для разных вещей, например баз данных:

* [PostgreSQL](https://hub.docker.com/_/postgres)
* [MySQL](https://hub.docker.com/_/mysql)
* [MongoDB](https://hub.docker.com/_/mongo)
* [Redis](https://hub.docker.com/_/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), чтобы задать диапазоны версий.

Например, ваш `requirements.txt` может выглядеть так:

```
fastapi[standard]>=0.113.0,<0.114.0
pydantic>=2.7.0,<3.0.0
```

И обычно вы установите эти зависимости командой `pip`, например:

<div class="termy">

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

</div>

/// note | Заметка

Существуют и другие форматы и инструменты для описания и установки зависимостей.

///

### Создать код **FastAPI** { #create-the-fastapi-code }

* Создайте директорию `app` и перейдите в неё.
* Создайте пустой файл `__init__.py`.
* Создайте файл `main.py` со следующим содержимым:

```Python
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: str | None = None):
    return {"item_id": item_id, "q": q}
```

### Dockerfile { #dockerfile }

Теперь в той же директории проекта создайте файл `Dockerfile`:

```{ .dockerfile .annotate }
# (1)!
FROM python:3.14

# (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 **вряд ли** будет использоваться для этого шага или **последующих шагов**.

    Поэтому важно разместить этот шаг **ближе к концу** `Dockerfile`, чтобы оптимизировать время сборки образа контейнера.

6. Указываем **команду** для запуска `fastapi run`, под капотом используется Uvicorn.

    `CMD` принимает список строк, каждая из которых — это то, что вы бы ввели в командной строке, разделяя пробелами.

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

/// tip | Подсказка

Посмотрите, что делает каждая строка, кликнув по номеру рядом со строкой. 👆

///

/// warning | Предупреждение

Всегда используйте **exec-форму** инструкции `CMD`, как описано ниже.

///

#### Используйте `CMD` — exec-форма { #use-cmd-exec-form }

Инструкцию Docker [`CMD`](https://docs.docker.com/reference/dockerfile/#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).

Подробнее об этом читайте в [документации Docker о shell- и exec-формах](https://docs.docker.com/reference/dockerfile/#shell-and-exec-form).

Это особенно заметно при использовании `docker compose`. См. раздел FAQ Docker Compose с техническими подробностями: [Почему мои сервисы пересоздаются или останавливаются 10 секунд?](https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop).

#### Структура директорий { #directory-structure }

Теперь у вас должна быть такая структура:

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

#### За прокси-сервером TSL-терминации { #behind-a-tls-termination-proxy }

Если вы запускаете контейнер за прокси-сервером TSL-терминации (балансировщиком нагрузки), таким как 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:

<div class="termy">

```console
$ docker build -t myimage .

---> 100%
```

</div>

/// tip | Подсказка

Обратите внимание на точку `.` в конце — это то же самое, что `./`. Так мы указываем Docker, из какой директории собирать образ контейнера.

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

///

### Запустить Docker-контейнер { #start-the-docker-container }

* Запустите контейнер на основе вашего образа:

<div class="termy">

```console
$ docker run -d --name mycontainer -p 80:80 myimage
```

</div>

## Проверка { #check-it }

Проверьте работу по адресу вашего Docker-хоста, например: [http://192.168.99.100/items/5?q=somequery](http://192.168.99.100/items/5?q=somequery) или [http://127.0.0.1/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://192.168.99.100/docs) или [http://127.0.0.1/docs](http://127.0.0.1/docs) (или аналогичный URL вашего Docker-хоста).

Вы увидите автоматическую интерактивную документацию API (на базе [Swagger UI](https://github.com/swagger-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://192.168.99.100/redoc) или [http://127.0.0.1/redoc](http://127.0.0.1/redoc) (или аналогичный URL вашего Docker-хоста).

Вы увидите альтернативную автоматическую документацию (на базе [ReDoc](https://github.com/Rebilly/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.14

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) применительно к контейнерам.

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

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

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

* HTTPS
* Запуск при старте
* Перезапуски
* Репликация (количество запущенных процессов)
* Память
* Предварительные шаги перед запуском

## HTTPS { #https }

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

Это может быть другой контейнер, например с [Traefik](https://traefik.io/), который берёт на себя **HTTPS** и **автоматическое** получение **сертификатов**.

/// tip | Подсказка

У Traefik есть интеграции с Docker, Kubernetes и другими, поэтому очень легко настроить и сконфигурировать HTTPS для ваших контейнеров.

///

В качестве альтернативы HTTPS может быть реализован как сервис облачного провайдера (при этом приложение всё равно работает в контейнере).

## Запуск при старте и перезапуски { #running-on-startup-and-restarts }

Обычно есть другой инструмент, отвечающий за **запуск и работу** вашего контейнера.

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

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

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

## Репликация — количество процессов { #replication-number-of-processes }

Если у вас есть <dfn title="Группа машин, настроенных так, чтобы быть соединенными и работать вместе определенным образом.">кластер</dfn> машин с **Kubernetes**, Docker Swarm Mode, Nomad или другой похожей системой для управления распределёнными контейнерами на нескольких машинах, скорее всего вы будете **управлять репликацией** на **уровне кластера**, а не использовать **менеджер процессов** (например, Uvicorn с воркерами) в каждом контейнере.

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

В таких случаях вы, скорее всего, захотите собрать **Docker-образ с нуля**, как [описано выше](#dockerfile), установить зависимости и запускать **один процесс Uvicorn** вместо множества воркеров Uvicorn.

### Балансировщик нагрузки { #load-balancer }

При использовании контейнеров обычно есть компонент, **слушающий главный порт**. Это может быть другой контейнер — **прокси-сервер TSL-терминации** для обработки **HTTPS** или похожий инструмент.

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

/// tip | Подсказка

Тот же компонент **прокси-сервер TSL-терминации**, который обрабатывает HTTPS, скорее всего также будет **балансировщиком нагрузки**.

///

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

### Один балансировщик — несколько контейнеров-воркеров { #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.14

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**), то вы, скорее всего, захотите иметь **отдельный контейнер**, выполняющий **предварительные шаги** в одном контейнере и одном процессе **до** запуска реплицированных контейнеров-воркеров.

/// note | Заметка

Если вы используете Kubernetes, это, вероятно, будет [Init Container](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/).

///

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

### Один контейнер { #single-container }

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

### Базовый Docker-образ { #base-docker-image }

Ранее существовал официальный Docker-образ FastAPI: [tiangolo/uvicorn-gunicorn-fastapi](https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker). Сейчас он помечен как устаревший. ⛔️

Скорее всего, вам **не стоит** использовать этот базовый образ (или какой-либо аналогичный).

Если вы используете **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](https://github.com/astral-sh/uv) для установки и управления проектом, следуйте их [руководству по Docker для uv](https://docs.astral.sh/uv/guides/integration/docker/).

## Резюме { #recap }

Используя системы контейнеризации (например, **Docker** и **Kubernetes**), довольно просто закрыть все **концепции развертывания**:

* HTTPS
* Запуск при старте
* Перезапуски
* Репликация (количество запущенных процессов)
* Память
* Предварительные шаги перед запуском

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

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