Контейнеризация, docker-compose, production deployment
Контейнеризация решает проблему «на моей машине работает» и делает деплой предсказуемым. В этой теме вы научитесь упаковывать FastAPI в контейнер, запускать с зависимостями и деплоить в production.
Docker — платформа для запуска приложений в изолированных контейнерах.
Ваша машина Production-сервер
┌──────────────┐ ┌──────────────┐
│ Docker │ │ Docker │
│ ┌────────┐ │ │ ┌────────┐ │
│ │ Контей │ │ │ │ Контей │ │
│ │ нер │ │ │ │ нер │ │
│ └────────┘ │ │ └────────┘ │
└──────────────┘ └──────────────┘
Одинаковое поведение в обоих местах
Зачем:
Всегда начинайте с .dockerignore — как .gitignore, но для Docker. Исключает ненужные файлы из контекста сборки (ускоряет билд, уменьшает размер образа).
# Виртуальное окружение
.venv
.env
# Git
.git
.gitignore
# Python-кэш
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
# Тесты и документация
tests/
*.md
docs/
# IDE
.vscode/
.idea/
*.swp
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# CI/CD
.github/
.gitlab-ci.yml
# coverage
.coverage
htmlcov/
.pytest_cache/Почему это важно:
.dockerignore Docker копирует всё, включая .git (может быть 500+ МБ).env с секретами не должен попадать в образFROM python:3.12-slim
# Рабочая директория
WORKDIR /app
# Переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Установка зависимостей
COPY pyproject.toml poetry.lock ./
RUN pip install --no-cache-dir poetry && \
poetry config virtualenvs.create false && \
poetry install --only main --no-interaction
# Копирование кода приложения
COPY . .
# Непривилегированный пользователь (безопасность)
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
# Порт
EXPOSE 8000
# Запуск
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Разбор ключевых строк:
python:3.12-slim — минимальный образ Python (меньше attack surface)PYTHONDONTWRITEBYTECODE=1 — не создавать .pyc файлыPYTHONUNBUFFERED=1 — логи выводятся сразу (без буферизации)EXPOSE 8000 — документация: порт, на котором работает приложениеUSER appuser — запуск не от root (безопасность)Multi-stage сборка уменьшает размер образа: зависимости собираются в одном контейнере, в финальный образ попадают только готовые пакеты.
# Stage 1: Сборка зависимостей
FROM python:3.12-slim as builder
WORKDIR /app
ENV PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# Stage 2: Минимальный runtime
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Копируем готовые wheel-пакеты из builder
COPY /wheels /wheels
RUN pip install --no-cache /wheels/*
# Непривилегированный пользователь
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
# Копируем код (пользователь appuser владеет файлами)
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Преимущества multi-stage:
Важно: в современных версиях Docker используется
docker compose(пробел, без дефиса) — это плагин, а не отдельная утилита. Ключversionбольше не нужен.
services:
api:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
env_file:
- .env
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
command: uvicorn main:app --reload --host 0.0.0.0 --port 8000
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:Ключевые моменты для разработки:
volumes: - .:/app — код монтируется в контейнер, изменение файлов сразу видно без пересборки--reload — uvicorn перезапускается при изменении кодаports — прокидываем порты, чтобы можно было подключиться с хост-машиныenv_file — загружаем переменные из .env файлаservices:
api:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
env_file:
- .env
environment:
- ENVIRONMENT=production
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
db:
image: postgres:16-alpine
restart: unless-stopped
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- app-network
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- api
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridgeКлючевые моменты для production:
restart: unless-stopped — автоматический перезапуск при падении (кроме ручной остановки)healthcheck — Docker проверяет здоровье сервисовcondition: service_healthy — API стартует только после того, как БД готоваnetworks — изолированная сеть между сервисами (нет доступа извне):ro — nginx конфиг только для чтения (безопасность)ports у БД и Redis — они доступны только внутри Docker сетиevents {
worker_connections 1024;
}
http {
upstream api {
server api:8000;
}
# HTTP → HTTPS redirect
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Размер запроса
client_max_body_size 10M;
# Таймауты
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
location / {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Статика
location /static {
alias /app/static;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
}Что здесь происходит:
api:8000 (по имени сервиса в Docker сети)# Приложение
SECRET_KEY=your-secret-key-change-in-production
ENVIRONMENT=production
DEBUG=false
# База данных
POSTGRES_USER=user
POSTGRES_PASSWORD=secure-password
POSTGRES_DB=mydb
DATABASE_URL=postgresql://user:secure-password@db:5432/mydb
# Redis
REDIS_URL=redis://redis:6379
# Email
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=user
SMTP_PASSWORD=passwordSECRET_KEY=dev-secret-key-not-for-production
ENVIRONMENT=development
DEBUG=true
POSTGRES_USER=user
POSTGRES_PASSWORD=pass
POSTGRES_DB=mydb
DATABASE_URL=postgresql://user:pass@db:5432/mydb
REDIS_URL=redis://redis:6379from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
secret_key: str
environment: str = "production"
debug: bool = False
database_url: str
redis_url: str
smtp_host: str
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
@lru_cache()
def get_settings() -> Settings:
return Settings()Примечание: в Pydantic v2 используется
model_configвместо вложенного классаConfig.
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false && \
poetry install --only main --no-interaction
COPY . .
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
# Миграции перед запуском приложения
CMD alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000Минусы: миграции запускаются при каждом старте, сложнее контролировать.
services:
migrate:
build: .
env_file:
- .env
command: alembic upgrade head
depends_on:
db:
condition: service_healthy
restart: "no" # Выполняется один раз и завершается
api:
build: .
env_file:
- .env
depends_on:
migrate:
condition: service_completed_successfully # API ждёт завершения миграцийПреимущества:
from fastapi import FastAPI
from pydantic import BaseModel
import psycopg2
import redis
app = FastAPI()
class HealthResponse(BaseModel):
status: str
database: str
redis: str
@app.get("/health", response_model=HealthResponse)
def health_check():
db_status = "healthy"
redis_status = "healthy"
# Проверка БД
try:
conn = psycopg2.connect(DATABASE_URL)
conn.close()
except Exception as e:
db_status = f"unhealthy: {str(e)}"
# Проверка Redis
try:
r = redis.Redis.from_url(REDIS_URL)
r.ping()
except Exception as e:
redis_status = f"unhealthy: {str(e)}"
overall = "healthy" if db_status == "healthy" and redis_status == "healthy" else "unhealthy"
return HealthResponse(
status=overall,
database=db_status,
redis=redis_status
)Как Docker использует healthcheck:
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s # Проверка каждые 30 секунд
timeout: 10s # Таймаут на выполнение
retries: 3 # Сколько неудачных попыток до 'unhealthy'
start_period: 10s # Время на старт (не считает failed проверки)services:
api:
build: .
logging:
driver: "json-file"
options:
max-size: "10m" # Максимальный размер файла
max-file: "3" # Количество файлов для ротацииimport logging
import json
class JSONFormatter(logging.Formatter):
def format(self, record):
log_record = {
"level": record.levelname,
"message": record.getMessage(),
"timestamp": self.formatTime(record),
"module": record.module,
}
if record.exc_info:
log_record["exc_info"] = self.formatException(record.exc_info)
return json.dumps(log_record, ensure_ascii=False)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])Зачем JSON-логи:
# Установка Docker (скрипт устанавливает и Docker, и Compose плагин)
curl -fsSL https://get.docker.com | sh
# Добавить пользователя в группу docker (чтобы не использовать sudo)
sudo usermod -aG docker $USER
# Перелогиниться или:
newgrp docker
# Проверить установку
docker --version
docker compose version
# Клонирование проекта
git clone <repository> /opt/myapp
cd /opt/myapp
# Создать .env из шаблона
cp .env.example .env
nano .env # Заполнить реальные значения# Первый запуск (сборка и запуск)
docker compose up -d --build
# Просмотр логов
docker compose logs -f
# Просмотр логов конкретного сервиса
docker compose logs -f api
# Статус сервисов
docker compose ps
# Остановка (сохраняя данные)
docker compose down
# Остановка с удалением volumes (осторожно!)
docker compose down -v| Команда | Описание |
|---|---|
docker compose up -d | Запуск всех сервисов в фоне |
docker compose up -d --build | Пересборка и запуск |
docker compose down | Остановка сервисов |
docker compose logs -f | Просмотр логов в реальном времени |
docker compose ps | Статус контейнеров |
docker compose exec api bash | Войти в контейнер api |
docker compose restart api | Перезапустить конкретный сервис |
stages:
- test
- build
- deploy
test:
stage: test
image: python:3.12-slim
script:
- pip install poetry
- poetry install
- poetry run pytest --tb=short
- poetry run ruff check .
deploy:
stage: deploy
image: docker:latest
script:
- docker compose build
- docker compose push
- ssh user@server "cd /opt/myapp && git pull && docker compose up -d --build"
only:
- main# На сервере в /opt/myapp:
# Обновить код и перезапустить
cd /opt/myapp && \
git pull && \
docker compose up -d --build && \
docker compose logs -f --tail=50services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
ports:
- "9090:9090"
networks:
- monitoring
grafana:
image: grafana/grafana
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- grafana_data:/var/lib/grafana
networks:
- monitoring
volumes:
grafana_data:
networks:
monitoring:
driver: bridge# ❌ Плохо: контейнер работает от root
CMD uvicorn main:app --host 0.0.0.0
# ✅ Хорошо: непривилегированный пользователь
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
CMD uvicorn main:app --host 0.0.0.0Без .dockerignore Docker передаёт всю директорию демону:
.git может весить 500+ МБ — медленная передача.env с паролями попадёт в образ.venv увеличит размерРешение: всегда создавайте .dockerignore перед Dockerfile.
# ❌ Плохо: COPY . . до установки зависимостей
# При любом изменении кода зависимости переустанавливаются
COPY . .
RUN pip install -r requirements.txt
# ✅ Хорошо: зависимости устанавливаются до копирования кода
COPY pyproject.toml poetry.lock ./
RUN poetry install --only main --no-interaction
COPY . .Как работает кэш Docker:
RUN, COPY, ADD создаёт слойdocker-compose вместо docker compose# ❌ Устаревший синтаксис (Compose V1, больше не поддерживается)
docker-compose up -d
# ✅ Современный синтаксис (Compose V2, плагин)
docker compose up -dversion в compose файле# ❌ Устарело (version не нужен с Compose V2)
version: '3.8'
services:
api:
build: .
# ✅ Современный синтаксис
services:
api:
build: ..dockerignore созданUSER appuser)max-size, max-file).env (не захардкожены)restart: unless-stopped для автомата.dockerignore — обязательный файл для быстрого и безопасного билдаdocker compose — запуск с зависимостями, разработка vs production.env, Pydantic SettingsВ следующей теме вы изучите CI/CD и мониторинг — автоматизация тестов, линтеры, Prometheus, Grafana, централизованное логирование.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.