feat(docker): многоступенчатая сборка с Nuitka и офлайн-режим

api и worker компилируются Nuitka (--mode=standalone) в builder-стадии; в финальном
образе — только скомпилированный бинарник и системные библиотеки, без исходников,
pip и build-инструментов. api/runtime — debian:bookworm-slim без Python вообще;
worker/runtime — python:3.12-slim-bookworm, но Python там нужен только на шаг
установки Chrome. db.cli объединён с сервером в один бинарник (entry_api.bin dbcli ...).

Заодно убрано 51 неиспользуемый пакет из requirements.txt (camoufox, PySide6, moz*-
стек и др., подтверждено графом зависимостей) — он давал больше веса, чем сама
компиляция. make build offline=1 ставит python-зависимости из wheels/ без PyPI.

make test-browser адаптирован под новую сборку: pytest не может подключиться к
internals скомпилированного бинарника, поэтому гоняется против отдельной стадии
test (Chrome из runtime + исходники как обычный интерпретируемый Python).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
s0k0landClaude Sonnet 5 committed 2026-11-06 13:52:28 +03:00
1 parent f21483489a
commit 0a4da80ca9
12 files changed
+218 -100

No files matched your search

+19 -13
View File
@@ -4,6 +4,7 @@ PIP := $(PYTHON) -m pip
WHEELS_DIR := wheels
REQUIREMENTS := requirements.txt
ver ?= latest
offline ?= 0
MAKECMDGOALS_TARGETS := install freeze wheels sync sync-offline clean help build release sync-dev test test-browser up down logs smoke key-create key-list key-revoke stats push dev run dev-deps dev-key
PKG := $(filter-out $(MAKECMDGOALS_TARGETS),$(MAKECMDGOALS))
@@ -41,10 +42,11 @@ help:
@echo "make sync - установить все пакеты из $(REQUIREMENTS) (из индекса)"
@echo "make sync-offline - установить все пакеты из $(REQUIREMENTS) только из $(WHEELS_DIR)/, без обращения к индексу"
@echo "make freeze - перезаписать $(REQUIREMENTS) текущим состоянием окружения"
@echo "make wheels - собрать wheel для всех пакетов из $(REQUIREMENTS)"
@echo "make wheels - собрать wheel-пакеты для requirements*.txt (api, worker, nuitka) в $(WHEELS_DIR)/"
@echo "make clean - удалить $(WHEELS_DIR)/"
@echo "make build [ver=latest] - собрать образы $(API_IMAGE):<ver> и $(WORKER_IMAGE):<ver>"
@echo "make push ver=<ver> - собрать образы и отправить в $(REGISTRY)"
@echo "make build [ver=latest] [offline=1] - собрать образы $(API_IMAGE):<ver> и $(WORKER_IMAGE):<ver>;"
@echo " offline=1 — ставить python-зависимости только из $(WHEELS_DIR)/ (make wheels), без PyPI"
@echo "make push ver=<ver> [offline=1] - собрать образы и отправить в $(REGISTRY)"
@echo "make release ver=<ver> [out=registry|file] - собрать образы и пакет для деплоя в build/<ver>/ (compose, .env);"
@echo " образы: registry — push в $(REGISTRY) (по умолчанию), file — архив .tar.gz"
@echo "make up - собрать и запустить локально: api, worker, rabbitmq, redis"
@@ -87,14 +89,14 @@ freeze: venv
wheels: venv
@mkdir -p $(WHEELS_DIR)
$(PIP) wheel -r $(REQUIREMENTS) -w $(WHEELS_DIR)
$(PIP) wheel -r $(REQUIREMENTS) -r requirements-api.txt -r requirements-build.txt -w $(WHEELS_DIR)
clean:
rm -rf $(WHEELS_DIR)
build:
docker build -f Dockerfile.api -t $(API_IMAGE):$(ver) .
docker build -t $(WORKER_IMAGE):$(ver) .
docker build --build-arg OFFLINE=$(offline) -f Dockerfile.api -t $(API_IMAGE):$(ver) .
docker build --build-arg OFFLINE=$(offline) --target=runtime -t $(WORKER_IMAGE):$(ver) .
push: build
for img in $(API_IMAGE) $(WORKER_IMAGE); do \
@@ -134,17 +136,17 @@ smoke:
$(PYTHON) tests/smoke.py $(API_URL) $(API_KEY)
key-create:
docker compose exec api python -m db.cli create "$(name)"
docker compose exec api ./entry_api.bin dbcli create "$(name)"
key-list:
docker compose exec api python -m db.cli list
docker compose exec api ./entry_api.bin dbcli list
key-revoke:
docker compose exec api python -m db.cli revoke $(id)
docker compose exec api ./entry_api.bin dbcli revoke $(id)
days ?= 7
stats:
docker compose exec api python -m db.cli stats --days $(days)
docker compose exec api ./entry_api.bin dbcli stats --days $(days)
sync-dev: venv
$(PIP) install -r requirements-dev.txt
@@ -157,11 +159,15 @@ client-build: venv
$(PIP) wheel ./client -w client/dist --no-deps
@echo "Готово: client/dist/ — pip install client/dist/robotex-*.whl"
test-browser: build
# entry_worker.bin — скомпилированный Nuitka-бинарник, pytest не может подключиться к его internals
# напрямую (tests/harness.py импортирует api/worker как обычные модули), поэтому тесты идут не против
# $(WORKER_IMAGE), а против стадии test — тот же Chrome/Xvfb, но исходники как интерпретируемый Python
test-browser:
docker build --build-arg OFFLINE=$(offline) --target=test -t $(WORKER_IMAGE)-test:$(ver) .
docker run --rm --shm-size=2g \
-v $(CURDIR)/tests:/app/tests:ro -v $(CURDIR)/pytest.ini:/app/pytest.ini:ro \
-v $(CURDIR)/requirements-dev.txt:/app/requirements-dev.txt:ro -v $(CURDIR)/client/src:/app/client/src:ro \
-w /app $(WORKER_IMAGE):$(ver) sh -c 'pip install -q --user -r requirements-dev.txt && \
-w /app $(WORKER_IMAGE)-test:$(ver) sh -c 'pip install -q --user -r requirements-dev.txt && \
xvfb-run -a --server-args="-screen 0 1920x1080x24" python -m pytest -m browser -p no:cacheprovider -s'
RELEASE_DIR := build/$(ver)
@@ -206,7 +212,7 @@ endif
'```' \
'' \
'Настройки (порт, слоты, CPU и память воркера, пароли RabbitMQ и Postgres) — в .env.' \
'Ключ клиенту: docker compose exec api python -m db.cli create <имя>' > $(RELEASE_DIR)/README.md
'Ключ клиенту: docker compose exec api ./entry_api.bin dbcli create <имя>' > $(RELEASE_DIR)/README.md
@echo "Готово: $(RELEASE_DIR)/"
# позволяет передавать имя пакета как аргумент: make install redis