# 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 form інструкції `CMD`, як пояснено нижче.

///

#### Використовуйте `CMD` - exec form { #use-cmd-exec-form }

Інструкцію Docker [`CMD`](https://docs.docker.com/reference/dockerfile/#cmd) можна записати у двох формах:

✅ Exec form:

```Dockerfile
# ✅ Робіть так
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
```

⛔️ Shell form:

```Dockerfile
# ⛔️ Не робіть так
CMD fastapi run app/main.py --port 80
```

Обов’язково завжди використовуйте exec form, щоб FastAPI міг коректно завершувати роботу та щоб були викликані [події тривалості життя](../advanced/events.md).

Докладніше про це можна прочитати в [документації Docker про shell та exec form](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
```

#### За представником з термінацією TLS { #behind-a-tls-termination-proxy }

Якщо ви запускаєте контейнер за представником з термінацією TLS (балансувальником навантаження), наприклад Nginx або Traefik, додайте опцію `--proxy-headers`. Це скаже Uvicorn (через CLI FastAPI) довіряти заголовкам, що надсилаються цим представником, які вказують, що застосунок працює за 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 та подібні інструменти також використовують внутрішній кеш під час збірки образу. Якщо файл не змінювався з моменту останньої збірки, тоді він повторно використає той самий шар, створений востанє, замість копіювання файлу знову та створення нового шару з нуля.

Просте уникнення копіювання файлів не обов’язково суттєво покращує ситуацію, але оскільки для цього кроку використано кеш, він може використати кеш і для наступного кроку. Наприклад, він може використати кеш для інструкції, яка встановлює залежності:

```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 }

Ви маєте змогу перевірити це за URL вашого 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) (або еквівалент, використовуючи ваш 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) (або еквівалент, використовуючи ваш 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) (або еквівалент, використовуючи ваш 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 }

При використанні контейнерів зазвичай є якийсь компонент, що слухає на головному порту. Це може бути інший контейнер, який також є представником з термінацією TLS для обробки HTTPS, або подібний інструмент.

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

/// tip | Порада

Той самий компонент представника з термінацією TLS, що використовується для HTTPS, швидше за все, також буде балансувальником навантаження.

///

І під час роботи з контейнерами та сама система, яку ви використовуєте для їх запуску і керування ними, вже матиме внутрішні інструменти для передавання мережевої комунікації (наприклад, HTTP-запитів) від цього балансувальника навантаження (який також може бути представником з термінацією TLS) до контейнерів із вашим застосунком.

### Один балансувальник навантаження - кілька контейнерів-працівників { #one-load-balancer-multiple-worker-containers }

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

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

А розподілена система контейнерів із балансувальником навантаження розподілятиме запити між кожним із контейнерів із вашим застосунком по черзі. Тож кожен запит може оброблятися одним із кількох реплікованих контейнерів, що запускають ваш застосунок.

І зазвичай цей балансувальник навантаження зможе обробляти запити, які йдуть до інших застосунків у вашому кластері (наприклад, до іншого домену або під іншим префіксом шляху 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). Але зараз він застарілий. ⛔️

Ймовірно, вам не слід використовувати цей базовий образ 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
* Автозапуск
* Перезапуски
* Реплікація (кількість запущених процесів)
* Пам’ять
* Попередні кроки перед запуском

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

Дотримуючись порядку інструкцій у `Dockerfile` і використовуючи кеш Docker, ви можете мінімізувати час збірки, щоб максимізувати свою продуктивність (і уникнути нудьги). 😎
