FastAPI

Deployment — Docker, CI/CD, Monitoring

Практичний посібник із запуску FastAPI у production-середовищі. Навчимося конфігурувати оточення через Pydantic Settings, оптимізувати Uvicorn/Gunicorn та проведемо паралелі з .NET.

Deployment — Docker, CI/CD, Monitoring

Перехід від розробки додатка на локальній машині до його запуску у реальному production-середовищі — це крок, який вимагає зміни мислення розробника. Локально ми цінуємо швидкість автоперезапуску коду (--reload), детальне відображення помилок на екрані та мінімалістичні in-memory бази даних. У production на перше місце виходять зовсім інші вимоги: надійність, безпека, масштабованість, швидкість завантаження та повний моніторинг стану системи.

Для .NET розробників концепції публікації застосунків є звичними завдяки потужним вбудованим інструментам SDK та IIS/Kestrel. В екосистемі Python процес розгортання є більш гнучким, але водночас вимагає від розробника самостійного збору «конструктора» з різних утиліт: контейнеризація, менеджери процесів, брокери та системи моніторингу.

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


1. Конфігурація середовища: Pydantic Settings та 12-Factor App

Перше правило безпечного деплою — ніколи не зберігайте паролі, секретні ключі та налаштування баз даних у коді додатку. Це порушує один із ключових принципів методології розробки хмарних додатків 12-Factor App (розділ III. Config), який стверджує: Конфігурація має зберігатися в змінних оточення (environment variables).

У Python стандартним і найбільш надійним інструментом для роботи з конфігурацією є бібліотека Pydantic Settings (про яку ми детально говорили у Статті 15).

Встановлення залежностей

Для роботи з конфігурацією встановимо необхідні бібліотеки:

pip install pydantic-settings python-dotenv

Практична реалізація конфігурації

Створимо клас конфігурації, який автоматично зчитуватиме змінні оточення, валідуватиме їхні типи та матиме локальний фолбек (fallback) у вигляді файлу .env для розробки.

config.py
from typing import Literal
from pydantic import PostgresDsn, RedisDsn, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    # Визначаємо рівень запуску додатка
    ENVIRONMENT: Literal["development", "staging", "production"] = "development"

    # Секретний ключ для шифрування JWT токенів
    SECRET_KEY: str = Field(min_length=32)

    # Валідація форматів підключення до БД та Redis через спеціальні типи Pydantic
    DATABASE_URL: PostgresDsn
    REDIS_URL: RedisDsn

    # Порт для запуску API
    PORT: int = 8000

    # Налаштування джерела зчитування
    # env_file вказує на файл, який зчитується локально, якщо системні змінні відсутні
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=True # Змінні в коді та в системі мають збігатися за регістром
    )

# Створюємо єдиний екземпляр налаштувань (Singleton)
settings = Settings()

Створимо локальний файл конфігурації .env (його обов'язково треба додати до .gitignore!):

.env
ENVIRONMENT=development
SECRET_KEY=super-secret-key-that-must-be-at-least-32-chars-long
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/taskforge
REDIS_URL=redis://localhost:6379/0
PORT=8000

Порівняльний аналіз конфігурації: .NET ↔ Python

Для розробників, які звикли до appsettings.json, підхід у Python може здатися занадто суворим через валідацію типів. Давайте порівняємо ці моделі:

Параметр порівняння.NET Configuration (IConfiguration)Python Pydantic Settings
Формат файлівJSON (appsettings.json, appsettings.Development.json)Системні env змінні або текстові .env файли
Валідація типівВідсутня за замовчуванням (зчитує все як рядки. Потрібно писати кастомні перевірки чи біндінг в Options)Вбудована та обов'язкова (якщо передати рядок у поле PORT: int, виникне помилка і додаток не запуститься)
Помилки конфігураціїВиникають під час виконання (Runtime), якщо код спробує зчитати пусте значенняВиникають на етапі запуску (Startup). Додаток впаде миттєво, якщо хоч одна змінна не пройде валідацію
Ієрархія перевизначенняЗчитує JSON → додає User Secrets → додає Environment VariablesСистемні змінні оточення мають абсолютний пріоритет над локальним .env файлом
Чому підхід Pydantic є безпечнішим? В ASP.NET Core помилка в налаштуваннях підключення до стороннього сервісу (наприклад, помилка в URL платіжного шлюзу) може бути непоміченою під час старту. Додаток успішно запуститься, а помилка "вистрілить" лише тоді, коли перший користувач спробує зробити покупку. Pydantic Settings гарантує Fail-Fast поведінку: якщо системний адміністратор забув прописати правильний DATABASE_URL у Kubernetes Deployment, контейнер просто впаде під час старту, запобігаючи деплою некоректної конфігурації.

2. Вебсервери в Production: Uvicorn та Gunicorn

Локально ми запускаємо FastAPI однією командою: uvicorn main:app --reload. Прапорець --reload запускає додатковий процес, який постійно моніторить зміни файлів на диску. Це створює велике навантаження на процесор та споживає зайву пам'ять.

Ніколи не використовуйте --reload у production! Окрім оверхеду на ресурси, автоперезапуск коду в production є критичною дірою в безпеці — якщо зловмисник зможе завантажити або змінити будь-який файл на сервері, вебсервер автоматично перезавантажиться і виконає шкідливий код.

Оптимальний запуск Uvicorn

У production ми маємо запускати Uvicorn без reload та з обмеженням кількості процесів (workers). Оскільки Python виконує код в одному потоці через GIL, щоб задіяти всі ядра процесора сервера, нам потрібно запустити кілька копій (воркерів) нашого додатка.

Формула розрахунку оптимальної кількості воркерів:

Завантаження...
N* = (2 \times N*) + 1

Запуск Uvicorn з кількома воркерами:

Запуск Uvicorn у Production
$ uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
INFO: Started parent process [45210]
INFO: Started server process [45211] (worker 1)
INFO: Started server process [45212] (worker 2)
INFO: Started server process [45213] (worker 3)
INFO: Started server process [45214] (worker 4)

Використання Gunicorn як менеджера процесів

Хоча Uvicorn має свій простий менеджер воркерів (--workers), для великих систем стандартом є запуск Uvicorn під керуванням Gunicorn (Green Unicorn).

Gunicorn — це зрілий WSGI/ASGI менеджер процесів, який:

  • Моніторить стан воркерів. Якщо один із воркерів впаде через витік пам'яті або критичну помилку (Segfault), Gunicorn миттєво та непомітно для користувачів запустить новий.
  • Дозволяє робити "Graceful reload" без зупинки сервера (за допомогою системного сигналу SIGHUP).
  • Має кращі алгоритми розподілу запитів між воркерами.
pip install gunicorn

Запуск додатка за допомогою Gunicorn та Uvicorn-воркерів:

Запуск через Gunicorn
$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
[2026-07-16 10:45:00 +0000] [45300] [INFO] Starting gunicorn 22.0.0
[2026-07-16 10:45:00 +0000] [45300] [INFO] Listening at: http://0.0.0.0:8000 (45300)
[2026-07-16 10:45:00 +0000] [45301] [INFO] Booting worker with pid: 45301

Graceful Shutdown (М'яка зупинка)

У хмарних середовищах (наприклад, Kubernetes або AWS ECS) контейнери постійно зупиняються та запускаються знову при масштабуванні чи оновленні. Коли сервер отримує команду на зупинку (SIGTERM), він має завершити її коректно:

  1. Припинити приймати нові HTTP-запити.
  2. Дати активним запитам завершити обробку (наприклад, закінчити запис у базу даних).
  3. Закрити підключення до БД та Redis.
  4. Завершити процес.

Gunicorn та Uvicorn роблять це автоматично. За замовчуванням час очікування (graceful timeout) становить 30 секунд. Якщо запити не завершаться за цей час, процес буде вбито примусово через SIGKILL.

Порівняльний аналіз хостингу: Kestrel ↔ Uvicorn

Критерій.NET KestrelPython Uvicorn / Gunicorn
АрхітектураПовністю готовий до production вебсервер. Може безпечно дивитися напряму в інтернет (edge server)ASGI-сервер. Зазвичай потребує зворотної проксі (Nginx/Traefik) перед собою для SSL та захисту від DDoS
БагатопотоковістьВбудований асинхронний Thread Pool на рівні CLR (один процес обробляє всі запити паралельно)Процесна модель ( GIL змушує запускати окремий процес (worker) на кожне ядро CPU)
Менеджер процесівНе потрібен. Процесом керує рантайм .NET та системні засоби (systemd, IIS)Потрібен додатковий менеджер (Gunicorn) для відстеження стану процесів-воркерів

3. Контейнеризація за допомогою Docker

Сьогодні стандартом розгортання вебзастосунків є контейнеризація. Docker гарантує, що додаток працюватиме в хмарі (Production) точно так само, як і на локальному комп'ютері розробника (Development), оскільки він упаковує код разом з усіма залежностями, системними бібліотеками та конфігураціями в ізольоване середовище.

У розділі 07.tools/01.docker/ ви вже вивчили базові концепції Docker. Тепер ми розберемо, як створити безпечний та оптимізований для продакшену Docker-образ для FastAPI додатку.

Вибір базового образу: Slim vs Alpine

Перше рішення при написанні Dockerfile — вибір базового образу (Base Image). В екосистемі Python є два основних варіанти мінімалістичних образів:

  1. python:3.12-alpine (на базі Alpine Linux):
    • Має надзвичайно малий розмір (близько 50 МБ).
    • Проблема з компіляцією C-extensions: Alpine використовує бібліотеку musl libc замість стандартної glibc (яка є в Debian/Ubuntu). Багато популярних Python бібліотек (наприклад, bcrypt, cryptography, psycopg2 для PostgreSQL, numpy, pandas) містять бінарний C-код. Для стандартних Linux-систем розробники випускають готові скомпільовані пакети (wheels). Але для Alpine готових wheels часто немає — Docker почне компілювати цей C-код із сирців прямо під час збірки образу. Це вимагає встановлення важких інструментів компіляції (gcc, make, g++), що сильно розтягує час збірки (з 1 хвилини до 15) та збільшує розмір фінального образу, нівелюючи всю перевагу Alpine.
  2. python:3.12-slim (на базі Debian):
    • Розмір трохи більший (близько 120 МБ).
    • Використовує стандартну glibc, тому всі готові скомпільовані колеса (wheels) встановлюються миттєво без компіляції. Це найкращий вибір для більшості Python вебдодатків у production.

Multi-stage Builds: Оптимізація та безпека

Для збірки образу ми використаємо патерн Multi-stage build (Багатоетапна збірка). Ми розділимо процес на два кроки:

  1. Stage 1 (builder): Встановлюємо важкі інструменти розробки (наприклад, менеджер залежностей poetry або uv), компілюємо необхідні бібліотеки та збираємо віртуальне середовище Python.
  2. Stage 2 (runner): Беремо чистий базовий образ slim, копіюємо туди лише готове віртуальне середовище з першого етапу та код нашого додатка. Усі інструменти збірки (компілятори, poetry файли) залишаються в першому етапі й не потрапляють у продакшн.

Це дозволяє зменшити розмір образу в 2-3 рази та прибрати зайві вразливості безпеки (наприклад, якщо у вашому контейнері немає компиляторів та утиліт розробника, зловмиснику буде значно важче закріпитися в системі при зламі).

Написання Dockerfile

Створимо оптимізований Dockerfile з урахуванням layer caching (кешування шарів) та запуском процесу від імені безпечного не-root користувача. Виберіть варіант залежно від вашого менеджера пакетів:

Dockerfile
# ==========================================
# STAGE 1: Builder (Етап збірки залежностей)
# ==========================================
FROM python:3.12-slim AS builder

# Вимикаємо створення файлів кену (.pyc) та вмикаємо буферизацію логів
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /build

# Встановлюємо інструменти компіляції, якщо вони потрібні для встановлення деяких пакетів
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Встановлюємо менеджер пакетів (наприклад, pip + wheel)
RUN pip install --no-cache-dir --upgrade pip setuptools wheel

# Копіюємо лише файли залежностей (layer caching)
# Якщо залежності не змінювалися, цей крок буде зчитано з кешу, що прискорить збірку
COPY requirements.txt .

# Встановлюємо залежності у локальну папку (віртуальне середовище)
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ==========================================
# STAGE 2: Runner (Етап запуску додатку)
# ==========================================
FROM python:3.12-slim AS runner

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# Копіюємо скомпільовані бібліотеки з builder-етапу
COPY --from=builder /install /usr/local

# Безпека: Створюємо не-привілейованого користувача системи (non-root user)
# За замовчуванням Docker запускає процеси від імені root, що є критичною вразливістю
RUN groupadd -g 10001 appuser && \
    useradd -u 10000 -g appuser -m -s /sbin/nologin appuser

# Копіюємо вихідний код додатку
COPY ./app ./app
COPY ./main.py .

# Передаємо права на папку нашому користувачу
RUN chown -R appuser:appuser /app

# Перемикаємось на безпечного користувача
USER appuser

# Відкриваємо порт
EXPOSE 8000

# Запускаємо через Gunicorn
CMD ["gunicorn", "main:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]

Також створимо файл .dockerignore в корені проекту, щоб не копіювати непотрібні локальні файли в образ (наприклад, локальну базу даних чи файли Git):

.dockerignore
.git
.github
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.env
*.db
tests/
Dockerfile
docker-compose.yml

Порівняльний аналіз: Dockerfile у .NET ↔ Python

Для розробників із .NET структура Dockerfile з розділенням на SDK (builder) та Runtime (runner) є рідною. Зіставимо їхні етапи:

.NET Core Dockerfile (C#)
# Builder етап використовує важкий SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app

# Runner етап використовує легкий Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
ENTRYPOINT ["dotnet", "MyApp.dll"]

Ключові паралелі:

  1. SDK vs Python Builder: В .NET ми використовуємо образ dotnet/sdk для відновлення пакетів (dotnet restore) та компіляції. У Python ми використовуємо образ python-slim і за потреби встановлюємо системні утиліти (build-essential) для компіляції C-залежностей.
  2. Runtime vs Python Runner: В .NET фінальний образ базується на aspnet (де є тільки CLR runtime). У Python ми копіюємо вміст папки /install (всі встановлені pip бібліотеки) у системні папки чистішого образу python-slim.
  3. Non-root user: В .NET 8 за замовчуванням налаштована змінна USER $APP_UID (безпечний користувач app з ID 1654). У Python ми маємо явно прописати RUN useradd ... та USER appuser у Dockerfile, якщо не використовуємо спеціально налаштовані сторонні образи.

4. Оркестрація локального середовища: Docker Compose

Сучасні додатки майже ніколи не працюють самі по собі. Нашому FastAPI потрібні:

  • Реляційна база даних (PostgreSQL).
  • Сховище для кешу та черг (Redis).

Для швидкого запуску та оркестрації всієї цієї інфраструктури однією командою використовують Docker Compose.

Зв'язок контейнерів та проблема готовності (Health Checks)

Одна з найпоширеніших помилок при написанні docker-compose.yml — нерозуміння різниці між станом контейнера "Running" та реальною готовністю сервісу всередині нього приймати підключення.

Помилка залежностей (Race Condition): Якщо просто написати depends_on: [db] для FastAPI, Docker Compose запустить контейнер з базою даних і відразу почне запускати контейнер з FastAPI. Проте PostgreSQL потребує близько 3-5 секунд на ініціалізацію внутрішніх файлів та запуск сокету. Як наслідок: FastAPI спробує підключитися до бази даних, отримає помилку ConnectionRefusedError і впаде з критичною помилкою на старті.

Для вирішення цієї проблеми ми маємо налаштувати Health Check (перевірку здоров'я) для бази даних, а для FastAPI вказати запуск лише після того, як база даних пройде цю перевірку успішно.

Написання docker-compose.yml

Створимо файл конфігурації для локального запуску всієї системи:

docker-compose.yml
version: '3.8'

services:
    # 1. Сервіс Бази Даних PostgreSQL
    db:
        image: postgres:16-alpine
        container_name: taskforge_postgres
        restart: always
        environment:
            POSTGRES_USER: postgres
            POSTGRES_PASSWORD: super-secure-db-password
            POSTGRES_DB: taskforge
        ports:
            - '5432:5432'
        volumes:
            - postgres_data:/var/lib/postgresql/data
        # Налаштовуємо перевірку працездатності БД
        healthcheck:
            test: ['CMD-SHELL', 'pg_isready -U postgres -d taskforge']
            interval: 5s # Як часто перевіряти
            timeout: 5s # Таймаут на відповідь
            retries: 5 # Скільки спроб до позначки "Unhealthy"

    # 2. Сервіс Redis (Кеш та Черга задач)
    redis:
        image: redis:7-alpine
        container_name: taskforge_redis
        restart: always
        ports:
            - '6379:6379'
        # Перевірка працездатності Redis
        healthcheck:
            test: ['CMD', 'redis-cli', 'ping']
            interval: 5s
            timeout: 3s
            retries: 5

    # 3. Наш FastAPI веб-додаток
    web:
        build:
            context: .
            dockerfile: Dockerfile
        container_name: taskforge_api
        restart: always
        ports:
            - '8000:8000'
        environment:
            - ENVIRONMENT=production
            - SECRET_KEY=another-super-secret-key-for-prod-32-chars
            # Зверніть увагу: ми використовуємо імена сервісів (db, redis) замість localhost!
            # Docker Compose створює внутрішню мережу, де імена сервісів працюють як DNS-імена
            - DATABASE_URL=postgresql://postgres:super-secure-db-password@db:5432/taskforge
            - REDIS_URL=redis://redis:6379/0
        # Чекаємо повної готовності залежних контейнерів
        depends_on:
            db:
                condition: service_healthy
            redis:
                condition: service_healthy

volumes:
    postgres_data: # Зберігає дані БД на хост-машині, щоб вони не зникли при видаленні контейнерів

Запустити весь проект можна однією командою у терміналі:

Запуск Docker Compose
$ docker compose up --build -d
Creating network "taskforge_default" with the default driver
Creating volume "taskforge_postgres_data" with default driver
Building web...
Creating taskforge_postgres ... done
Creating taskforge_redis ... done
Waiting for taskforge_postgres (healthy) ...
Waiting for taskforge_redis (healthy) ...
Creating taskforge_api ... done

5. CI/CD автоматизація: GitHub Actions

Для того, щоб переконатися, що наш код відповідає стандартам якості (пройшов лінтинг, перевірку типів та всі unit-тести), а також автоматично збирати свіжі Docker-образи при кожному пуші в репозиторій, ми налаштуємо конвеєр автоматизації GitHub Actions.

Створимо файл конфігурації workflow у спеціальній папці .github/workflows/:

.github/workflows/ci.yml
name: CI/CD Pipeline

# Тригери для запуску workflow
on:
    push:
        branches: ['main']
    pull_request:
        branches: ['main']

jobs:
    # ----------------------------------------------------
    # ЕТАП 1: Тестування та перевірка якості коду (Lint & Test)
    # ----------------------------------------------------
    lint-and-test:
        runs-on: ubuntu-latest

        steps:
            - name: Checkout repository code
              uses: actions/checkout@v4

            - name: Set up Python 3.12
              uses: actions/setup-python@v5
              with:
                  python-version: '3.12'
                  # Кешуємо залежності pip, щоб пришвидшити наступні збірки
                  cache: 'pip'

            - name: Install dependencies
              run: |
                  python -m pip upgrade pip
                  pip install -r requirements.txt
                  # Додаткові утиліти для тестування
                  pip install ruff mypy pytest pytest-asyncio httpx

            - name: Run Ruff Linter
              run: ruff check app/

            - name: Run Ruff Formatter check
              run: ruff format --check app/

            - name: Run Mypy Type Checker
              run: mypy app/

            - name: Run Pytest suite
              run: pytest tests/
              env:
                  # Передаємо тестові налаштування
                  SECRET_KEY: test-secret-key-placeholder-32-chars-long
                  DATABASE_URL: sqlite+aiosqlite:///./test.db
                  REDIS_URL: redis://localhost:6379/0

    # ----------------------------------------------------
    # ЕТАП 2: Збірка Docker образу (Build & Push)
    # Виконується лише якщо попередній етап пройшов успішно та гілка - main
    # ----------------------------------------------------
    build-and-publish:
        needs: lint-and-test
        runs-on: ubuntu-latest
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'

        steps:
            - name: Checkout code
              uses: actions/checkout@v4

            - name: Set up Docker Buildx
              uses: actions/setup-buildx-action@v3

            # Авторизуємось у GitHub Container Registry (GHCR)
            - name: Log in to GHCR
              uses: docker/login-action@v3
              with:
                  registry: ghcr.io
                  username: ${{ github.actor }}
                  password: ${{ secrets.GITHUB_TOKEN }}

            # Збираємо образ та відправляємо його в реєстр
            - name: Build and Push Docker image
              uses: docker/build-push-action@v5
              with:
                  context: .
                  file: Dockerfile
                  push: true
                  # Формуємо теги для нашого образу
                  tags: |
                      ghcr.io/${{ github.repository }}/api:latest
                      ghcr.io/${{ github.repository }}/api:${{ github.sha }}
                  # Кешування шарів Docker для прискорення збірок у хмарі
                  cache-from: type=gha
                  cache-to: type=gha,mode=max

6. Логування та Моніторинг у Production

Запустити додаток в Docker — це лише половина справи. Після запуску нам необхідно бачити, що відбувається всередині системи: чи є помилки, яка затримка запитів (latency) та чи не споживає додаток занадто багато пам'яті.

Структуроване логування з structlog

Стандартний модуль logging в Python за замовчуванням виводить текстові рядки. У великих системах, де логи збираються у централізовані сховища (Elasticsearch, Loki, Datadog), текстові логи важко аналізувати. Production вимагає структурованих логів у форматі JSON.

Для цього використовується бібліотека structlog. Вона перетворює кожен виклик логу на JSON-об'єкт, який легко парситься пошуковими системами.

pip install structlog

Налаштуємо structlog у FastAPI:

app/logger.py
import logging
import sys
import structlog

def setup_logging():
    # Налаштовуємо стандартне логування для сумісності з іншими бібліотеками
    logging.basicConfig(
        format="%(message)s",
        stream=sys.stdout,
        level=logging.INFO,
    )

    structlog.configure(
        processors=[
            # Додає інформацію про рівень логування (info, error)
            structlog.processors.add_log_level,
            # Додає мітку часу
            structlog.processors.TimeStamper(fmt="iso"),
            # Форматує вивід як JSON рядок
            structlog.processors.JSONRenderer()
        ],
        # Направляємо лог у стандартний вивід
        logger_factory=structlog.stdlib.LoggerFactory(),
        wrapper_class=structlog.stdlib.BoundLogger,
        cache_logger_on_first_use=True,
    )

logger = structlog.get_logger()

Якщо викликати такий логгер: logger.info("user_login", user_id=123, ip="127.0.0.1"), у консолі з'явиться структурована інформація:

{
    "event": "user_login",
    "user_id": 123,
    "ip": "127.0.0.1",
    "level": "info",
    "timestamp": "2026-07-16T10:45:00.123456Z"
}

Ендпоінти перевірки здоров'я: Liveness та Readiness

Для оркестраторів на кшталт Kubernetes критично знати стан контейнера. Існує два типи перевірок:

  • Liveness Probe (чи живий додаток?): Перевіряє, чи не завис Event Loop. Якщо ендпоінт не відповідає, Kubernetes перезапустить контейнер.
  • Readiness Probe (чи готовий приймати трафік?): Перевіряє, чи є зв'язок з базою даних та Redis. Якщо зв'язку немає, Kubernetes тимчасово відключить контейнер від балансувальника трафіку, щоб користувачі не отримували помилки 500.

Реалізуємо ці перевірки у FastAPI:

app/main.py
from fastapi import FastAPI, status, Response
from app.database import database_check_connection # імітація перевірки з БД
from app.redis import redis_check_connection # імітація перевірки з Redis
from app.logger import logger

app = FastAPI()

# Liveness: Проста швидка відповідь
@app.get("/healthz", status_code=status.HTTP_200_OK)
async def liveness():
    return {"status": "alive"}

# Readiness: Глибока перевірка залежностей
@app.get("/readyz")
async def readiness(response: Response):
    db_healthy = await database_check_connection()
    redis_healthy = await redis_check_connection()

    if not db_healthy or not redis_healthy:
        logger.error("readiness_check_failed", database=db_healthy, redis=redis_healthy)
        response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
        return {"status": "unready", "database": db_healthy, "redis": redis_healthy}

    return {"status": "ready"}

Порівняльний аналіз логування: .NET ↔ Python

Параметр порівняння.NET Serilog / OpenTelemetryPython structlog / OpenTelemetry
Логування в JSONЗручно конфігурується через WriteTo.Console(new RenderedCompactJsonFormatter())Налаштовується через процесори structlog.processors.JSONRenderer
Властивості (Enrichers)Додавання контексту через LogContext.PushProperty("UserId", 123)Додавання контексту через logger.bind(user_id=123)
Експорт метрикВбудований інструментарій System.Diagnostics.MetricsПотребує бібліотек prometheus-client або opentelemetry-sdk

7. Практичний проєкт від А до Я: Контейнеризація та Деплой "StatusMonitor"

Щоб закріпити здобуті знання на практиці, ми побудуємо з нуля повністю працездатний, задокерований мікросервіс моніторингу статусів StatusMonitor. Цей застосунок зчитуватиме налаштування з оточення, логуватиме події в JSON, перевірятиме з'єднання з базою даних та Redis через /readyz та матиме налаштований пайплайн для автоматичного тестування.

Створіть нову папку проекту status-monitor/ та розмістіть у ній файли згідно з такою структурою:

Структура файлів
status-monitor/
├── .github/
│   └── workflows/
│       └── ci.yml
├── app/
│   ├── __init__.py
│   ├── config.py
│   ├── database.py
│   ├── logger.py
│   └── main.py
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
└── requirements.txt

Крок 1: Залежності проекту

Створимо файл requirements.txt з усіма необхідними бібліотеками для нашої продакшн-конфігурації:

requirements.txt
fastapi>=0.110.0
uvicorn[standard]>=0.28.0
gunicorn>=22.0.0
pydantic-settings>=2.2.0
python-dotenv>=1.0.1
structlog>=24.1.0
sqlalchemy[asyncio]>=2.0.28
asyncpg>=0.29.0
redis>=5.0.3
prometheus-fastapi-instrumentator>=7.0.0
pytest>=8.1.1
pytest-asyncio>=0.23.5
httpx>=0.27.0

Крок 2: Конфігурація оточення (app/config.py)

Опишемо клас налаштувань, який зчитуватиме змінні системи та валідуватиме їх за допомогою Pydantic:

app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import PostgresDsn, RedisDsn

class Settings(BaseSettings):
    ENVIRONMENT: str = "production"
    SECRET_KEY: str
    DATABASE_URL: PostgresDsn
    REDIS_URL: RedisDsn
    PORT: int = 8000

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8"
    )

settings = Settings()

Крок 3: Логування у форматі JSON (app/logger.py)

Налаштуємо логер structlog для виводу повідомлень у структурованому форматі, придатному для систем збору логів:

app/logger.py
import logging
import sys
import structlog

def setup_logging():
    logging.basicConfig(
        format="%(message)s",
        stream=sys.stdout,
        level=logging.INFO,
    )
    structlog.configure(
        processors=[
            structlog.processors.add_log_level,
            structlog.processors.TimeStamper(fmt="iso"),
            structlog.processors.JSONRenderer()
        ],
        logger_factory=structlog.stdlib.LoggerFactory(),
        wrapper_class=structlog.stdlib.BoundLogger,
        cache_logger_on_first_use=True,
    )

logger = structlog.get_logger()

Крок 4: Підключення до БД та перевірка здоров'я (app/database.py)

Створимо асинхронний рушій SQLAlchemy та функцію для перевірки реального зв'язку з базою даних:

app/database.py
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from app.config import settings
from app.logger import logger

engine = create_async_engine(str(settings.DATABASE_URL), echo=False)

async def check_db_connection() -> bool:
    try:
        async with engine.connect() as conn:
            await conn.execute(text("SELECT 1"))
        return True
    except Exception as e:
        logger.error("database_ping_failed", error=str(e))
        return False

Крок 5: Вебдодаток та перевірки здоров'я (app/main.py)

Зберемо наш додаток FastAPI, підключимо структуроване логування, експорт метрик для Prometheus та опишемо ендпоінти перевірок /healthz та /readyz:

app/main.py
import redis.asyncio as aioredis
from fastapi import FastAPI, Response, status
from prometheus_fastapi_instrumentator import Instrumentator

from app.config import settings
from app.logger import setup_logging, logger
from app.database import check_db_connection

# Ініціалізуємо структуровані логи
setup_logging()

app = FastAPI(title="StatusMonitor API")

# Підключаємо Prometheus метрики
Instrumentator().instrument(app).expose(app)

@app.on_event("startup")
async def startup_event():
    logger.info("application_starting", environment=settings.ENVIRONMENT)

@app.get("/healthz", status_code=status.HTTP_200_OK)
async def liveness():
    # Швидка перевірка, чи не завис Event Loop
    return {"status": "healthy"}

@app.get("/readyz")
async def readiness(response: Response):
    # Глибока перевірка зв'язку з БД
    db_ok = await check_db_connection()

    # Глибока перевірка зв'язку з Redis
    redis_ok = False
    try:
        r = aioredis.from_url(str(settings.REDIS_URL))
        redis_ok = await r.ping()
    except Exception as e:
        logger.error("redis_ping_failed", error=str(e))

    if not db_ok or not redis_ok:
        logger.warn("readiness_check_failed", database=db_ok, redis=redis_ok)
        response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
        return {"status": "unready", "database": db_ok, "redis": redis_ok}

    return {"status": "ready", "database": db_ok, "redis": redis_ok}

@app.get("/")
async def root():
    return {"message": "Welcome to StatusMonitor API"}

Крок 6: Контейнеризація (Dockerfile та .dockerignore)

Створимо оптимізований multi-stage Dockerfile (ми використаємо варіант з pip для демонстрації, оскільки залежності описані в requirements.txt):

Dockerfile
# STAGE 1: Builder
FROM python:3.12-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# STAGE 2: Runner
FROM python:3.12-slim AS runner
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local

# Створюємо безпечного non-root користувача
RUN groupadd -g 10001 appuser && \
    useradd -u 10000 -g appuser -m -s /sbin/nologin appuser
COPY ./app ./app
COPY ./main.py .
RUN chown -R appuser:appuser /app
USER appuser

EXPOSE 8000
CMD ["gunicorn", "main:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]

Створимо файл .dockerignore, щоб не копіювати непотрібне сміття в образ:

.dockerignore
.git
.github
.venv
__pycache__
*.db
.env
Dockerfile
docker-compose.yml

Крок 7: Локальна оркестрація (docker-compose.yml)

Створимо файл для запуску нашого API разом з PostgreSQL та Redis, налаштувавши взаємозв'язок за допомогою Health Checks:

docker-compose.yml
version: '3.8'

services:
    db:
        image: postgres:16-alpine
        container_name: monitor_db
        environment:
            POSTGRES_USER: postgres
            POSTGRES_PASSWORD: monitorpassword
            POSTGRES_DB: monitordb
        ports:
            - '5432:5432'
        volumes:
            - monitor_db_data:/var/lib/postgresql/data
        healthcheck:
            test: ['CMD-SHELL', 'pg_isready -U postgres -d monitordb']
            interval: 5s
            timeout: 5s
            retries: 5

    redis:
        image: redis:7-alpine
        container_name: monitor_redis
        ports:
            - '6379:6379'
        healthcheck:
            test: ['CMD', 'redis-cli', 'ping']
            interval: 5s
            timeout: 3s
            retries: 5

    web:
        build: .
        container_name: monitor_api
        ports:
            - '8000:8000'
        environment:
            - ENVIRONMENT=production
            - SECRET_KEY=super-secret-status-monitor-key-32-chars
            - DATABASE_URL=postgresql+asyncpg://postgres:monitorpassword@db:5432/monitordb
            - REDIS_URL=redis://redis:6379/0
        depends_on:
            db:
                condition: service_healthy
            redis:
                condition: service_healthy

volumes:
    monitor_db_data:

Крок 8: Автоматизація перевірок (.github/workflows/ci.yml)

Створимо GitHub Actions файл для автоматичного лінтингу та тестування:

.github/workflows/ci.yml
name: Code Quality CI

on:
    push:
        branches: ['main']
    pull_request:
        branches: ['main']

jobs:
    validate:
        runs-on: ubuntu-latest
        steps:
            - name: Checkout code
              uses: actions/checkout@v4

            - name: Set up Python
              uses: actions/setup-python@v5
              with:
                  python-version: '3.12'
                  cache: 'pip'

            - name: Install dependencies
              run: |
                  python -m pip install --upgrade pip
                  pip install -r requirements.txt
                  pip install ruff mypy

            - name: Ruff Lint
              run: ruff check app/

            - name: Mypy Type Check
              run: mypy app/

Як запустити та протестувати проєкт:

  1. Запустіть контейнери: docker compose up --build -d.
  2. Перевірте статус контейнерів: docker compose ps. Ви повинні побачити, що всі сервіси перебувають у статусі healthy.
  3. Зробіть тестовий запит до Liveness Probe:
    curl http://localhost:8000/healthz
    # Відповідь: {"status":"healthy"}
    
  4. Зробіть запит до Readiness Probe, що перевіряє реальні з'єднання:
    curl http://localhost:8000/readyz
    # Відповідь: {"status":"ready","database":true,"redis":true}
    
  5. Перегляньте Prometheus метрики, які генеруються автоматично: curl http://localhost:8000/metrics.

8. Практичні завдання (Practice)

Рівень 1: Контейнеризація додатка за допомогою Docker Compose

  1. Створіть простенький FastAPI додаток.
  2. Напишіть для нього Dockerfile на базі python:3.12-slim.
  3. Напишіть docker-compose.yml, який запускає цей FastAPI додаток разом із базою даних PostgreSQL.
  4. Налаштуйте healthcheck для БД так, щоб FastAPI запускався лише після того, як база даних буде повністю готова до прийому з'єднань.

Рівень 2: Налаштування CI/CD конвеєра в GitHub Actions

  1. Створіть новий публічний репозиторій на GitHub та завантажте туди код вашого FastAPI додатка.
  2. Створіть файл .github/workflows/ci.yml.
  3. Налаштуйте автоматичний запуск Ruff для лінтингу та перевірки форматування коду при кожному пуші в гілку main або створенні Pull Request.
  4. Додайте крок запуску тестів через Pytest.

Рівень 3: Додавання Prometheus метрик до API

  1. Встановіть бібліотеку prometheus-fastapi-instrumentator.
  2. Інтегруйте її у ваш FastAPI додаток.
  3. Перевірте, що за адресою /metrics відображаються системні метрики додатка (кількість запитів, час відповіді).
  4. Запустіть поруч у Docker Compose контейнер Prometheus, налаштуйте збір метрик з вашого API та візуалізуйте їх у Grafana.

Домашнє завдання (TaskForge Practice)

Для успішного завершення цієї теми виконайте наступні зміни у вашому проекті TaskForge:

  1. Створення multi-stage Dockerfile:
    • Напишіть оптимізований Dockerfile з двома етапами (builder та runner).
    • Налаштуйте запуск додатка від імені не-root користувача (appuser).
  2. Створення docker-compose.yml:
    • Налаштуйте запуск FastAPI додатка, бази даних PostgreSQL та Redis.
    • Забезпечте збереження даних PostgreSQL на диск хост-машини через volumes.
    • Пропишіть коректні healthcheck перевірки.
  3. Налаштування Pydantic Settings:
    • Реалізуйте зчитування конфігурації підключень до БД та Redis через змінні оточення.
  4. Додавання health check:
    • Створіть ендпоінти /healthz (liveness) та /readyz (readiness) з логуванням стану.
  5. CI/CD конвеєр:
    • Налаштуйте GitHub Actions workflow для автоматичної перевірки коду (ruff + mypy + pytest).

Git Commit: feat: add Docker, docker-compose, CI/CD and health checks

🎉 Вітаємо! Ви пройшли повний шлях розробки та розгортання API. Тепер ваш проект TaskForge є повністю готовим, стійким та масштабованим production-ready застосунком на базі FastAPI!

Copyright © 2026