MAX-olymp

Запуск без Docker (Windows)

Основной путь — Docker, см. deploy-my-olymp.md. Этот вариант остаётся для машины, где Docker не ставится: например, Windows Server на VPS без вложенной виртуализации. Там Linux-контейнеры не запускаются вовсе, и всё, что делал compose, приходится задать руками.

Шаги 0, 4 и 5 основной инструкции — домен, подписка на события и адрес в кабинете MAX — те же самые, только команды без docker compose.

Настройка

Пропущенное здесь проявляется не при старте, а на первом запросе.

Рабочий каталог решает. Файл .env ищется относительно каталога, из которого запущен процесс, а не рядом с кодом. Запускаете из C:\Server — файл должен лежать в C:\Server\.env. С версии, где появился этот раздел, приложение само пишет в лог полный путь, по которому искало.

Проверить, что файл вообще есть и назван правильно:

dir /a C:\Server\.env

dir без /a скрытые файлы не показывает. Если видно .env.txt — это и есть причина: «Блокнот» дописывает расширение, а такой файл не читается. Переименовать можно так:

ren C:\Server\.env.txt .env

Не создавайте файл через > в PowerShell. ... > .env и Out-File в Windows PowerShell 5.1 пишут UTF-16, и приложение падает на старте с UnicodeDecodeError. Надёжнее из cmd:

cd /d C:\Server
(echo BOT_TOKEN=ваш_токен)>.env

Всё остальное файл переживает: переводы строк CRLF, BOM, кавычки вокруг значения и пробелы вокруг = разбираются правильно. Пути Windows можно писать как есть: EXTRA_CA_CERTS_DIR=C:\Server\certs.

Что задать помимо токена. Значения по умолчанию рассчитаны на Docker и вне его не работают:

Переменная Значение по умолчанию Почему не подойдёт
DATABASE_URL postgresql+asyncpg://app:app@db:5432/app хост db существует только в сети compose; нужен адрес вашей Postgres — см. ниже
EXTRA_CA_CERTS_DIR certs подойдёт как есть, если каталог certs\ лежит рядом с рабочим
APP_ENV development на публичном сервере открывает /docs и /openapi.json

Без первой переменной API отвечает ошибкой подключения к базе, без второй запросы к platform-api2.max.ru падают на TLS.

Postgres нужна настоящая, SQLite не подойдёт. В моделях каталога используется тип JSONB, он есть только в Postgres; миграции написаны под неё же. Порядок на Windows:

  1. Поставить PostgreSQL для Windows. В compose стоит 16 (postgres:16-alpine), но 17 работает так же — версия важна только тем, что от неё зависит путь к утилитам. В окне выбора компонентов нужны два:

    Компонент Нужен Зачем
    PostgreSQL Server да собственно база
    Command Line Tools да psql, createdb — ими делаются шаги ниже
    pgAdmin 4 по желанию графический клиент, ~500 МБ; для запуска не нужен
    Stack Builder нет загрузчик расширений вроде PostGIS, ничего из него не требуется

    В мастере: каталоги оставить по умолчанию, пароль пользователю postgres — только буквы и цифры (@, :, /, ? пришлось бы кодировать в строке подключения). На последнем шаге снять галочку запуска Stack Builder.

    Запомните порт. Обычно 5432, но если он занят — например, уже стоит другая версия Postgres, — установщик молча предложит 5433. Этот номер дальше нужен и в командах createdb/psql (ключ -p), и в DATABASE_URL. Посмотреть, кто занимает порты:

    Get-NetTCPConnection -LocalPort 5432,5433 -State Listen | Select-Object LocalPort, OwningProcess
    
  2. Добавить утилиты Postgres в PATH текущей сессии — тогда их можно вызывать по имени, и команды ниже одинаковы в PowerShell и в cmd. Номер версии подставьте свой, по имени созданной папки:

    $env:Path += ";C:\Program Files\PostgreSQL\17\bin"
    

    Без этого в PowerShell команда, начинающаяся с кавычки, не запустится: он примет её за строку и ответит «Непредвиденная лексема». Вызвать файл по полному пути там можно только через оператор &: & "C:\Program Files\...\createdb.exe" -U postgres ....

  3. Создать базу — обязательно с явной кодировкой UTF-8:

    createdb -U postgres -E UTF8 -T template0 --locale-provider=icu --icu-locale=ru-RU max_olymp
    

    Спросит пароль пользователя postgres — тот, что задали при установке.

    На русской Windows кластер создаётся в системной локали, и без этих ключей база получилась бы в WIN1251 — русские названия олимпиад в ней поехали бы. --icu-locale=ru-RU заодно даёт правильную сортировку: в каталоге ORDER BY name выполняет сама база.

    Если сборка без ICU и команда откажет, сработает вариант попроще, с сортировкой по байтам (кириллица упорядочится верно, но заглавные окажутся впереди строчных):

    createdb -U postgres -E UTF8 -T template0 --locale=C max_olymp
    

    Проверить, что вышло:

    psql -U postgres -l
    

    В строке max_olymp должно стоять UTF8. Если там WIN1251 — базу нужно удалить (dropdb -U postgres max_olymp) и создать заново, пока каталог в неё не импортирован.

  4. Прописать в .env:

    DATABASE_URL=postgresql+asyncpg://postgres:пароль@localhost:5432/max_olymp
    

    Порт в строке — тот, что выбрал установщик. Если 5433, то и здесь 5433.

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

    Если в пароле есть @, :, / или ?, их нужно закодировать (@ → %40): иначе строка разберётся неправильно. Проще задать пароль из букв и цифр.

    Проверить, что приложение читает именно то, что нужно, можно по логу старта: он печатает строку подключения целиком, с портом и без пароля.

  5. Создать схему и наполнить каталог, из C:\Server:

    alembic upgrade head
    python -m scripts.import_subjects
    python -m scripts.import_schedule
    python -m scripts.import_catalog
    

    Без import_schedule у олимпиад каталога нет дат, и календарь пуст.

Связь с базой проверяется при старте: в логе будет либо «База … отвечает», либо причина отказа. Пароль в лог не попадает.

Сертификаты кладите в certs\ рядом с рабочим каталогом — то есть C:\Server\certs\, а не C:\Server\app\certs\. Годятся только файлы с расширением .crt, .pem и .cer; связку .p7b с Госуслуг нужно сначала разобрать на отдельные сертификаты. Лог старта перечисляет, что подхватил, а если не подхватил ничего — пишет полный путь, где искал.

Статику приложение отдаёт само — если найдёт. Положите каталог frontend/ рядом с рабочим каталогом, из которого запускаете:

C:\Server\
├── app\          код приложения
├── frontend\     файлы мини-приложения
└── .env

Тогда GET / вернёт index.html, и в MAX приложение откроется. Каталог ищется по FRONTEND_DIR (по умолчанию frontend, путь от рабочего каталога). В лог старта пишется, нашёлся он или нет.

Если каталога нет, раздача выключается и корень отвечает 404 — это нормально для Docker, где статику отдаёт nginx, но при запуске без него означает, что мини-приложение не откроется вовсе.

Отдаётся с Cache-Control: no-cache: встроенный браузер MAX иначе держит старую версию и после выкладки.

Быстрая проверка, что токен доехал — не заглядывая в файл:

curl https://my-olymp.ru/health

Поле bot_configured должно быть true. Если false — процесс токена не видит, и смотреть надо в лог старта: там теперь написано, какой именно файл искали и нашли ли.

Чтобы отделить «токен неверный» от «файл не читается», задайте переменную прямо в сессии и перезапустите — переменная окружения имеет приоритет над файлом:

set BOT_TOKEN=<токен>
uvicorn app.main:app --host 127.0.0.1 --port 8000

Предупреждение про BOT_TOKEN пропало — значит дело было в файле.

HTTPS: Caddy

Всё описанное выше поднимает бэкенд, и на этом останавливается. А MAX доставляет события только по https на 443 и не принимает самоподписанные сертификаты, так что без TLS мини-приложение не откроется, а бот не получит ни одного события.

В Docker этим занимались nginx и certbot — оба контейнерами. Без Docker их надо чем-то заменить, и проще всего Caddy: одна программа вместо двух, сертификат Let’s Encrypt выпускается при первом запуске и продлевается сам, без webroot и заданий в планировщике.

Готовая конфигурация лежит в deploy/Caddyfile — она повторяет то, что делает наш nginx: статика мини-приложения, прокси на /api/*, /webhook, /health и схему API, перенаправление с www, заголовки безопасности и запрет кэширования статики.

Что нужно до запуска:

Порядок:

  1. Скачать caddy.exe с caddyserver.com/download и положить, например, в C:\Caddy.
  2. Скопировать туда deploy/Caddyfile.
  3. Поправить в нём два места: путь в строке root * — он должен вести на каталог frontend вашего клона, — и почту в email.
  4. Запустить:

    cd C:\Caddy
    .\caddy.exe run --config Caddyfile
    

При первом запуске Caddy сходит в Let’s Encrypt за сертификатом; в выводе появятся строки про certificate obtained successfully. После этого https://my-olymp.ru/health должен отвечать тем же, чем и http://127.0.0.1:8000/health.

Перенаправление с http на https Caddy включает сам — отдельный серверный блок на 80, как в nginx, писать не нужно.

Заголовки в блоке handle дополняют заголовки уровня сайта, а не заменяют их: в отличие от nginx, повторять Strict-Transport-Security и X-Content-Type-Options во вложенном блоке не требуется.

caddy run держит окно занятым и умирает вместе с сеансом RDP. Для постоянной работы Caddy нужно запускать службой — через обёртку вроде NSSM или заданием планировщика «При запуске компьютера».


Обновление кода

cd C:\MAX-olymp
git pull
cd backend
pip install -r requirements.txt
alembic upgrade head

и перезапустить бэкенд. alembic upgrade head здесь обязателен: в Docker миграции применяет команда запуска контейнера, а uvicorn, запущенный руками, этого не делает. Если шаг пропустить, новые таблицы не появятся — бэкенд при этом поднимется и про утренние напоминания скажет одной строкой в логе, что они отключены и почему.

Если git pull принёс новый data\olympiads_schedule.json, следом выполните python -m scripts.import_schedule — импорт обновляет этапы на месте, прогресс пользователей не теряется.

Проверить напоминания после обновления, не дожидаясь утра:

python -m scripts.send_reminders --users
python -m scripts.send_reminders --dry-run

При старте бэкенд сам спрашивает у MAX, оформлена ли подписка на события, и пишет об этом строку в лог. «MAX не присылает события на …» значит, что бот глух: не узнает о запуске, не ответит на сообщения, сводки уходить будет некому. Лечится шагом 4 — те же команды, только без docker compose:

python -m scripts.setup_webhook          # оформить подписку на my-olymp.ru
python -m scripts.setup_webhook --list   # проверить; туннельные адреса снять --delete --url

Кто запустил бота, пока подписки не было, для сервера остаётся «без событий»: bot_started приходит один раз. Такому человеку достаточно написать боту любое сообщение — после этого --users покажет «запущен».


Если что-то не работает

Снимок состояния — powershell -ExecutionPolicy Bypass -File deploy\diagnose.ps1, подробнее в основной инструкции. Симптомы, которые бывают только без Docker:

Симптом Где смотреть
BOT_TOKEN не задан при живом .env процесс запущен не из той папки или файл назван .env.txt; лог старта печатает путь, по которому искали
404 на / при работающем /api рядом с рабочим каталогом нет frontend/; лог старта пишет, где искали
В MAX вместо приложения 404 то же самое: корень домена не отдаёт index.html
500 на /api/*, в трейсбеке getaddrinfo failed DATABASE_URL ведёт на хост db из Docker; лог старта пишет это отдельной строкой
ConnectionRefusedError вместо getaddrinfo хост верный, а порт нет: установщик мог занять 5433; сверьте с логом старта
«Утренние напоминания отключены: в базе нет таблицы reminder_log» после git pull не выполнен alembic upgrade head; выполнить и перезапустить бэкенд
Календарь пуст, в ответе /api/me/calendar нет поля olympiads после git pull работает старый бэкенд: остановить uvicorn и запустить заново
Календарь пуст, olympiads есть, но пустые при сохранённых олимпиадах в базу не загружено расписание: python -m scripts.import_schedule. Если уже загружено — у выбранных олимпиад на olimpiada.ru нет дат (так у 85 из 146)