---
type: infra-requirements
work_item_id: ET-015
title: "Инфраструктурные требования — ET-015: Healthcheck enduro-trails-app через python urllib"
version: 1
status: approved
created_at: 2026-06-05
authors:
  - "agent:architect"
---

# Инфраструктурные требования — ET-015

## 1. Резюме

ET-015 — **infrastructure-only bug-fix** одной YAML-секции в
`docker-compose.yml`. Меняется:

- `docker-compose.yml` — секция `healthcheck` сервиса `app`
  (-1 строка `test`, +5 строк новый массив + `start_period`).
- `CHANGELOG.md` — +1 строка в `Unreleased`.
- `docs/work-items/ET-015/06-adr/ADR-020-...md` — новый ADR.
- `docs/architecture/adr/README.md` — +1 строка в глобальном индексе.

Инфраструктура **почти не меняется**:

- 0 новых docker-сервисов.
- 0 изменений в `Dockerfile` (образ `python:3.12-slim` остаётся as-is).
- 0 новых пакетов в образе (никаких `apt-get install curl/wget`).
- 0 новых файлов БД, миграций, индексов.
- 0 новых cron-записей.
- 0 новых env-переменных, секретов, API-ключей.
- 0 новых исходящих HTTPS-соединений (healthcheck — на loopback контейнера).
- 0 новых портов.
- 0 изменений в nginx (на хосте).
- 0 изменений в backend (`src/api/*`).
- 0 изменений во фронтенде (`src/web/*`).
- 0 изменений в стилях, конфигах, скриптах деплоя.

**Меняется только**:

- Команда, которую Docker запускает для healthcheck'а контейнера `app`.
- Конфигурация healthcheck'а: добавляется `start_period: 20s`.

Эскалация: **minor change** (см. ADR-020 §«Классификация изменения»).

## 2. Контейнеры и сервисы

### 2.1 Сводная таблица

| Аспект | Требование |
|--------|------------|
| Новый сервис | **Нет** |
| Изменения `Dockerfile` | **Нет** (образ `python:3.12-slim` без новых пакетов) |
| Изменения `docker-compose.yml` — `app` | **Да**: секция `healthcheck.test` + `start_period: 20s` |
| Изменения `docker-compose.yml` — `gps-collector` | **Нет** (BRD §7 out of scope) |
| Изменения `docker-compose.yml` — networks/volumes/profiles | **Нет** |
| Перезапуск `app` после деплоя | Нужен — `docker compose up -d app` (пересоздание контейнера, ~5 сек простоя HTTP) |
| Ребилд образа `app` (`docker compose build`) | **Не нужен** (TRZ R-2, AC-04, IT-04). Допускается, но не обязателен |
| Перезапуск `gps-collector` | Не нужен (не затронут, batch profile) |
| Очистка серверных кэшей | Не требуется |
| Очистка клиентских кэшей | Не требуется (фронтенд не меняется) |

### 2.2 Зависимости между сервисами

Без изменений vs PH-1..PH-8:

- `app` (uvicorn :5556 внутри контейнера) — отдаёт `/api/health`,
  `/api/route/*`, `/api/gps-tracks/*`, `/terrain/*`, статику `/enduro/*`.
- `nginx` (хост mva154) → `app:5556` через docker bridge.
- `gps-collector` (profile `batch`) → пишет в `data/gps_tracks.sqlite`,
  не имеет открытого порта, не задействован в healthcheck.

Healthcheck живёт **внутри network namespace** контейнера `app` и
обращается к `http://localhost:5556/api/health` — это loopback самого
контейнера, **не** хост и **не** другой контейнер. Не зависит от
nginx, iptables хоста, `OSRM_URL` или `gps-collector`.

### 2.3 Образ `app`

| Параметр | До ET-015 | После ET-015 |
|----------|-----------|--------------|
| Базовый образ | `python:3.12-slim` | `python:3.12-slim` |
| Размер | ~250 МБ (приблизительно) | ~250 МБ (тот же) |
| Пакеты `apt` | базовый набор slim + pip-зависимости | без изменений |
| Python | 3.12 (alias `python` → `python3`) | 3.12 (без изменений) |
| `urllib.request`, `sys` | stdlib (входят в Python) | stdlib (входят в Python) |
| `curl` | **отсутствует** (источник бага) | **отсутствует** (не нужен) |
| `wget` | отсутствует | отсутствует |
| Слои Docker | без изменений | без изменений |

### 2.4 `gps-collector` — почему без healthcheck'а

| Причина | Источник |
|---------|----------|
| `profiles: ["batch"]` — не стартует при `docker compose up -d` | `docker-compose.yml:30` |
| `restart: "no"` — контейнер не должен подниматься обратно | `docker-compose.yml:40` |
| Нет открытого порта (нет `ports:` секции) | `docker-compose.yml:28-40` |
| Команда `python -m scripts.gps_collect` отрабатывает и завершается | ADR-007 (ET-008) |
| Healthcheck для batch-задачи бессмыслен (это not a daemon) | BRD §7 (ET-015) |

Если профиль когда-нибудь сменится на long-running daemon — нужен
отдельный work-item (см. TD-2 в ADR-020).

## 3. Сеть

| Аспект | Требование |
|--------|------------|
| Новые входящие порты | **Нет** |
| Изменения nginx (хост) | **Нет** |
| Новые исходящие соединения | **Нет** (healthcheck — loopback внутри контейнера) |
| CORS | Без изменений |
| HTTPS / TLS | Без изменений |
| Docker bridge / networks | Без изменений |
| iptables на хосте | Без изменений |
| Firewall / security groups | Без изменений |

### 3.1 Healthcheck network path

```
[docker exec health probe]
   │
   ▼
python process (in container)
   │
   ▼ urllib.request.urlopen("http://localhost:5556/api/health", timeout=3)
   │
   ▼ TCP connect → 127.0.0.1:5556 (loopback в network namespace контейнера)
   │
   ▼
uvicorn (тот же процесс, который запущен `CMD ["uvicorn", "src.api.main:app", ...]`)
   │
   ▼ FastAPI router → @app.get("/api/health") → src/api/main.py:1224
   │
   ▼ HTTP 200 + JSON {"status": "ok", ...}
   │
   ▼
python sys.exit(0)
   │
   ▼
Docker: exit code 0 → State.Health.Status = healthy
```

Никаких внешних сетевых вызовов. Никакого DNS resolve. Никаких TLS.
Никакой зависимости от `nginx`, `OSRM`, `gps-collector`, тайл-провайдеров.

### 3.2 Ingress/Egress — оценка дельты

ET-015 не меняет паттерн трафика приложения. Healthcheck-трафик
(`/api/health` каждые 30 с) **уже был** до фикса — Docker и раньше
пытался его делать через `curl`, но проваливался до connect'а. Теперь
запросы реально доходят до uvicorn. Дельта:

- **+2 req/min** к `/api/health` внутри контейнера, ~7 мс ответ,
  ~0.1 КБ ответ. **Пренебрежимо** для uvicorn (он и так уже
  обслуживает реальный трафик пользователей).
- Egress / nginx-трафик — без изменений.

## 4. Серверные ресурсы

### 4.1 Сводная таблица

| Аспект | Требование | Дельта |
|--------|------------|--------|
| CPU `app` | Без изменений | +0.01% (fork+exec python каждые 30 с — пренебрежимо) |
| RAM `app` | Без изменений | временно ~5–10 МБ на ~50–100 мс жизни healthcheck-процесса |
| Disk `app` | Без изменений | 0 |
| CPU `gps-collector` | Без изменений | 0 |
| RAM `gps-collector` | Без изменений | 0 |
| Disk `gps-collector` | Без изменений | 0 |

### 4.2 Оценка дельты CPU/RAM

- **Fork + exec `python -c "..."`:** интерпретатор поднимается за
  ~80–150 мс на mva154 (нагретый ФС-кэш). За цикл 30 с — 0.5% от
  одного ядра в пике (на 100–150 мс), что в среднем ≈ **0.005% CPU**.
- **RAM:** одноразово ~5–10 МБ на жизнь процесса. После завершения —
  возвращается ОС.
- На фоне общего idle-загруза `app` (uvicorn ~50–80 МБ RAM, ~1–2% CPU
  в idle) — пренебрежимо.

### 4.3 Disk

- Образ не растёт (`Dockerfile` не меняется).
- Логи Docker (`/var/lib/docker/containers/.../*.log`) — Docker
  пишет результаты healthcheck'а в `State.Health.Log` (хранится в
  inspect-структуре контейнера). Объём — небольшой, ограничен
  ротацией Docker (по умолчанию 5 последних записей).
- `nginx access.log` — без изменений (healthcheck-трафик внутренний,
  через nginx не проходит).

## 5. Конфигурация и секреты

| Аспект | Требование |
|--------|------------|
| Новые env-переменные | **Нет** |
| Новые секреты | **Нет** |
| Новые API-ключи | **Нет** |
| Изменения `config/*.yaml` | **Нет** |
| Изменения runtime config | **Нет** |
| Изменения `style.json`/`style-dark.json` | **Нет** |

Healthcheck-URL зашит в YAML-строку (`http://localhost:5556/api/health`).
Порт **не** параметризован через `${PORT}` намеренно (TRZ R-3):

- `PORT=5556` стоит в Dockerfile (`ENV PORT=5556`) и в compose
  (`PORT=5556`). Если в будущем порт станет переменным, healthcheck-строку
  можно будет переписать через shell-form (`CMD-SHELL`) с подстановкой
  `$PORT`. Сейчас — YAGNI.

## 6. Деплой

### 6.1 Среды

- **dev (локально)**: `make dev` (или `docker compose up -d app`).
  Достаточно `git pull && docker compose up -d app` для смены
  healthcheck-команды. Без `docker compose build`.
- **test (mva154)**: `https://openclaw.mva154.duckdns.org/enduro/`.
  Деплой через `make deploy-test` (стандартная процедура), либо ручной
  SSH + `docker compose up -d app`.
- **prod** — пока не задействован; ET-015 деплоится только в test.

### 6.2 Процедура деплоя в test

1. **Pre-deploy snapshot** — зафиксировать «как было»:
   ```bash
   ssh mva154 'docker inspect enduro-trails-app-1 \
     --format "Status: {{.State.Health.Status}} | FailingStreak: {{.State.Health.FailingStreak}} | RestartCount: {{.RestartCount}}"'
   ```
   Ожидается (для подтверждения бага из BRD §1): `Status: unhealthy |
   FailingStreak: <большое число> | RestartCount: 0`.

2. **Pre-deploy smoke** — проверить, что приложение реально живо:
   ```bash
   curl -sI 'https://openclaw.mva154.duckdns.org/enduro/api/health' | head -1
   ```
   Ожидается `HTTP/1.1 200 OK`.

3. **Pull новой версии** на mva154 (после merge в main):
   ```bash
   ssh mva154 'cd /home/slin/enduro-trails && git pull'
   ```

4. **Пересоздание контейнера `app`** (без ребилда образа — TRZ R-2):
   ```bash
   ssh mva154 'cd /home/slin/enduro-trails && docker compose up -d app'
   ```
   Docker увидит изменение `healthcheck` в compose-файле, пересоздаст
   контейнер `enduro-trails-app-1`. Контейнер `gps-collector` не
   трогается (batch profile + restart: "no").

5. **Pre-stable wait** — Docker применит `start_period: 20s`. Первые
   ~20 с healthcheck может показывать `starting`. Затем циклы
   `interval=30s × retries=3` — то есть до `healthy` пройдёт ≤ 20 +
   30 = 50 с в нормальном случае, гарантированный SLA — ≤ 120 с
   (AC-01).

6. **Post-deploy verification** — три замера:

   ```bash
   # T0 = сразу после up -d app, повторять с интервалом 30 с до healthy
   for i in 1 2 3 4 5 6; do
     ssh mva154 'docker inspect enduro-trails-app-1 \
       --format "T+{{.State.Health.Status}} streak={{.State.Health.FailingStreak}}"'
     sleep 30
   done
   ```
   Ожидается: за ≤ 120 с — `T+healthy streak=0`.

   ```bash
   # Подтверждение AC-02: healthy через 5 и 10 минут
   sleep 300 && ssh mva154 'docker inspect ... --format "{{.State.Health.Status}} {{.State.Health.FailingStreak}}"'
   sleep 300 && ssh mva154 'docker inspect ... --format "{{.State.Health.Status}} {{.State.Health.FailingStreak}}"'
   ```
   Ожидается: оба замера — `healthy 0`.

   ```bash
   # Подтверждение AC-08: эндпоинт живой снаружи
   curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' \
     'https://openclaw.mva154.duckdns.org/enduro/api/health'
   ```
   Ожидается `200 <1.0`.

7. **Записать результаты** в `docs/work-items/ET-015/13-test-report.md`
   и `docs/work-items/ET-015/14-deploy-log.md` (на следующих этапах).

### 6.3 Rollback

В случае проблем (например, python one-liner крэшит на нестандартной
Docker Engine или эндпоинт `/api/health` начал отвечать медленнее 3 с):

1. **Revert коммита**: `git revert <commit>` на mva154 в `main`.
2. **Пересоздание контейнера**: `docker compose up -d app`.
3. Старая (поломанная) healthcheck-команда `curl ...` вернётся, но
   само приложение продолжит работать (доказано в BRD §1).

RTO: ≤ 5 минут.
RPO: 0 — никаких данных не теряется (healthcheck — read-only HTTP
запрос).

### 6.4 CI/CD гейты

- **`make lint`** (ruff + eslint + YAML-валидация compose) — должен
  быть зелёным. Проверяет, что docker-compose.yml парсится.
- **`make test`** (pytest unit + integration):
  - ST-01..ST-07 — статические проверки (grep по compose/Dockerfile/CHANGELOG).
  - UT-01..UT-03 — smoke на python one-liner против live `make dev`
    (опционально, требует поднятого uvicorn).
- **Integration-CI / ручная проверка**:
  - IT-01..IT-04 — `docker compose up -d app` локально + проверка
    переходов healthy/unhealthy.
- **E2E на mva154**:
  - E2E-01 — `docker inspect` после `make deploy-test` (оператор).
  - E2E-02 — `curl https://openclaw.mva154.duckdns.org/enduro/api/health`
    (автоматизируется).

### 6.5 Зависимости деплоя

- **Docker Engine на mva154**: должен поддерживать `start_period`
  (введён в Docker 1.12 / 2016). На mva154 — Docker ≥ 20.10
  (BRD §6). ✓
- **Compose version**: `version: "3.8"` (`docker-compose.yml:1`)
  поддерживает все используемые healthcheck-поля. ✓
- **Образ `python:3.12-slim`** должен оставаться available на
  Docker Hub. ✓

## 7. Observability / Логирование

| Аспект | Требование |
|--------|------------|
| Новые лог-сообщения | **Нет** новых на стороне приложения |
| Логи healthcheck | Docker пишет в `State.Health.Log` (просмотр через `docker inspect`) |
| Метрики / Prometheus | Не вводим (но `State.Health.Status` теперь стал **достоверным** для будущей интеграции) |
| Health endpoint | `/api/health` без изменений; `/api/gps-tracks/health` без изменений |
| `uvicorn.access` лог | +2 req/min на `/api/health` (внутренний loopback) — фоновый шум, не блокирует анализ |

### 7.1 Что мониторить после деплоя

**Сутки наблюдения** на mva154 (ручная проверка, без алёртов):

1. **`docker inspect enduro-trails-app-1`**:
   - `State.Health.Status` должен быть `healthy` стабильно.
   - `State.Health.FailingStreak` должен оставаться `0`.
   - `State.Health.Log[-5:]` — все `ExitCode: 0`, `Output: ""` (или
     ничего значимого).
   - `RestartCount` должен оставаться прежним (контейнер не перезапускается
     из-за healthcheck — у нас нет `restart_policy.condition: unhealthy`,
     но всё равно полезно зафиксировать).

2. **`uvicorn access.log` в контейнере**:
   - `GET /api/health HTTP/1.1 200` каждые ~30 с.
   - Время ответа стабильно < 100 мс (на mva154 — ~7 мс по замерам BRD §1).

3. **`nginx access.log` на хосте** (внешний трафик):
   - Без изменений vs до деплоя; healthcheck идёт **внутри**
     контейнера и в nginx не виден.

### 7.2 Алёрты (будущее)

ET-015 закрывает причину «вечного `unhealthy`» — теперь
`docker inspect ... .State.Health.Status` снова **достоверная метрика**.
Если в проекте появится мониторинг (Prometheus + alertmanager, или
простой cron-скрипт), можно настроить:

- Алёрт «контейнер unhealthy ≥ 5 мин» — теперь это будет реальный сигнал.
- Алёрт «FailingStreak растёт» — раньше был ложно-положительным,
  теперь — настоящий.

Это **не задача ET-015** (out of scope BRD §7), но ET-015 — необходимое
условие для будущей интеграции.

## 8. Резервное копирование / Disaster recovery

| Аспект | Требование |
|--------|------------|
| Backup БД | Без изменений vs ET-013/ET-008 (ET-015 не трогает БД) |
| Backup тайлов | Без изменений |
| Backup статики | Без изменений; git — источник истины |
| Backup конфигурации | `docker-compose.yml` — в git, перепрочитывается при каждом `docker compose up` |
| RTO | ≤ 5 минут (rollback через git revert + `docker compose up -d app`) |
| RPO | 0 — никаких данных не теряется |

## 9. Безопасность

| Аспект | Требование |
|--------|------------|
| Auth / Authorization | Без изменений |
| Валидация входных данных | Не применимо — healthcheck не принимает внешних входов |
| CSP | Без изменений |
| Rate-limit | Без изменений (loopback-трафик не подпадает) |
| TLS | Без изменений |
| Shell injection | **Снят как риск** (см. ADR-020 Вариант A: используется `CMD`-массив, не `CMD-SHELL`; нет интерполяции пользовательского ввода) |
| `urllib.request` SSRF | Не применимо: URL зашит в YAML, не строится из переменных; loopback only |
| Privilege escalation | Не применимо: python запускается от того же user'а, что и uvicorn (root в python:3.12-slim — стандартно для этого образа; ET-015 это не меняет) |

### 9.1 Анализ риска `urllib.request` vs `curl` (security delta)

- `curl` (если бы был установлен): C-код с историей CVE (HTTP/2,
  TLS, libidn). Не используется — изначально его нет.
- `urllib.request`: чистый Python, stdlib. История CVE значительно
  меньше; используется только на loopback с фиксированным URL → SSRF
  поверхность отсутствует.
- **Чистый выигрыш по security**: меньше attack surface, меньше
  кода в образе.

## 10. Совместимость

| Аспект | Требование |
|--------|------------|
| API контракт | Без изменений |
| Совместимость с PH-1..PH-9 | Полностью совместимо: healthcheck — runtime инфра, не задевает фичи |
| Совместимость с ET-007/008/009/011/012/013/014 | Полностью совместимо |
| Совместимость с Docker Engine | ≥ 20.10 (требуется `start_period`); подтверждено на mva154 (BRD §6) |
| Совместимость с Docker Compose | `version: "3.8"` поддерживает все используемые поля |
| Совместимость с базовым образом | `python:3.12-slim` → `python` alias + `urllib.request` + `sys` гарантированы |
| Совместимость с будущими образами `python:3.13-slim` и далее | Высокая: `urllib.request` стабильный API с Python 2.x; alias `python` поддерживается во всех современных python-slim тегах |
| localStorage migration | Не применимо — фронтенд не трогается |
| Совместимость со старыми вкладками браузера | Не применимо |

## 11. Связанные документы

- `01-brd.md` §1–§9
- `02-trz.md` §1–§9 (особенно §3 — целевое состояние, §4 — альтернативы)
- `03-acceptance-criteria.md` AC-01..AC-10
- `04-test-plan.yaml` ST-01..ST-07, UT-01..UT-03, IT-01..IT-04, E2E-01..E2E-02
- `06-adr/ADR-020-healthcheck-via-python-urllib.md`
- `08-data-requirements.md` (этот пакет)
- `10-tech-risks.md` (этот пакет)
- `docs/architecture/README.md` §«Компоненты», §«Деплой»
- `docs/work-items/ET-014/07-infra-requirements.md` — образец «zero-infra»
  work-item (наследие)
- `docs/work-items/ET-008/07-infra-requirements.md` (если есть) —
  образец docker-compose правок с major-change escalation (наследие,
  для контраста: ET-015 явно minor-change)
