Исправлен деплой скрипт

This commit is contained in:
2026-10-07 16:20:42 +03:00
parent d897601d88
commit cc194b8eee
7 changed files with 201 additions and 19 deletions

View File

@@ -10,5 +10,7 @@ SCHEDULE_CACHE_TTL=900
SCHEDULE_RETRY_AFTER=60 SCHEDULE_RETRY_AFTER=60
# --- порты (только для docker) ------------------------------------------------- # --- порты (только для docker) -------------------------------------------------
# Оба порта публикуются на 127.0.0.1 (жёстко задано в docker-compose.yml):
# снаружи сервисы отдаёт nginx, напрямую в интернет они не смотрят.
API_PORT=8000 API_PORT=8000
FRONTEND_PORT=8080 FRONTEND_PORT=8080

View File

@@ -1,7 +1,7 @@
PY := $(CURDIR)/.venv/bin/python PY := $(CURDIR)/.venv/bin/python
PIP := $(CURDIR)/.venv/bin/pip PIP := $(CURDIR)/.venv/bin/pip
.PHONY: help venv run dev run-api run-front test build up down restart logs ps deploy .PHONY: help venv run dev run-api run-front test build up down restart logs ps deploy nginx-print nginx-install
help: ## Показать этот список команд help: ## Показать этот список команд
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}' @grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}'
@@ -57,3 +57,11 @@ ps: ## Статус сервисов
deploy: ## Собрать и запустить (docker) deploy: ## Собрать и запустить (docker)
./deploy.sh ./deploy.sh
# --- nginx (на сервере) ------------------------------------------------------
nginx-print: ## Показать конфиг nginx для этого стенда, ничего не меняя
deploy/nginx-site.sh --print
nginx-install: ## Сгенерировать, включить и перечитать конфиг nginx (запускать с sudo)
sudo deploy/nginx-site.sh

View File

@@ -24,7 +24,7 @@ make run # создаст .venv, поставит зависим
Яндекс.Диск (xlsx) Яндекс.Диск (xlsx)
│ скачивание по публичной ссылке │ скачивание по публичной ссылке
▼ ▼
api (FastAPI), порт 8000, наружу — только 127.0.0.1 (в docker так публикуется порт) api (FastAPI), порт 8000, в docker публикуется на 127.0.0.1
• скачивает и разбирает xlsx без сторонних библиотек • скачивает и разбирает xlsx без сторонних библиотек
• кэш в памяти: TTL + проверка даты изменения файла + stale-while-error • кэш в памяти: TTL + проверка даты изменения файла + stale-while-error
• один воркер, поэтому кэш общий • один воркер, поэтому кэш общий
@@ -128,7 +128,8 @@ src/
tools/ tools/
make_icons.py генерация иконок приложения из логотипа (нужен rsvg-convert) make_icons.py генерация иконок приложения из логотипа (нужен rsvg-convert)
deploy/ deploy/
nginx-dev.conf пример сайта nginx для отдельного стенда nginx-site.sh генерация и включение конфига nginx (SSL — сертификат родителя)
nginx-dev.conf конфиг-заготовка для стенда без wildcard у родителя
README-dev.md как поднять второй стенд рядом с основным README-dev.md как поднять второй стенд рядом с основным
tests/ tests/
conftest.py общие фикстуры conftest.py общие фикстуры
@@ -176,8 +177,8 @@ tests/
| `SCHEDULE_RETRY_AFTER` | `60` | api | пауза между попытками при сбое источника | | `SCHEDULE_RETRY_AFTER` | `60` | api | пауза между попытками при сбое источника |
| `SCHEDULE_API` | `http://127.0.0.1:8000` | frontend | адрес бэкенда (в docker: `http://api:8000`) | | `SCHEDULE_API` | `http://127.0.0.1:8000` | frontend | адрес бэкенда (в docker: `http://api:8000`) |
| `SCHEDULE_PROXY_TIMEOUT` | `150` | frontend | таймаут прокси к бэкенду, сек | | `SCHEDULE_PROXY_TIMEOUT` | `150` | frontend | таймаут прокси к бэкенду, сек |
| `API_PORT` | `8000` | docker | внешний порт api | | `API_PORT` | `8000` | docker | внешний порт api (публикуется на 127.0.0.1) |
| `FRONTEND_PORT` | `8080` | docker | внешний порт frontend (на проде: `127.0.0.1:8090`) | | `FRONTEND_PORT` | `8080` | docker | внешний порт frontend (публикуется на 127.0.0.1) |
`.env` ищется вверх от каталога модуля: и backend, и frontend находят общий `.env` ищется вверх от каталога модуля: и backend, и frontend находят общий
`.env` в корне репозитория; `load_dotenv` не перезаписывает уже выставленные `.env` в корне репозитория; `load_dotenv` не перезаписывает уже выставленные
@@ -219,12 +220,35 @@ tests/
пакет venv: `sudo apt install python3-venv` (для нестандартной версии — пакет venv: `sudo apt install python3-venv` (для нестандартной версии —
например, `python3.12-venv`). например, `python3.12-venv`).
- `httpx2` и `httpcore2` — новые пакеты (август 2026, httpx перешёл под - `httpx2` и `httpcore2` — новые пакеты (август 2026, httpx перешёл под
опеку Pydantic и был переименован). Если pip берёт пакеты из корпоративного опеку Pydantic и был переименован). В коде и тестах модуль импортируется
зеркала или старого индекса, этих пакетов там может ещё не быть; проверьте как `httpx2`; `import httpx` больше не работает. В рабочем коде httpx2
установку напрямую: `pip install -r requirements-dev.txt -i https://pypi.org/simple`. не используется — нужен только тестам.
В коде и тестах модуль импортируется как `httpx2`; `import httpx` больше
не работает. В рабочем коде httpx2 не используется — нужен только тестам. **Если pip не находит пакеты: зеркала**
- Makefile написан под bash: Linux, macOS или WSL. На Windows без WSL
Если pip настроен на корпоративное/национальное зеркало, оно может отставать
от PyPI — свежие пакеты на нём ещё не появились. Диагностика и обход:
```bash
pip index versions httpx2 # есть ли пакет на текущем индексе
pip install -r requirements-dev.txt -i <зеркало> # разово через другое зеркало
pip config set global.index-url <зеркало> # закрепить зеркало навсегда
```
Зеркала, проверенные 07.10.2026 (пакеты `httpx2`/`httpcore2` на месте;
URL подставлять как есть в `-i` / `index-url`):
| Зеркало | URL |
|---|---|
| PyPI, официальный | `https://pypi.org/simple` |
| TUNA (Tsinghua) | `https://pypi.tuna.tsinghua.edu.cn/simple` |
| Huawei Cloud | `https://mirrors.huaweicloud.com/repository/pypi/simple` |
| BFSU | `https://mirrors.bfsu.edu.cn/pypi/web/simple` |
| SJTU | `https://mirror.sjtu.edu.cn/pypi/web/simple` |
| USTC | `https://mirrors.ustc.edu.cn/pypi/simple` |
| SUSTech | `https://mirrors.sustech.edu.cn/pypi/simple` |
Makefile написан под bash: Linux, macOS или WSL. На Windows без WSL
используйте Docker-вариант из раздела «В Docker». используйте Docker-вариант из раздела «В Docker».
## Запуск ## Запуск
@@ -243,6 +267,8 @@ make run # = make venv + запуск api и сайта
- API — <http://127.0.0.1:8000> (например, `/status` и `/groups`); - API — <http://127.0.0.1:8000> (например, `/status` и `/groups`);
- сайт — <http://127.0.0.1:8080> (его и открывать в браузере). - сайт — <http://127.0.0.1:8080> (его и открывать в браузере).
Чтобы отвязать процесс от терминала, стоит запускать через `nohup make run &`
Остановить — `Ctrl+C`. Данные скачиваются сразу на старте приложения Остановить — `Ctrl+C`. Данные скачиваются сразу на старте приложения
(скачивание занимает несколько секунд) и дальше обновляются по интервалу. (скачивание занимает несколько секунд) и дальше обновляются по интервалу.
@@ -274,6 +300,24 @@ cp .env.example .env # вписать SCHEDULE_LINK (и порты при
У обоих сервисов есть healthcheck на `/healthz`; frontend стартует после того, У обоих сервисов есть healthcheck на `/healthz`; frontend стартует после того,
как api стал healthy. Обновление: `git pull && docker compose up -d --build`. как api стал healthy. Обновление: `git pull && docker compose up -d --build`.
### Настройка nginx (прод)
Скрипт генерирует и включает конфиг сайта: http → https, прокси на
`FRONTEND_PORT` из `.env`, после проверки `nginx -t` перечитывает nginx.
SSL обеспечивается сертификатом родительского домена (обычно wildcard
`*.our-place.ru`) — отдельный certbot для поддомена не нужен.
```bash
sudo ls /etc/letsencrypt/live/ # имя сертификата родительского домена
sudo make nginx-install # или: sudo deploy/nginx-site.sh
```
Параметры: `--domain sched.example.ru`, `--cert example.ru` (по умолчанию —
родительский домен от `--domain`), `--port 8090` (по умолчанию —
`FRONTEND_PORT` из `.env`). Посмотреть конфиг, ничего не меняя:
`make nginx-print`. Если wildcard у родителя нет — выпустите сертификат
скриптом certbot, как для dev-стенда (см. `deploy/README-dev.md`).
### Тесты ### Тесты
```bash ```bash

View File

@@ -19,13 +19,13 @@ git clone <адрес репозитория> /opt/schedule-dev
SCHEDULE_LINK=https://disk.yandex.ru/i/... SCHEDULE_LINK=https://disk.yandex.ru/i/...
SCHEDULE_INTERVAL_MINUTES=15 SCHEDULE_INTERVAL_MINUTES=15
SCHEDULE_RETRY_AFTER=60 SCHEDULE_RETRY_AFTER=60
API_PORT=127.0.0.1:8001 API_PORT=8001
FRONTEND_PORT=127.0.0.1:8091 FRONTEND_PORT=8091
``` ```
Порты 8001/8091 выбраны, чтобы не пересечься с основным стендом (8000/8090). Порты 8001/8091 выбраны, чтобы не пересечься с основным стендом (8000/8090).
Привязка к `127.0.0.1` — чтобы API и фронт не смотрели в интернет напрямую, В docker-compose.yml оба порта публикуются на `127.0.0.1` — API и фронт не
наружу их отдаёт nginx. смотрят в интернет напрямую, наружу их отдаёт nginx.
## 3. Запуск ## 3. Запуск
@@ -37,8 +37,16 @@ sudo docker compose ps
## 4. nginx и сертификат ## 4. nginx и сертификат
В `deploy/nginx-dev.conf` порт `proxy_pass` должен совпадать с Конфиг генерирует `deploy/nginx-site.sh`: порт фронта возьмёт из `.env`,
`FRONTEND_PORT` из `.env` (по умолчанию 8091). сертификат — родительского домена (wildcard), затем включит сайт, проверит
`nginx -t` и перечитает nginx:
```bash
sudo deploy/nginx-site.sh --domain dev.schedule.our-place.ru --cert our-place.ru
```
Если у родительского домена нет wildcard-сертификата — выпустите свой
(конфиг-заготовка для ручного пути: `deploy/nginx-dev.conf`):
```bash ```bash
sudo cp deploy/nginx-dev.conf /etc/nginx/sites-available/dev.schedule.our-place.ru sudo cp deploy/nginx-dev.conf /etc/nginx/sites-available/dev.schedule.our-place.ru

120
deploy/nginx-site.sh Executable file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Генерирует и включает конфиг nginx для сайта расписания.
#
# SSL обеспечивает сертификат родительского домена (обычно wildcard *.example.ru):
# отдельный certbot для поддомена не нужен — достаточно имени сертификата.
# Список имён: sudo ls /etc/letsencrypt/live/
#
# Имя сертификата берётся из --cert, иначе — родительский домен от --domain
# (schedule.example.ru -> example.ru). Порт фронта берётся из --port, иначе из
# FRONTEND_PORT файла .env в корне проекта, иначе 8090.
#
# Примеры:
# deploy/nginx-site.sh --print # показать конфиг, ничего не менять
# sudo deploy/nginx-site.sh # поставить и перечитать nginx
# sudo deploy/nginx-site.sh --domain sched.example.ru --cert example.ru
set -euo pipefail
usage() {
grep '^#' "$0" | sed 's/^# \{0,1\}//' | tail -n +2
}
DOMAIN="schedule.our-place.ru"
CERT_NAME=""
FRONTEND_PORT=""
PRINT=0
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) DOMAIN="$2"; shift 2 ;;
--cert) CERT_NAME="$2"; shift 2 ;;
--port) FRONTEND_PORT="$2"; shift 2 ;;
--print) PRINT=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Неизвестный аргумент: $1 (см. --help)" >&2; exit 2 ;;
esac
done
[[ $DOMAIN =~ ^[a-zA-Z0-9.-]+$ ]] || { echo "Кривое имя домена: $DOMAIN" >&2; exit 2; }
if [[ -z $CERT_NAME ]]; then
CERT_NAME="${DOMAIN#*.}" # schedule.our-place.ru -> our-place.ru (wildcard родителя)
fi
[[ $CERT_NAME =~ ^[a-zA-Z0-9.-]+$ ]] || { echo "Кривое имя сертификата: $CERT_NAME" >&2; exit 2; }
if [[ -z $FRONTEND_PORT ]]; then
# FRONTEND_PORT из .env; значение вида «127.0.0.1:8090» трактуем как «порт после двоеточия»
FRONTEND_PORT=$(
grep -E '^FRONTEND_PORT=' .env 2>/dev/null | tail -1 | cut -d= -f2- \
| tr -d ' "' | awk -F: '{print $NF}'
) || FRONTEND_PORT=""
fi
FRONTEND_PORT=${FRONTEND_PORT:-8090}
[[ $FRONTEND_PORT =~ ^[0-9]+$ ]] || { echo "Не разобрал FRONTEND_PORT из .env, задайте --port" >&2; exit 2; }
read -r -d '' CONFIG <<'TEMPLATE' || true
# Сгенерировано deploy/nginx-site.sh — правки вносить в скрипт, не сюда.
server {
listen 80;
listen [::]:80;
server_name __DOMAIN__;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name __DOMAIN__;
# сертификат родительского домена (обычно wildcard), обновляет certbot родителя
ssl_certificate /etc/letsencrypt/live/__CERT__/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/__CERT__/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 8m;
location / {
# порт FRONTEND_PORT из .env стенда
proxy_pass http://127.0.0.1:__PORT__;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# холодный кэш: бэкенд качает таблицу, до ~2 минут
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
TEMPLATE
CONFIG=${CONFIG//__DOMAIN__/$DOMAIN}
CONFIG=${CONFIG//__CERT__/$CERT_NAME}
CONFIG=${CONFIG//__PORT__/$FRONTEND_PORT}
if [[ $PRINT == 1 ]]; then
printf '%s\n' "$CONFIG"
exit 0
fi
if [[ $EUID -ne 0 ]]; then
echo "Установка требует root: sudo deploy/nginx-site.sh (посмотреть конфиг — deploy/nginx-site.sh --print)" >&2
exit 1
fi
DEST="/etc/nginx/sites-available/${DOMAIN}"
printf '%s\n' "$CONFIG" > "$DEST"
ln -sf "$DEST" "/etc/nginx/sites-enabled/${DOMAIN}"
if nginx -t 2>&1; then
systemctl reload nginx
echo "Готово: https://${DOMAIN} -> 127.0.0.1:${FRONTEND_PORT} (сертификат ${CERT_NAME})"
else
echo "nginx -t не прошёл: конфиг записан в ${DEST} и включён, но nginx НЕ перечитан. Разберитесь и выполните: sudo nginx -t && sudo systemctl reload nginx" >&2
exit 1
fi

View File

@@ -27,7 +27,8 @@ services:
# внутри compose api доступен по имени сервиса; менять не нужно # внутри compose api доступен по имени сервиса; менять не нужно
SCHEDULE_API: ${SCHEDULE_API:-http://api:8000} SCHEDULE_API: ${SCHEDULE_API:-http://api:8000}
ports: ports:
- "${FRONTEND_PORT:-8080}:8080" # фронт обслуживает nginx; наружу напрямую не смотрит
- "127.0.0.1:${FRONTEND_PORT:-8080}:8080"
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
api: api:

View File

@@ -1,3 +1,2 @@
-r requirements.txt
pytest==9.1.1 pytest==9.1.1
httpx2==2.12.0 httpx2==2.12.0