# Эксплуатация ## djangoctl.sh `bin/djangoctl.sh` — точка входа контейнеров. Он же устанавливается в образ как `/usr/local/bin/djangoctl.sh`. | Команда | Что делает | |---|---| | `prepare` | `collectstatic --noinput --clear`, `compress --force`, `migrate --noinput` | | `run` | `prepare`, затем gunicorn на `0.0.0.0:8000` | | `initialize` | Загрузить начальные фикстуры | | `run_celery worker` / `run_celery beat` | Воркер или планировщик Celery | | `develop [SOURCE_DIR]` | `prepare` и `runserver`, опционально ставит проект в editable-режиме | | `develop_celery [SOURCE_DIR]` | Воркер с `-B` (worker + beat в одном процессе) | ```{warning} `prepare` вызывает `collectstatic --clear`, то есть очищает `STATIC_ROOT` целиком. Всё, что положено в статику в обход `collectstatic`, будет удалено при каждом перезапуске `web`. ``` ### Параметры gunicorn Умолчания gunicorn (один синхронный воркер, таймаут 30 с) для портала не годятся: загрузка артефакта дольше полуминуты убивала бы воркер посреди запроса. | Переменная | По умолчанию | |---|---| | `GUNICORN_WORKERS` | `nproc * 2 + 1` — формула самого gunicorn | | `GUNICORN_TIMEOUT` | `600` | Число воркеров считается от хоста, а не вписывается в репозиторий: стенд работает на восьми, прод на шестнадцати, и одна цифра в коде не подходит ни тому, ни другому. `GUNICORN_WORKERS` в `.env` фиксирует значение там, где хост знает лучше формулы. ```{important} До правки 2026-08-26 `bin/djangoctl.sh` перекрывался копией из `$FILES_FOLDER` при каждой выкатке, и репозиторный файл на серверах не исполнялся вовсе. Копии разошлись: на стенде и на проде оказались разные варианты, а в одном из них worker и beat запускались фоном под `tail -f /dev/null` — при смерти любого из них контейнер оставался `Up`, и `restart: on-failure` не срабатывал. ``` ## Повседневные операции ```bash make shell # bash в контейнере web make djshell # django-admin shell docker compose logs -f web celery # логи docker compose run --rm web django-admin migrate docker compose run --rm web django-admin createsuperuser ``` Административный интерфейс Django — `/admin/`. ## Здоровье контейнеров | Сервис | Проверка | Лимит памяти | |---|---|---| | `web` | `GET /ready/`: `SELECT 1` и план миграций пуст → 200, иначе 503 | `12g` | | `celery` | `celery inspect ping` своего воркера раз в минуту | `8g` | | `celery-beat` | файл `/var/run/celery/celerybeat-schedule` моложе 15 минут: beat переписывает его, когда отправляет задачу, а самая частая идёт раз в 5 минут | `512m` | | `mqtt-bridge` | нет: переподключается сам, упавший процесс перезапускается | `512m` | | `db`, `redis` | `pg_isready`, `redis-cli ping` | нет: OOM-kill Postgres обрывает все соединения; память у них регулируют `shared_buffers` и `maxmemory` | `unhealthy` **не перезапускает** контейнер: вне swarm Docker так не делает. Он виден в `docker compose ps` и лампой в блоке «Контейнеры» страницы «Система». Перезапускает `restart: unless-stopped`, когда процесс упал или убит по памяти, а также после перезагрузки хоста. Лимиты — прод-замер `docker stats` с запасом в полтора раза (`web` 8,4 ГиБ, воркер вместе с beat 5,4 ГиБ на `1.9.2.1`). Если контейнер перезапускается сам, проверьте `docker inspect --format '{{.State.OOMKilled}}' <контейнер>`. `true` — поднять `mem_limit` в `docker-compose.yml`. Задача, на которой убит воркер, возвращается в очередь (`acks_late`). `/ready/` отвечает без авторизации. Наружу через nginx его выставлять незачем. ## Письма об ошибках Ошибки уровня `ERROR` логгеров `django` и `django.request` уходят на `EMAIL_ADMIN`, когда `DEBUG=False`. Одинаковая ошибка (класс исключения и путь запроса) — одно письмо за 10 минут, поэтому crash-loop не заваливает ящик. Полный поток остаётся в `docker compose logs web`. Лимит хранится в кэше (Redis); если кэш недоступен, письма уходят без лимита. ## Страница «Система» `/system/` — состояние инсталляции для суперпользователя (ссылка в меню «Сеть» рядом с «Настройками»). Шесть блоков, каждый обновляется сам и несёт лампочку: зелёная — норма, оранжевая — предупреждение, красная — проблема, серая — проверка не выполнилась. | Блок | Откуда данные | Пороги | |---|---|---| | Шапка | `SONIKS_IMAGE` из `.env`, `network.__version__`, `ENVIRONMENT`; дата сборки образа и старт `web` — из Docker API | — | | Контейнеры | `GET /containers/json` + inspect + `stats` через `docker-proxy` | `running` без healthcheck или `healthy` — норма, `starting` — предупреждение, остальное — проблема | | Хост | `/proc/stat`, `/proc/meminfo`, `/proc/uptime`, `os.getloadavg()`, `shutil.disk_usage` | CPU и память: 70/90 %; диски: 80/90 %; load average: больше числа ядер / вдвое больше | | Приложение | `SELECT 1`, `cache.set/get`, план миграций, `pg_database_size`, `LatestTleSet.last_modified`, станции онлайн | миграции не применены — проблема; TLE старше 8 ч / 24 ч | | Celery сейчас | `inspect().ping/active/reserved`, `LLEN` очереди в Redis | нет воркеров — проблема; очередь 100 / 1000 | | Резервные копии БД | `list_objects_v2` бакета `BACKUP_S3_BUCKET` с префиксом `ENVIRONMENT/`, presigned-ссылки на час, запись последнего запуска `backup_database` из кеша | не настроено — серая; нет копий или `FAILURE` — проблема; свежая копия старше 26 ч — предупреждение | | Задачи по расписанию | `APP.conf.beat_schedule` + запись последнего запуска в кеше (см. [](tasks.md)) | `FAILURE` — проблема; просрочка больше 10 минут — предупреждение | | Логи | `GET /containers/{id}/logs?tail=400` по каждому контейнеру, оставляются строки с `WARN`, `ERROR`, `CRITICAL`, `FATAL`, `PANIC`, `Traceback`, `Exception` | — | Пороги — константы в начале `network/base/system_status.py`. Кнопка «Запустить» у задачи ставит в очередь ту же обёртку с теми же аргументами, что и beat (`APP.send_task`), после подтверждения. Имя принимается только из расписания beat. ```{important} Блоки «Контейнеры», «Логи» и даты в шапке требуют сервис `docker-proxy` из `docker-compose.yml`: web не имеет ни docker-сокета, ни CLI, а прокси отдаёт только GET по контейнерам и образам. Без него эти блоки серые, остальная страница работает. Адрес — `DOCKER_API_URL` (см. [](configuration.md)). ``` Диск «/» считается по bind-монтированному `/workdir/.env`, то есть по корневой файловой системе хоста, а не по overlay контейнера; «media» — по `MEDIA_ROOT`. Если это один и тот же диск, показывается один. ## Резервные копии БД Задача `backup_database` (см. [](tasks.md)) раз в сутки в 00:00 UTC делает `pg_dump --format=custom` и потоком отправляет его в S3 — объект `/.dump` в бакете `BACKUP_S3_BUCKET`. Дамп не пишется ни на диск, ни в память целиком, размер базы не важен. Задача регистрируется только при заданном `BACKUP_S3_BUCKET`; на стенде переменная пустая. Что нужно на стороне облака (один раз): 1. Отдельный бакет **без публичного доступа**: дамп содержит e-mail, хэши паролей и токены станций, в бакет медиа он не кладётся. 2. Сервисному аккаунту с ключами `AWS_*` — права на чтение и запись в этот бакет. 3. Lifecycle-правило на бакете (например, удалять объекты старше 30 дней) — ротацию приложение не делает. Статус и ссылки — блок «Резервные копии БД» на странице «Система»; там же кнопка «Запустить» у `backup_database` делает внеплановую копию. `pg_dump` для задачи ставится в образ пакетом `postgresql-client-15` (`Dockerfile`). ### Восстановление из копии 1. Взять дамп. На странице «Система» в блоке «Резервные копии БД» — 14 последних копий с кнопкой «Скачать»; ссылка действует час. Без портала — из бакета ключами `AWS_*`: ```bash aws s3 cp "s3://$BACKUP_S3_BUCKET/production/2026-09-16T000000Z.dump" . ``` 2. Восстановить, из каталога с `docker-compose.yml` и `.env`: ```bash ./soniks.sh restore ./2026-09-16T000000Z.dump ``` Команда спрашивает подтверждение, останавливает все сервисы (`web`, `celery`, `celery-beat`, `mqtt-bridge` держат соединения и продолжают писать), поднимает `db`, ждёт `pg_isready`, подаёт дамп на stdin `pg_restore --clean --if-exists --no-owner` и поднимает сервисы обратно. Файлы `.sql` идут через `psql` — формат определяется по расширению. Если `pg_restore` сообщил об ошибках, сервисы всё равно поднимаются, а команда завершается ненулевым кодом: ошибки видны в выводе, база может быть неполной. 3. Если дамп старше кода — применить миграции: ```bash docker compose exec web django-admin migrate ``` 4. Проверить по странице «Система»: БД отвечает, число наблюдений соответствует дате дампа, задачи Celery идут. ```{important} `--clean` удаляет объекты, которые есть в дампе, но не те, которых в нём нет: таблица или колонка, добавленная миграцией после снятия копии, останется на месте. Откат схемы назад — отдельная процедура, см. раздел «Откат» в [](dev/deploy.md). ``` Время восстановления копии прода — _(замер со стенда, вписать после первого прогона)_. Запасной путь, если скрипта под рукой нет: ```bash docker compose cp file.dump db:/tmp/ docker compose exec db pg_restore -U "$POSTGRES_DB_USER" -d "$POSTGRES_DB_NAME" \ --clean --if-exists --no-owner /tmp/file.dump ``` Что процедура рабочая, проверяет тест `network/base/test_backup.py`: он снимает дамп настоящей задачей, разворачивает его теми же флагами в отдельную БД и сверяет число строк. ## Миграции Схема БД применяется автоматически при старте `web` (`djangoctl.sh prepare`). Вручную: ```bash docker compose run --rm web django-admin makemigrations docker compose run --rm web django-admin migrate ``` ```{important} Существующие файлы в `network/*/migrations/` не редактируются — только новые миграции. ``` Пока миграции идут, `web` не отвечает, а `/ready/` отдаёт `503`. На объёме прода счёт идёт на минуты — порядок выкатки и замеры в [](dev/deploy.md), раздел «Выкатка с долгими миграциями». ## Celery Воркер и beat поднимаются сервисом `celery`. Разовый запуск задачи из shell: ```python from network.base.tasks import fetch_tle fetch_tle.delay() ``` Список задач и их расписание — в [](tasks.md). У задач есть лимиты времени (по умолчанию 10 минут мягкий, 15 жёсткий; у бэкапа и обходов каталога — свои, см. [](configuration.md)) и до трёх повторов на сетевую ошибку у задач, которые ходят во внешние API. На странице «Система» это видно так: `RETRY` в состоянии задачи — повтор идёт; `FAILURE` с `SoftTimeLimitExceeded` или `TimeLimitExceeded` в ошибке — задача не уложилась в лимит. Если это повторяется у одной задачи, поднимать её собственный лимит на обёртке в `network/celery.py`, а не глобальный; при лимите выше `REDIS_VISIBILITY_TIMEOUT` поднять и его. Число процессов воркера — `CELERY_WORKER_CONCURRENCY`, по умолчанию `4`. В отличие от `GUNICORN_WORKERS` оно не считается от хоста: собственное умолчание Celery — процесс на ядро, на проде это 32 процесса при лимите памяти `8g`, рассчитанном на четыре, и 32 соединения с базой сверх воркеров gunicorn. Поднимать вместе с `mem_limit` сервиса `celery` и с оглядкой на `max_connections` Postgres. Проверка: `docker compose exec celery celery -A network inspect stats` → `pool.max-concurrency`. ## Обслуживание зависимостей Пины в `requirements.txt`, `requirements-dev.txt` и `constraints.txt` **генерируются** — править их руками нельзя. Верхнеуровневые зависимости живут в `setup.cfg` (`install_requires` и extra `dev`), после правки: ```bash ./contrib/refresh-requirements.sh # или ./soniks.sh refresh ``` Скрипт создаёт временный virtualenv, ставит проект, снимает `pip freeze`, разделяет рантайм и dev, и проверяет результат через `pip check`. Зависимости сборки документации живут отдельно, в `docs/requirements.txt`, и в `setup.cfg` не входят. Фронтенд-зависимости обновляются через `./soniks.sh update` (см. [](dev/frontend.md)). ## Схема OpenAPI ```bash ./manage.py spectacular --file soniks-network-api-client/openapi.yml --validate ``` То же самое делает джоба `schema` в CI; следующая за ней джоба `api` генерирует из схемы Python-клиент и интерактивный HTML-справочник. Содержимое `soniks-network-api-client/` — сгенерированное, руками не правится.