Главная/Проекты/Чеклист

Проект 3. Список проверок

Проходить после реализации и до чтения SOLUTION.md.

Весь список прогнан против настоящего стека: Docker Engine 29.7.1 и Compose, 2026-08-05, 35 подтверждений из 35, включая десять запусков подряд. Скрипт прогона — verify/62-project3.sh; он собирает стек из блоков SOLUTION.md проектов 2 и 3, то есть проверяет ровно то, что напечатано в курсе. Прогон нашёл в эталонном решении шесть дефектов — они исправлены и разобраны в SOLUTION.md.

Подготовка

bash
mkdir -p secrets && head -c 32 /dev/urandom | base64 > secrets/db_password.txt
docker compose build
docker compose up -d
docker compose ps

1. Состав

bash
docker compose ps --format 'table {{.Service}}\t{{.Status}}'
text
SERVICE   STATUS
api       Up 30 seconds (healthy)
cache     Up 45 seconds (healthy)
db        Up 45 seconds (healthy)
migrate   Exited (0) 35 seconds ago
worker    Up 30 seconds (healthy)

☐ Пять сервисов
migrate в состоянии Exited (0), а не Up
☐ Остальные healthy, а не просто Up

Про migrate. Одноразовая задача не должна оставаться запущенной. Если она Up — вероятно, в конце команды стоит что-то вроде sleep infinity, чтобы «не падала». Это антипаттерн (урок 19.4): container живёт, работа не сделана, и никто не узнает.


2. Готовность

Главная проверка проекта — воспроизводимость старта:

bash
for i in $(seq 10); do
  docker compose down -v >/dev/null 2>&1
  docker compose up -d >/dev/null 2>&1
  sleep 15
  status=$(docker compose ps api --format '{{.Status}}')
  printf '%2d: %s\n' "$i" "$status"
done
text
 1: Up 12 seconds (healthy)
 2: Up 12 seconds (healthy)
...
10: Up 12 seconds (healthy)

☐ Десять запусков подряд без падения api
☐ У db и cache условие service_healthy
☐ У migrate условие service_completed_successfully

Один успешный запуск ничего не доказывает. Гонка при старте проявляется не всегда: на прогретой машине база успевает, на холодной нет. Десять запусков — минимальная проверка.

bash
docker compose config | grep -A 3 depends_on | head -20

☐ Нигде нет depends_on в форме простого списка


3. Миграции

bash
docker compose run --rm migrate
docker compose run --rm migrate
text
{"level":"INFO","message":"применяю миграцию"}
{"level":"INFO","message":"применяю миграцию"}
{"level":"INFO","message":"применяю миграцию"}
применено миграций: 3 (001_events, 002_stats_indexes, 003_processed_flag)

новых миграций нет

Схема:

bash
docker compose exec db psql -U app -d appdb -c '\d events'
text
    Column    |           Type           | Nullable |              Default
--------------+--------------------------+----------+------------------------------------
 id           | bigint                   | not null | nextval('events_id_seq'::regclass)
 path         | text                     | not null |
 method       | text                     | not null |
 status       | integer                  | not null |
 duration_ms  | double precision         | not null |
 ts           | timestamp with time zone | not null | now()
 processed_at | timestamp with time zone |          |

☐ Первый запуск применяет все миграции
☐ Второй печатает «новых миграций нет» и возвращает 0
☐ Порядок применения соответствует номерам в именах
☐ Изменение уже применённого файла приводит к отказу

Последнее проверяется так:

bash
echo "-- правка" >> migrations/002_stats_indexes.sql
docker compose run --rm migrate; echo "код: $?"
git checkout migrations/002_stats_indexes.sql
text
миграции: 002_stats_indexes: файл изменён после применения
(a1b2c3d4e5f6a7b8 → 9f8e7d6c5b4a3210); исправлять применённую миграцию нельзя
код: 1

Почему это важно. Правка применённой миграции безобидна на вашей машине, где схему можно пересоздать, и разрушительна в эксплуатации: там миграция уже применена в старом виде, и схемы разъедутся молча.


4. Сети

bash
docker compose exec api python -c "
import socket
for host, port in (('db', 5432), ('cache', 6379)):
    s = socket.create_connection((host, port), 2); s.close()
    print(f'{host}:{port} доступен изнутри')"
text
db:5432 доступен изнутри
cache:6379 доступен изнутри

Снаружи — не должны:

bash
timeout 3 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/5432' 2>/dev/null \
  && echo "ОШИБКА: база доступна с хоста" || echo "база с хоста недоступна"
curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/healthz
text
база с хоста недоступна
200

db и cache доступны изнутри backend
☐ Порты базы и кэша не опубликованы
api доступен с хоста
backend объявлена internal: true

bash
docker network inspect eventstack_backend --format '{{.Internal}}'
text
true

Про internal: true. Это не то же самое, что «не публиковать порты». Публикация решает доступ снаружи внутрь; internal запрещает выход изнутри наружу. Второе защищает от того, что скомпрометированная зависимость обратится в интернет.

Но защищает не всех. Проверено запуском: worker состоит только в backend и наружу не ходит, а api состоит и в frontend — и выход в интернет у него есть:

bash
docker compose exec worker python -c "
import socket; socket.setdefaulttimeout(4)
socket.create_connection(('1.1.1.1', 443))" 2>&1 | tail -1
docker compose exec api python -c "
import socket; socket.setdefaulttimeout(4)
socket.create_connection(('1.1.1.1', 443)); print('api: выход есть')"
text
socket.gaierror: [Errno -3] Temporary failure in name resolution
api: выход есть

internal — свойство сети, а не сервиса. Сервис, состоящий в двух сетях, получает возможности обеих. Проверять нужно каждый сервис отдельно, и api из этой проверки не исключение, а её смысл: наружу смотрит ровно один сервис, и это видно.


5. Состояние

bash
curl -s -X POST localhost:8000/events -H 'content-type: application/json' \
  -d '{"path":"/api/x","method":"GET","status":200,"duration_ms":5}' >/dev/null
curl -s localhost:8000/stats | python3 -c 'import json,sys; print("до:", json.load(sys.stdin)["total"])'

docker compose down && docker compose up -d && sleep 15
curl -s localhost:8000/stats | python3 -c 'import json,sys; print("после down/up:", json.load(sys.stdin)["total"])'

docker compose down -v && docker compose up -d && sleep 20
curl -s localhost:8000/stats | python3 -c 'import json,sys; print("после down -v:", json.load(sys.stdin)["total"])'
text
до: 1
после down/up: 1
после down -v: 0

☐ Данные переживают down
☐ Данные исчезают при down -v
☐ После down -v схема создаётся заново автоматически

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


6. Ресурсы

bash
docker compose config | python3 -c '
import sys, yaml
d = yaml.safe_load(sys.stdin)
for name, svc in d["services"].items():
    limits = (svc.get("deploy") or {}).get("resources", {}).get("limits", {})
    print(f"{name:<8} {limits or \"НЕТ ОГРАНИЧЕНИЙ\"}")'
text
api      {'cpus': '1.0', 'memory': '512M'}
cache    {'cpus': '0.5', 'memory': '256M'}
db       {'cpus': '1.0', 'memory': '1G'}
migrate  {'cpus': '0.5', 'memory': '128M'}
worker   {'cpus': '0.5', 'memory': '256M'}

☐ У всех пяти сервисов заданы лимиты
☐ Ротация логов настроена у всех
maxmemory у Redis задан и не превышает лимит container'а

Про Redis. maxmemory внутри Redis и memory в лимитах container'а — разные вещи. Если первый больше второго, Redis не начнёт вытеснять ключи, а будет убит OOM killer — без записи в лог (checkpoint 3).


7. Тесты

bash
docker compose -f compose.yaml -f compose.test.yaml run --rm tests
text
.....................................                                    [100%]

Name                   Stmts   Miss  Cover
------------------------------------------
app/secrets.py            17      4    76%
app/storage_pg.py         67      3    96%
migrations/runner.py      86     17    80%
worker/main.py            69     19    72%
worker/queue.py           41     14    66%
------------------------------------------
TOTAL                    280     57    80%
Required test coverage of 75% reached. Total coverage: 79.64%

Покрытие считается по новому коду, а не по всему app. Файлы main.py, models.py, settings.py пришли из проекта 2 вместе со своими тестами; включить их сюда — получить 40 % и провал порога на ровном месте.

☐ Тесты проходят с настоящей базой
☐ Покрытие не ниже заявленного порога
☐ Тесты используют отдельную базу, а не рабочую

Главная проверка этого раздела — что происходит без базы:

bash
EVENTAPI_DATABASE_URL= pytest -q                      # локально
EVENTAPI_DATABASE_URL= EVENTAPI_REQUIRE_DB=1 pytest -q # в конвейере
text
...ssss.ss.sssssssssssssssssssssss...                                    [100%]
33 skipped, 4 passed

ERROR tests/test_worker.py::test_stop_finishes_current_batch - Failed:
EVENTAPI_DATABASE_URL не задан: интеграционные тесты не выполнялись
(EVENTAPI_REQUIRE_DB=1)

☐ Локально отсутствие базы даёт пропуск
☐ В конвейере отсутствие базы даёт отказ

Почему нужны оба режима. Пропуск удобен при работе над кодом, не касающимся базы. В конвейере он опасен: «33 skipped» в отчёте выглядит как успех, и сборка проходит зелёной, ничего не проверив. Это то же различение трёх состояний, что в уроке 16.4.


8. Конфигурации

bash
docker compose config >/dev/null && echo "основная: ок"
docker compose -f compose.yaml -f compose.test.yaml config >/dev/null && echo "тестовая: ок"
docker compose -f compose.yaml -f compose.dev.yaml config >/dev/null && echo "разработка: ок"
text
основная: ок
тестовая: ок
разработка: ок

☐ Три конфигурации разбираются без ошибок
☐ Отладочные настройки не попадают в основную
☐ В тестовой база не использует том

bash
docker compose -f compose.yaml -f compose.test.yaml config | grep -A 3 'db:' | grep -E 'tmpfs|volumes'
text
      tmpfs:

Про fsync=off в тестовой конфигурации. Он ускоряет прогон в разы и абсолютно недопустим в эксплуатации: при отказе питания база не восстановится. Если такая настройка есть — убедитесь, что она только в тестовом файле, и напишите об этом в комментарии.


9. Секреты

bash
grep -riE 'password\s*[:=]\s*[^$/]' compose*.yaml || echo "паролей в открытом виде нет"
git check-ignore secrets/db_password.txt && echo "файл секрета исключён из репозитория"
docker compose config | grep -i password
text
паролей в открытом виде нет
файл секрета исключён из репозитория
        POSTGRES_PASSWORD_FILE: /run/secrets/db_password

☐ В compose*.yaml нет значений паролей
☐ Файл секрета в .gitignore
☐ Используется _FILE-переменная, а не значение


Сводка

ПроверкаТребованияОтметка
1Состав1
2Готовность2, 3
3Миграции4, 5
4Сети6
5Состояние7
6Ресурсы8
7Тесты9
8Конфигурации10
9Секреты11

Девять из девяти — можно открывать SOLUTION.md.

bash
docker compose down -v

Навигация

← Техническое задание
Эталонное решение →
Вернуться к проектам
Главное оглавление

Markdown на GitHub ↗