раздел 05 · шаг 7/7
Шаг 7. Деплой в Docker
Финальный шаг. Multi-stage Dockerfile для бэка и фронта, общий docker-compose.yml, smoke-тест который проверяет что прод реально работает. Локально поднимаем всё одной командой.
Что делаем
- Пишем Dockerfile для backend (python:3.12-slim + uvicorn)
- Пишем Dockerfile для frontend (multi-stage: node build, nginx-alpine serve)
- Собираем
docker-compose.ymlс двумя сервисами - Выносим SQLite в named volume
- Делаем
test/smoke.py- проверяет прод-инварианты deploy.shзапускает сборку, поднятие и smoke-тест
Промпт для Claude
Сделай Docker-инфраструктуру для todo-ai.
Структура infra/:
- infra/backend.Dockerfile
- infra/frontend.Dockerfile
- infra/nginx.conf
- docker-compose.yml (в корне проекта)
- .env.example (в корне) - с ANTHROPIC_API_KEY=
- deploy.sh (в корне) - executable bash-скрипт
- test/smoke.py - smoke-тест
infra/backend.Dockerfile:
- Base: python:3.12-slim
- Multi-stage не критичен, но --no-cache-dir обязателен
- Установить системные deps если нужны (build-essential для C-расширений)
- Скопировать requirements.txt, pip install --no-cache-dir
- Скопировать app/
- CMD: uvicorn app.main:app --host 0.0.0.0 --port 8000
- Healthcheck: curl -f http://localhost:8000/api/tasks || exit 1
infra/frontend.Dockerfile:
- Stage 1 (builder): node:20-alpine
- WORKDIR /app
- COPY package.json pnpm-lock.yaml
- RUN corepack enable && pnpm install --frozen-lockfile
- COPY . .
- ARG VITE_API_URL (по умолчанию /api - чтобы nginx проксировал)
- RUN pnpm build
- Stage 2 (runtime): nginx:alpine
- COPY --from=builder /app/dist /usr/share/nginx/html
- COPY infra/nginx.conf /etc/nginx/conf.d/default.conf
infra/nginx.conf:
- Слушать порт 80
- location / - try_files $uri $uri/ /index.html (SPA fallback)
- location /api/ - proxy_pass http://backend:8000
- gzip on для js/css/json
- кеш для статики 1 год, для index.html no-cache
docker-compose.yml:
- services: backend, frontend
- backend:
build: { context: ./backend, dockerfile: ../infra/backend.Dockerfile }
env_file: .env
volumes:
- todo-data:/data
environment:
DATABASE_URL: sqlite+aiosqlite:////data/tasks.db
restart: unless-stopped
- frontend:
build: { context: ./frontend, dockerfile: ../infra/frontend.Dockerfile }
ports:
- "8080:80"
depends_on:
- backend
restart: unless-stopped
- volumes:
todo-data:
test/smoke.py:
- Python-скрипт без зависимостей кроме stdlib + curl
- Принимает аргумент base_url (по умолчанию http://localhost:8080)
- Проверки (каждая - exit 1 при провале):
1. GET /api/tasks возвращает 200 и JSON-массив
2. POST /api/tasks с {"title":"smoke test"} возвращает 201,
в ответе есть id, title, category (любая из валидных), done=false
3. GET / возвращает 200 и Content-Type text/html
4. GET /index.html содержит <div id="root"> или подобный маркер
5. Созданная задача появляется в GET /api/tasks
6. DELETE задачи возвращает 204
7. Финал: SUCCESS или FAILED с диагностикой
- Вывод цветной (зелёный/красный), компактный
deploy.sh:
#!/bin/bash
set -e
docker compose up -d --build
echo "Waiting for services..."
sleep 5
python3 test/smoke.py http://localhost:8080 || {
echo "Smoke test FAILED, rolling back"
docker compose logs
exit 1
}
echo "Deploy successful"
Сделай deploy.sh executable: chmod +x deploy.sh
Что Claude создаст
После генерации структура такая:
todo-ai/
├── backend/
├── frontend/
├── infra/
│ ├── backend.Dockerfile
│ ├── frontend.Dockerfile
│ └── nginx.conf
├── test/
│ └── smoke.py
├── docker-compose.yml
├── deploy.sh
├── .env
├── .env.example
└── CLAUDE.md
Локальный запуск
# убедитесь что .env содержит ANTHROPIC_API_KEY
cat .env
./deploy.sh
Что произойдёт:
- Docker собирает оба образа (первый раз - 2-3 минуты)
- Стартуют контейнеры backend и frontend
- Через 5 секунд запускается smoke.py
- Если всё ок - "Deploy successful"
- Если что-то отвалилось - exit 1 + логи
Откройте http://localhost:8080 - там должен быть рабочий ToDo.
Smoke-тест: что внутри
test/smoke.py минимальный, без зависимостей:
#!/usr/bin/env python3
import json
import subprocess
import sys
GREEN = "\033[92m"
RED = "\033[91m"
RESET = "\033[0m"
base = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8080"
failures = []
def curl(method, path, body=None):
cmd = ["curl", "-s", "-X", method, f"{base}{path}"]
if body:
cmd += ["-H", "Content-Type: application/json", "-d", json.dumps(body)]
cmd += ["-w", "\n%{http_code}"]
out = subprocess.check_output(cmd, text=True)
parts = out.rsplit("\n", 1)
status = int(parts[-1])
payload = parts[0] if len(parts) > 1 else ""
return status, payload
def check(name, ok, hint=""):
if ok:
print(f"{GREEN}PASS{RESET} {name}")
else:
print(f"{RED}FAIL{RESET} {name} - {hint}")
failures.append(name)
# 1. GET /api/tasks
status, body = curl("GET", "/api/tasks")
check("GET /api/tasks 200", status == 200, f"status={status}")
tasks_data = json.loads(body) if body else None
check("GET /api/tasks returns list", isinstance(tasks_data, list))
# 2. POST /api/tasks
status, body = curl("POST", "/api/tasks", {"title": "smoke test"})
check("POST /api/tasks 201", status == 201, f"status={status}")
created = json.loads(body) if body else {}
check("POST returns id", isinstance(created.get("id"), int))
check("POST returns title", created.get("title") == "smoke test")
check("POST returns category", created.get("category") in
{"work", "personal", "learning", "health", "other"})
# 3. GET /
status, _ = curl("GET", "/")
check("GET / 200", status == 200)
# 4. DELETE
if created.get("id"):
status, _ = curl("DELETE", f"/api/tasks/{created['id']}")
check("DELETE 204", status == 204, f"status={status}")
if failures:
print(f"\n{RED}FAILED{RESET}: {len(failures)} checks")
sys.exit(1)
print(f"\n{GREEN}SUCCESS{RESET}: all checks passed")
Прокси для Anthropic
Если деплой идёт на сервер в РФ - бэк должен ходить к Anthropic через прокси. Добавьте в docker-compose.yml:
backend:
environment:
HTTPS_PROXY: http://188.166.23.110:3128
HTTP_PROXY: http://188.166.23.110:3128
NO_PROXY: localhost,frontend
(Этот прокси - моя инфра. У вас будет свой адрес.)
Деплой на сервер
Базовое правило: только через Git, никаких scp.
# локально
git push origin main
# на сервере
ssh selectel
cd /opt/todo-ai
git pull
./deploy.sh
Если smoke.py упал - откат:
git log --oneline -5 # найти предыдущий рабочий коммит
git checkout <hash>
./deploy.sh
Точка сохранения
git add .
git commit -m "step 7: Docker deploy, smoke tests, deploy.sh"
git push
Что дальше
Базовый проект готов. Дальше можно:
- Добавить аутентификацию (FastAPI-Users или собственное JWT)
- Перевести SQLite на PostgreSQL
- Добавить редактирование title и категории через UI
- Сделать фильтрацию задач по категории
- Расширить системный промпт примерами для качества категоризации
- Добавить мониторинг (Sentry, простой /health endpoint с метриками)
Каждое из этих расширений - один промпт Claude + проверка smoke.py.
Полезные ссылки
- Docker Compose - официальная документация
- Nginx как reverse proxy для SPA - паттерны для SPA + API