# Runbook эксплуатации и деплоя

## Паспорт документа

- Статус документа: living document
- Актуально на: 2 апреля 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении deploy-контура, инфраструктуры, rollback-модели или runtime topology
- Область применения: эксплуатационный и migration-слой production-инфраструктуры проекта
- Связанные документы:
- [Текущее состояние](../project/current-state.md)
- [Roadmap](../project/roadmap.md)
- [Post-deploy checklist](./post-deploy-checklist.md)
- [Инженерные принципы](../governance/engineering-principles.md)
- [Модель stage delivery и release handoff](./stage-delivery-model.md)

## 1. Текущая production-модель

Важно не путать этот документ с stage release-процессом.

Этот runbook описывает:

- текущий production/runtime-контур данного сервера;
- локальные operational команды;
- rollback и host/container topology именно этой машины.

Этот runbook не утверждает, что stage-сервер должен обновляться вручную с этой машины. Каноническая модель stage release зафиксирована отдельно в [stage-delivery-model.md](./stage-delivery-model.md): локально мы готовим change packages, а отдельный stage-контур забирает `main` автоматически.

На текущем сервере Qadam развёрнут в смешанном runtime-контуре:

- backend checkout находится в `/data/qadam-core`;
- frontend checkout находится в `/data/qadam-web`;
- `api` обслуживает production через container runtime `qadam-api-container.service`;
- legacy `qadam-api.service` сохранён как rollback-контур и не должен считаться активным production runtime по умолчанию;
- product frontend и roadmap-service пока запускаются через `systemd`;
- для `qadam-web` уже подтверждён registry-backed shadow runtime на `127.0.0.1:3002`, но он пока не обслуживает production-трафик;
- внешний трафик обслуживает `nginx`;
- PostgreSQL и Redis установлены на хосте;
- TLS обслуживается через Let’s Encrypt.

При этом важно:

- Docker Engine на сервере уже установлен;
- его `data-root` вынесен в `/mnt/qadam100gb/docker`;
- для `qadam-core/api` уже подтверждены shadow smoke, registry delivery и production cutover;
- для `qadam-web` уже подтверждены registry publish/pull и shadow smoke от registry image, но production всё ещё канонически обслуживается через `systemd`.

Дополнительные важные факты:

- этот сервер уже переведён на deploy из `qadam-core` и `qadam-web`;
- roadmap-портал читает документы из `qadam-core/docs` через `QADAM_PROJECT_ROOT=/data/qadam-core`;
- host nginx использует named upstream `qadam_api_backend` и сейчас проксирует `/api/*` на `127.0.0.1:5002`;
- для API уже существует source-controlled reversible cutover path между `qadam-api.service` и `qadam-api-container.service`;
- старый checkout `/data/uzbek` больше не является production source of truth и должен считаться legacy-копией.

Важно: legacy-скрипты вне `deploy/` и старые Docker-артефакты в корне репозитория не являются источником истины для этого production-сервера.

План полного перехода на новый container runtime описан отдельно в `docs/operations/docker-contour-migration-plan.md`. До завершения этого migration именно этот runbook остаётся каноническим для текущего mixed runtime на split-репозиториях.

Смежные обязательные документы:

- [backup-restore-runbook.md](./backup-restore-runbook.md)
- [incident-response.md](./incident-response.md)
- [environment-matrix.md](./environment-matrix.md)
- [post-deploy-checklist.md](./post-deploy-checklist.md)

## 2. Домены

### Публичный домен

- `https://qadam.2fab.app`

### Внутренний портал документации

- `https://qadam-roadmap.2fab.app`
- защищён `basic auth`
- пароль хранится на сервере в `/root/qadam-roadmap-basic-auth.txt`

## 3. Ключевые файлы на сервере

- `/etc/systemd/system/qadam-api.service`
- `/etc/systemd/system/qadam-api-container.service`
- `/etc/systemd/system/qadam-web.service`
- `/etc/systemd/system/qadam-roadmap.service`
- `/etc/systemd/system/qadam-core-runner.service`
- `/etc/systemd/system/qadam-monitor.service`
- `/etc/systemd/system/qadam-monitor.timer`
- `/etc/systemd/system/qadam-backup.service`
- `/etc/systemd/system/qadam-backup.timer`
- `/etc/docker/daemon.json`
- `/etc/nginx/sites-available/qadam.2fab.app.conf`
- `/etc/nginx/sites-available/qadam-roadmap.2fab.app.conf`
- `/etc/nginx/conf.d/qadam-api-upstream.conf`
- `/etc/qadam/qadam.env`
- `/etc/qadam/qadam-api-runtime.env`
- `/etc/qadam/qadam-web-runtime.env`
- `/etc/qadam/qadam-monitor.env`
- `/etc/qadam/qadam-backup.env`
- `/etc/qadam/qadam-roadmap.env`
- `/opt/act_runner/qadam-core/.runner`

## 4. Сервисы

### Проверка статуса

```bash
systemctl status qadam-api-container qadam-web qadam-roadmap nginx
systemctl status qadam-api --no-pager
systemctl status qadam-core-runner
systemctl status qadam-monitor.timer qadam-backup.timer --no-pager
```

### Перезапуск

```bash
systemctl restart qadam-api-container
systemctl restart qadam-web
systemctl restart qadam-roadmap
systemctl restart qadam-core-runner
systemctl start qadam-monitor.service
systemctl start qadam-backup.service
systemctl reload nginx
```

### Логи

```bash
journalctl -u qadam-api-container -n 200 --no-pager
journalctl -u qadam-api -n 200 --no-pager
journalctl -u qadam-web -n 200 --no-pager
journalctl -u qadam-roadmap -n 200 --no-pager
journalctl -u qadam-core-runner -n 200 --no-pager
journalctl -u qadam-monitor.service -n 200 --no-pager
journalctl -u qadam-backup.service -n 200 --no-pager
journalctl -u nginx -n 200 --no-pager
```

## 5. Переменные окружения

Боевые переменные лежат в `/etc/qadam/qadam.env`.

Для image-based API runtime боевые переменные лежат в `/etc/qadam/qadam-api-runtime.env`.

Для shadow/container runtime product web боевые runtime env лежат в `/etc/qadam/qadam-web-runtime.env`.

Для отдельного roadmap-service боевые переменные лежат в `/etc/qadam/qadam-roadmap.env`.

Для monitoring и backup policy боевые переменные лежат в `/etc/qadam/qadam-monitor.env` и `/etc/qadam/qadam-backup.env`.

Ключевые группы переменных:

- runtime: `NODE_ENV`, `PORT`, `HOSTNAME`
- база данных: `DATABASE_URL`, `DATABASE_REPLICA_URL`, `POSTGRES_PASSWORD`
- auth: `JWT_SECRET`
- инфраструктура: `REDIS_URL`, `CORS_ORIGIN`, `NEXT_PUBLIC_API_URL`, `API_URL`
- roadmap-портал: `QADAM_PROJECT_ROOT`, `QADAM_ROADMAP_STORAGE_DIR`, `PORT=3001`, `HOSTNAME=127.0.0.1`
- API runtime image: `QADAM_API_IMAGE`, `QADAM_API_PORT`, `QADAM_API_HOSTNAME`, `QADAM_API_ENV_FILE`, `QADAM_API_COMPOSE_PROJECT`
- web runtime image: `QADAM_WEB_IMAGE`, `QADAM_WEB_PORT`, `QADAM_WEB_BIND_HOST`, `QADAM_WEB_ENV_FILE`, `QADAM_WEB_COMPOSE_PROJECT`
- интеграции: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALERT_CHAT_ID`, `SELLER_TELEGRAM_BOT_TOKEN`, `SELLER_TELEGRAM_BOT_USERNAME`, `SELLER_TELEGRAM_WEBHOOK_SECRET`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`, `SMTP_REPLY_TO`, `SELLER_STATUS_CHANGE_NOTIFY_STATUSES`, `AXIOM_TOKEN`, `AXIOM_DATASET`
- backup runtime: `QADAM_BACKUP_ROOT_DIR`, `QADAM_BACKUP_OFFSITE_PREFIX`, `QADAM_BACKUP_*RETENTION_DAYS`

Секреты не дублируются в репозиторий.

## 6. Порядок обновления приложения на хосте

Этот раздел относится именно к текущему серверу и нужен как operational/rollback runbook.

Он не должен автоматически трактоваться как обязательный релизный процесс для stage, если stage-контур разворачивается отдельной автоматикой из `main`.

```bash
cd /data/qadam-core
pnpm install --frozen-lockfile
pnpm check-types
pnpm test
pnpm build
pnpm export:openapi
pnpm --filter @repo/prisma migrate:deploy

pnpm docker:build:api
pnpm docker:publish:api
# далее обновить QADAM_API_IMAGE в /etc/qadam/qadam-api-runtime.env на нужный git-<commit-sha> tag
pnpm docker:cutover:api

cd /data/qadam-web
pnpm install --frozen-lockfile
set -a
source /etc/qadam/qadam.env
export API_URL=http://127.0.0.1:5002/api/v1
set +a
pnpm generate:api-contract
pnpm check-types
pnpm build

systemctl restart qadam-web
systemctl restart qadam-roadmap
systemctl reload nginx
```

Если менялись только отдельные пакеты, допускается более узкая сборка, но канонический путь для production на этом сервере — полная проверка и полная сборка. Для `api` канонический runtime-update теперь идёт через image tag и `pnpm docker:cutover:api`, а не через прямой `systemctl restart qadam-api`.

Repo-side CI для `qadam-core` теперь обслуживается локальным self-hosted runner:

- сервис: `qadam-core-runner`
- рабочий каталог: `/opt/act_runner/qadam-core`
- регистрационное состояние runner: `/opt/act_runner/qadam-core/.runner`
- workflow source: `/data/qadam-core/.gitea/workflows/quality-gate.yml`
- runner cache и hostexecutor workspace вынесены в `/tmp/act_runner-cache` и `/tmp/act_runner-tmp`, чтобы CI не упирался в `ENOSPC` на корневом разделе
- runner состоит в группе `docker`, поэтому workflow умеет собирать API image и запускать container smoke локально на хосте
- для runner уже настроен `docker login` в Gitea Container Registry `git.2fab.app`, поэтому workflow может публиковать API image без ручного ввода credentials

Если `Quality Gate` падает на `ENOSPC`, канонический порядок проверки такой:

```bash
df -h / /tmp
systemctl status qadam-core-runner
readlink -f /opt/act_runner/.cache
journalctl -u qadam-core-runner -n 200 --no-pager
```

Нормальное состояние:

- `/opt/act_runner/.cache` указывает в `/tmp/act_runner-cache`;
- `qadam-core-runner.service` выставляет `XDG_CACHE_HOME=/tmp/act_runner-cache` и `TMPDIR=/tmp/act_runner-tmp`;
- root disk не используется как основное место для runner workspace и Prisma cache.

Если релиз меняет API-контракт, перед рестартом должен проходить и контрактный sync-check:

```bash
cd /data/qadam-core
pnpm export:openapi

cd /data/qadam-web
pnpm generate:api-contract
pnpm check:api-contract
```

Если релиз затрагивает канонические документы или delivery-процесс, дополнительно обязателен:

```bash
cd /data/qadam-core
pnpm check:docs
```

## 6A. Контейнерный smoke для `qadam-core/api`

Этот сценарий пока не заменяет канонический host deploy, но уже является обязательной инженерной проверкой для `CP-301`.

Сейчас у него два канонических режима:

```bash
cd /data/qadam-core
pnpm smoke:api-container
```

Этот сценарий поднимает временные `postgres` и `redis` контейнеры в отдельной docker network, применяет migrations внутри API image и проверяет `health/ready` и `metrics` без обращения к production БД.

Второй сценарий проверяет уже сам runtime-manifest на текущем production-хосте, но без cutover:

```bash
cd /data/qadam-core
pnpm docker:build:api
pnpm docker:smoke:api-runtime
```

Он:

- собирает API image с каноническим тегом `qadam-core-api:git-<commit>`;
- поднимает `deploy/compose/docker-compose.api-runtime.yml` как shadow-runtime на `127.0.0.1:5002`;
- использует production env из `/etc/qadam/qadam.env`, но не останавливает host-level `qadam-api`;
- проверяет `GET /api/v1/health/ready` и `GET /api/v1/metrics`;
- после проверки удаляет shadow-container.

Ожидаемое поведение:

- image собирается без обращения к `/var/lib/docker` на корневом разделе;
- shadow-container поднимается на `127.0.0.1:5002`;
- `GET /api/v1/health/ready` отвечает `200`;
- `GET /api/v1/metrics` отвечает `200` на loopback;
- host-level `qadam-api` на `127.0.0.1:5001` продолжает работать штатно.

Если нужно пройти publish/pull путь через registry, канонический сценарий такой:

```bash
cd /data/qadam-core
pnpm docker:build:api
pnpm docker:publish:api
QADAM_API_IMAGE=git.2fab.app/eldar/qadam-core-api:git-<commit-sha> pnpm docker:smoke:api-runtime
```

Ожидаемое поведение:

- image публикуется в Gitea Container Registry `git.2fab.app/eldar/qadam-core-api`;
- registry-tag можно поднять через тот же shadow smoke без локальной пересборки;
- host-level `qadam-api` остаётся активным и не прерывается.

Канонический template delivery-обвязки для такого запуска лежит в:

- `deploy/env/api-runtime.env.example`
- `deploy/compose/docker-compose.api-runtime.yml`
- `deploy/scripts/build-api-image.sh`
- `deploy/scripts/publish-api-image.sh`
- `deploy/scripts/smoke-api-runtime-compose.sh`

## 6C. Registry-backed shadow smoke для `qadam-web`

Для product web container delivery на production-хосте пока каноничен не cutover, а shadow smoke от готового registry image:

```bash
cd /data/qadam-web
pnpm install --frozen-lockfile
set -a
source /etc/qadam/qadam.env
export API_URL=http://127.0.0.1:5002/api/v1
set +a
pnpm build
pnpm docker:build:web
pnpm docker:publish:web
QADAM_WEB_IMAGE=git.2fab.app/eldar/qadam-web-app:git-<commit-sha> pnpm docker:smoke:web-runtime
```

Этот сценарий:

- собирает лёгкий runtime image из уже готового `.next/standalone`, `.next/static` и `public`;
- публикует его в Gitea Container Registry `git.2fab.app/eldar/qadam-web-app`;
- при необходимости делает `docker pull` registry-tag без локальной пересборки;
- поднимает shadow runtime на `127.0.0.1:3002` через `deploy/compose/docker-compose.web-runtime.yml`;
- использует app env из `/etc/qadam/qadam-web-runtime.env`, где server-side `API_URL` должен указывать на `http://host.docker.internal:5002/api/v1`.

Ожидаемое поведение:

- `GET http://127.0.0.1:3002/api/health` отвечает `200`;
- главная страница product web отвечает `200` и содержит ожидаемый `<title>`;
- production-трафик на `https://qadam.2fab.app` при этом продолжает обслуживаться host-level `qadam-web.service`.

Канонические файлы этого слоя:

- `Dockerfile`
- `.dockerignore`
- `deploy/compose/docker-compose.web-runtime.yml`
- `deploy/env/web-runtime.env.example`
- `deploy/scripts/build-web-image.sh`
- `deploy/scripts/publish-web-image.sh`
- `deploy/scripts/smoke-web-runtime-compose.sh`

## 6B. Reversible cutover для `api`

Для текущего production API канонический переключатель выглядит так:

```bash
cd /data/qadam-core
pnpm docker:cutover:api
```

Этот сценарий:

- читает `/etc/qadam/qadam-api-runtime.env`;
- поднимает `qadam-api-container.service` на `127.0.0.1:5002`;
- ждёт локальный `health/ready`;
- переводит `/etc/nginx/conf.d/qadam-api-upstream.conf` на новый порт;
- перезагружает `nginx`;
- проверяет публичный `https://qadam.2fab.app/api/v1/health/ready`;
- останавливает legacy `qadam-api.service` только после успешного public health.

Канонический rollback:

```bash
cd /data/qadam-core
pnpm docker:rollback:api
```

Он:

- поднимает legacy `qadam-api.service` на `127.0.0.1:5001`;
- ждёт локальный и публичный `health/ready`;
- переводит `qadam_api_backend` обратно на `5001`;
- останавливает `qadam-api-container.service`.

## 7. Smoke-check после деплоя

Полный operational checklist смотри в [post-deploy-checklist.md](./post-deploy-checklist.md). Ниже остаётся минимальный канонический smoke-set.

```bash
curl -I https://qadam.2fab.app
curl https://qadam.2fab.app/api/v1/health
curl https://qadam.2fab.app/api/v1/health/live
curl https://qadam.2fab.app/api/v1/health/ready
curl -I https://qadam.2fab.app/api/docs
curl https://qadam.2fab.app/api/openapi.json
curl http://127.0.0.1:5002/api/v1/metrics | head
curl -I https://qadam.2fab.app/roadmap
curl -I -u <login>:<password> https://qadam-roadmap.2fab.app
curl -u <login>:<password> https://qadam-roadmap.2fab.app/api/health
bash -lc 'set -a; source /etc/qadam/qadam.env; set +a; cd /data/qadam-core && pnpm telegram:alert -- --message "<b>Qadam</b> monitoring smoke test"'
```

Ожидаемое поведение:

- `https://qadam.2fab.app/roadmap` должен возвращать `404`;
- `https://qadam.2fab.app/api/v1/health/live` и `https://qadam.2fab.app/api/v1/health/ready` должны отвечать `200`;
- `http://127.0.0.1:5002/api/v1/metrics` должен отдавать Prometheus-compatible text/plain, а внешний `https://qadam.2fab.app/api/v1/metrics` — резаться на `403`;
- `pnpm telegram:alert` должен доставлять тестовое сообщение в operational Telegram-канал, заданный через `TELEGRAM_ALERT_CHAT_ID`;
- `https://qadam-roadmap.2fab.app` требует `basic auth`;
- `https://qadam-roadmap.2fab.app/api/health` должен отвечать `{"status":"ok"}` после аутентификации.

Дополнительно полезно проверить, что runtime действительно ушёл на новые checkout-пути:

```bash
systemctl show qadam-api -p ExecStart -p WorkingDirectory
systemctl show qadam-api-container -p ExecStart -p WorkingDirectory
systemctl show qadam-web -p ExecStart -p WorkingDirectory
systemctl show qadam-roadmap -p ExecStart -p WorkingDirectory
systemctl show qadam-core-runner -p ExecStart -p WorkingDirectory
systemctl show qadam-monitor -p ExecStart -p WorkingDirectory
systemctl show qadam-backup -p ExecStart -p WorkingDirectory
```

Дополнительно для monitoring baseline полезно проверить timer:

```bash
systemctl status qadam-monitor.timer --no-pager
systemctl status qadam-monitor.service --no-pager
cat /var/lib/qadam-monitor/state.json
```

Дополнительно для backup baseline полезно проверить:

```bash
systemctl status qadam-backup.timer --no-pager
systemctl status qadam-backup.service --no-pager
cat /var/lib/qadam-backup/state.json
find /var/backups/qadam -maxdepth 1 -mindepth 1 | sort
```

Нормальное состояние для `qadam-backup.service` после успешного ручного или timer-run запуска — `inactive (dead)` с `status=0/SUCCESS`, потому что это `Type=oneshot`, а не long-running daemon.

## 8. SSL и renewal

- Сертификаты лежат в `/etc/letsencrypt/live/`.
- Renewal выполняется через `certbot.timer`.
- После обновления сертификатов `nginx` должен быть перезагружен.

Полезные команды:

```bash
systemctl status certbot.timer
certbot certificates
certbot renew --dry-run
```

## 9. Rollback

Если после обновления приложение не стартует:

1. Смотри `journalctl` по `qadam-api-container`, `qadam-api`, `qadam-web` и `qadam-roadmap`.
2. Если проблема в API runtime-cutover, сначала выполни `pnpm -C /data/qadam-core docker:rollback:api`.
3. Проверяй корректность `/data/qadam-web/apps/web/.next/standalone`, `/data/qadam-web/apps/roadmap/.next/standalone`, image tag в `/etc/qadam/qadam-api-runtime.env` и миграций Prisma.
4. Откатывай код или image tag к предыдущему рабочему состоянию.
5. Повторяй сборку и рестарт только тех сервисов, которые реально затронуты.
6. Если проблема только в frontend, можно временно откатить web без отката БД и без rollback API runtime.

Rollback не должен использовать разрушительные git-команды без понимания состояния рабочей директории.

Если rollback связан с данными или storage, использовать [backup-restore-runbook.md](./backup-restore-runbook.md), а не импровизировать вручную.

## 10. Что ещё нужно улучшить

- Сделать автоматизированный post-deploy smoke script.
- Поверх базового Telegram alerting добавить более зрелый alert routing и dashboards.
- Довести `qadam-web` от registry-backed shadow runtime до production cutover и rollback-процедуры.
- После стабилизации mixed runtime удалить legacy host-only API deploy steps.
