diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..99e4136 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.venv +.env +.pytest_cache +.claude +docs +tests +scripts +**/__pycache__ +**/*.egg-info diff --git a/.env.example b/.env.example index 277af00..2bd9a23 100644 --- a/.env.example +++ b/.env.example @@ -2,5 +2,8 @@ MPU_LOGIN= MPU_PASSWORD= -# Подключение к PostgreSQL DATABASE_URL= + +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_DB= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8d53714 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.14-slim + +ENV PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +COPY pyproject.toml ./ +RUN python -c "import tomllib; [print(d) for d in tomllib.load(open('pyproject.toml', 'rb'))['project']['dependencies']]" \ + | pip install -r /dev/stdin + +COPY README.md alembic.ini ./ +COPY src ./src +COPY alembic ./alembic +RUN pip install --no-deps . + +RUN useradd --create-home --uid 1000 mcp +USER mcp + +EXPOSE 8000 + +CMD ["sh", "-c", "alembic upgrade head && exec python -m mospolytech_mcp.server --http --host 0.0.0.0 --port 8000"] diff --git a/Makefile b/Makefile index cb20eec..93eeda5 100644 --- a/Makefile +++ b/Makefile @@ -15,10 +15,10 @@ HOST ?= 0.0.0.0 PORT ?= 8000 .DEFAULT_GOAL := help -.PHONY: help up setup install check-env migrate run run-http test check-lk clean +.PHONY: help up setup install check-env migrate run run-http test check-lk clean docker-up docker-down docker-logs help: - @$(SYS_PYTHON) -X utf8 -c "import re, sys; [print(' make {:<10} {}'.format(*m.groups())) for m in re.finditer(r'^([a-z-]+):.*?## (.*)', open(sys.argv[1], encoding='utf-8').read(), re.M)]" $(firstword $(MAKEFILE_LIST)) + @$(SYS_PYTHON) -X utf8 -c "import re, sys; [print(' make {:<12} {}'.format(*m.groups())) for m in re.finditer(r'^([a-z-]+):.*?## (.*)', open(sys.argv[1], encoding='utf-8').read(), re.M)]" $(firstword $(MAKEFILE_LIST)) up: migrate run ## всё сразу: окружение, .env, миграции, сервер @@ -57,5 +57,14 @@ test: install ## офлайн-тесты check-lk: install .env ## проверка lk_api на реальном аккаунте $(PY) scripts/check_lk_api.py +docker-up: + docker compose up --build -d + +docker-down: + docker compose down + +docker-logs: + docker compose logs -f + clean: ## удалить .venv и кэши $(SYS_PYTHON) -c "import shutil; [shutil.rmtree(p, ignore_errors=True) for p in ('.venv', '.pytest_cache')]" diff --git a/README.md b/README.md index f7f6c80..cc43c7a 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,44 @@ Connect — в списке инструментов появится `list_grou прямо оттуда. Авторизации на сервере нет, так что наружу (`0.0.0.0`) открывать только на тестовой машине. +## Docker + +Тот же сервер, но в контейнере и со своей PostgreSQL рядом — так он живёт +на ВМ начиная с ЛР3. Нужны Docker и docker compose v2, `.env` и venv не +нужны: + +```bash +make docker-up +make docker-logs +make docker-down +``` + +Что происходит: + +- `Dockerfile` собирает образ из исходников: `python:3.14-slim`, отдельным + слоем зависимости из `pyproject.toml`, потом сам пакет. При старте + контейнер накатывает миграции (`alembic upgrade head`) и запускает сервер + в HTTP-режиме на `0.0.0.0:8000` — снаружи это `http://<адрес>:8000/mcp`. +- `docker-compose.yaml` поднимает два сервиса: `db` (`postgres:17-alpine`) + и `mcp` (наш образ). Сервер стартует только после того, как БД ответила на + `pg_isready`, и ходит к ней по имени `db` внутри сети compose — наружу + порт PostgreSQL не выставлен. `DATABASE_URL` для контейнера compose + собирает сам из `POSTGRES_*`; `.env` в образ не попадает (`.dockerignore`). +- Файлы БД лежат в volume `pgdata`, поэтому `docker compose down` и + повторный `up` данные не теряют. Снести вместе с данными — + `docker compose down -v`. + +Логин, пароль и имя БД по умолчанию `mospolytech_mcp`; переопределяются через +`POSTGRES_USER`/`POSTGRES_PASSWORD`/`POSTGRES_DB` в `.env` — compose читает +его сам, а `DATABASE_URL` оттуда для контейнера не используется. + +Посмотреть, что кэш реально лёг в контейнерную БД (это же удобно показывать +после рестарта): + +```bash +docker compose exec db psql -U mospolytech_mcp -c "select id, jsonb_array_length(groups), fetched_at from groups_cache" +``` + ## Структура репозитория ``` @@ -166,4 +204,6 @@ src/mospolytech_mcp/ alembic/ — миграции БД (alembic upgrade head накатывает схему) tests/ — офлайн-тесты (pytest, без сети) scripts/ — ручные проверочные скрипты (против реального ЛК) +Dockerfile — образ сервера (сборка из исходников, миграции + HTTP-режим на старте) +docker-compose.yaml — сервер + PostgreSQL с volume для данных ``` diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..63bb7d5 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,34 @@ +services: + db: + image: postgres:17-alpine + container_name: mospolytech-db + hostname: db + environment: + POSTGRES_DB: ${POSTGRES_DB:-mospolytech_mcp} + POSTGRES_USER: ${POSTGRES_USER:-mospolytech_mcp} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mospolytech_mcp} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 3s + retries: 10 + restart: unless-stopped + + mcp: + build: . + image: mospolytech-mcp + container_name: mospolytech-mcp + hostname: mcp + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-mospolytech_mcp}:${POSTGRES_PASSWORD:-mospolytech_mcp}@db:5432/${POSTGRES_DB:-mospolytech_mcp} + ports: + - "8000:8000" + depends_on: + db: + condition: service_healthy + restart: unless-stopped + +volumes: + pgdata: