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:
1 parent
f21483489a
commit
0a4da80ca9
12 files changed
+218
-100
No files matched your search
+6
-1
@@ -1,10 +1,15 @@
|
|||||||
.git
|
.git
|
||||||
.idea
|
.idea
|
||||||
.venv
|
.venv
|
||||||
wheels
|
|
||||||
extra
|
extra
|
||||||
docs
|
docs
|
||||||
tests
|
tests
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
|
# артефакты Nuitka при локальных прогонах (вне Docker)
|
||||||
|
*.build
|
||||||
|
*.dist
|
||||||
|
*.onefile-build
|
||||||
|
.nuitka-cache
|
||||||
|
# wheels/ НЕ в игноре: нужен в контексте сборки для OFFLINE=1 (make wheels)
|
||||||
+2
-1
@@ -2,7 +2,8 @@ extra/
|
|||||||
.venv/
|
.venv/
|
||||||
.idea/
|
.idea/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
wheels/
|
wheels/*
|
||||||
|
!wheels/.gitkeep
|
||||||
docs/*.bkp
|
docs/*.bkp
|
||||||
docs/*.dtmp
|
docs/*.dtmp
|
||||||
build/
|
build/
|
||||||
|
|||||||
+74
-15
@@ -1,39 +1,98 @@
|
|||||||
# Сервис worker (#78): выполняет задачи из очереди в Google Chrome под Xvfb.
|
# Сервис worker (#78): выполняет задачи из очереди в Google Chrome под Xvfb.
|
||||||
FROM python:3.12-slim-bookworm
|
#
|
||||||
|
# builder: компилирует src/ (через entry_worker.py) в самодостаточный standalone-бинарник Nuitka —
|
||||||
|
# в финальный образ не попадают ни исходники .py, ни gcc/pip, ни сам requirements.txt.
|
||||||
|
FROM python:3.12-slim-bookworm AS builder
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
PIP_NO_CACHE_DIR=1 \
|
PIP_NO_CACHE_DIR=1 \
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
# build-essential (gcc + libc6-dev — заголовки Python.h тянут stdlib.h/limits.h) — компилятор для
|
||||||
|
# сгенерированного Nuitka C-кода; patchelf — правит RPATH бинарника в standalone-режиме;
|
||||||
|
# ccache — ускоряет повторные сборки при пересборке образа
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends build-essential patchelf ccache \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# OFFLINE=1: ставить зависимости только из wheels/ (make wheels), без обращения к PyPI — см. docs/Deployment.md
|
||||||
|
ARG OFFLINE=0
|
||||||
|
WORKDIR /build
|
||||||
|
COPY requirements.txt requirements-build.txt ./
|
||||||
|
COPY wheels/ ./wheels/
|
||||||
|
RUN set -e; \
|
||||||
|
if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=./wheels"; else PIP_OPTS=""; fi; \
|
||||||
|
pip install $PIP_OPTS -r requirements.txt -r requirements-build.txt
|
||||||
|
|
||||||
|
COPY src/ ./src/
|
||||||
|
|
||||||
|
# standalone: entry_worker.dist/ — изолированная папка со своим Python-рантаймом и всеми пакетами, которые
|
||||||
|
# реально нужны воркеру (patchright, playwright, browserforge, dramatiq, db, tasks, engine, antibot, captcha,
|
||||||
|
# полный api/ — см. worker/windows.py). worker.actors попадает в сборку только из-за явного import в
|
||||||
|
# entry_worker.py: dramatiq грузит его по строке "worker.actors" динамически, статика Nuitka это не видит
|
||||||
|
# --include-package-data: Nuitka копирует только код, а patchright/browserforge везут с собой
|
||||||
|
# небольшой код + большие данные не-.py (драйвер patchright — Node.js бинарник и JS, browserforge и
|
||||||
|
# apify_fingerprint_datapoints — zip/json датасеты отпечатков), без них падают с FileNotFoundError в runtime
|
||||||
|
RUN python -m nuitka --mode=standalone \
|
||||||
|
--include-package=worker --include-package=engine --include-package=antibot --include-package=captcha \
|
||||||
|
--include-package=db --include-package=tasks --include-package=api \
|
||||||
|
--include-package-data=patchright --include-package-data=browserforge \
|
||||||
|
--include-package-data=apify_fingerprint_datapoints \
|
||||||
|
--include-data-files=src/api/js/canvas_noise.js=api/js/canvas_noise.js \
|
||||||
|
--output-dir=/build/dist \
|
||||||
|
src/entry_worker.py
|
||||||
|
|
||||||
|
# runtime: Python тут остаётся — он нужен только на один шаг (установка Chrome через `patchright install`,
|
||||||
|
# тот же механизм, что и раньше), а дальше запускается готовый standalone-бинарник, а не исходники из
|
||||||
|
# requirements.txt. Полный интерпретатор здесь — осознанный компромисс: переносить apt-зависимости Chrome
|
||||||
|
# между стадиями вручную хрупко (patchright сам решает, что ставить под конкретную версию Chrome).
|
||||||
|
FROM python:3.12-slim-bookworm AS runtime
|
||||||
|
|
||||||
|
ENV PIP_NO_CACHE_DIR=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
# Xvfb — браузер запускается в headful-режиме (headless=False), нужен виртуальный дисплей
|
# Xvfb — браузер запускается в headful-режиме (headless=False), нужен виртуальный дисплей
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends tini xvfb xauth fonts-liberation fonts-noto-color-emoji ca-certificates \
|
&& apt-get install -y --no-install-recommends tini xvfb xauth fonts-liberation fonts-noto-color-emoji ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
# real_chrome=True → channel "chrome": ставим Google Chrome stable вместе с системными зависимостями.
|
||||||
|
# Версия patchright — как в requirements.txt (#109): её CLI и сам пакет worker'у в runtime не нужны,
|
||||||
COPY requirements.txt .
|
# только Chrome и его системные библиотеки, которые он поставит через apt
|
||||||
RUN pip install -r requirements.txt
|
ARG OFFLINE=0
|
||||||
|
COPY wheels/ /tmp/wheels/
|
||||||
# real_chrome=True → channel "chrome": ставим Google Chrome stable вместе с системными зависимостями
|
RUN set -e; \
|
||||||
RUN patchright install --with-deps chrome \
|
if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=/tmp/wheels"; else PIP_OPTS=""; fi; \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
pip install $PIP_OPTS patchright==1.63.0 \
|
||||||
|
&& patchright install --with-deps chrome \
|
||||||
|
&& pip uninstall -y patchright \
|
||||||
|
&& rm -rf /tmp/wheels /var/lib/apt/lists/* /root/.cache
|
||||||
|
|
||||||
RUN useradd --create-home --uid 1000 app \
|
RUN useradd --create-home --uid 1000 app \
|
||||||
&& mkdir -p /app/extra \
|
&& mkdir -p /app/extra \
|
||||||
&& chown -R app:app /app
|
&& chown -R app:app /app
|
||||||
|
|
||||||
COPY --chown=app:app src/ ./src/
|
WORKDIR /app
|
||||||
|
COPY --from=builder --chown=app:app /build/dist/entry_worker.dist/ ./
|
||||||
|
|
||||||
USER app
|
USER app
|
||||||
WORKDIR /app/src
|
|
||||||
|
|
||||||
# профили браузера (extra/user_data_dir) и geoip-кэш — в volume
|
# профили браузера (extra/user_data_dir) и geoip-кэш — в volume
|
||||||
VOLUME ["/app/extra"]
|
VOLUME ["/app/extra"]
|
||||||
|
|
||||||
# tini как PID 1: пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb (иначе — зомби)
|
# tini как PID 1: пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb (иначе — зомби)
|
||||||
ENTRYPOINT ["/usr/bin/tini", "-g", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "-g", "--"]
|
||||||
# один процесс: открытые окна браузера живут в его памяти; потоков — с запасом над слотами, чтобы
|
# BROWSER_SLOTS/WORKER_THREADS читает сам entry_worker.bin (--processes 1, потоков — вдвое больше слотов)
|
||||||
# задачи в уже открытые окна не ждали, пока потоки заняты задачами без свободного слота
|
CMD ["sh", "-c", "exec xvfb-run -a --server-args='-screen 0 1920x1080x24' ./entry_worker.bin"]
|
||||||
CMD ["sh", "-c", "exec xvfb-run -a --server-args='-screen 0 1920x1080x24' dramatiq worker.actors --processes 1 --threads ${WORKER_THREADS:-$((${BROWSER_SLOTS:-4} * 2))}"]
|
|
||||||
|
# test (make test-browser; не публикуется, не используется по умолчанию — docker build должен указывать
|
||||||
|
# --target=runtime): entry_worker.bin — скомпилированный бинарник, pytest/tests/harness.py не может
|
||||||
|
# подключиться к его internals напрямую. Этой стадии нужны тот же Chrome/Xvfb из runtime и исходники
|
||||||
|
# как обычный интерпретируемый Python рядом с бинарником — они не конфликтуют (разные пути)
|
||||||
|
FROM runtime AS test
|
||||||
|
USER root
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY --chown=app:app src/ ./src/
|
||||||
|
USER app
|
||||||
|
WORKDIR /app
|
||||||
+45
-11
@@ -1,24 +1,58 @@
|
|||||||
# Сервис api (#78): принимает задачи и отдаёт результаты. Без Chrome — лёгкий образ.
|
# Сервис api (#78): принимает задачи и отдаёт результаты. Без Chrome — лёгкий образ.
|
||||||
FROM python:3.12-slim-bookworm
|
#
|
||||||
|
# builder: компилирует src/ (через entry_api.py) в самодостаточный standalone-бинарник Nuitka —
|
||||||
|
# в финальный образ не попадают ни исходники .py, ни gcc/pip, ни сам requirements-api.txt.
|
||||||
|
FROM python:3.12-slim-bookworm AS builder
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
PIP_NO_CACHE_DIR=1 \
|
PIP_NO_CACHE_DIR=1 \
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
WORKDIR /app
|
# build-essential (gcc + libc6-dev — заголовки Python.h тянут stdlib.h/limits.h) — компилятор для
|
||||||
|
# сгенерированного Nuitka C-кода; patchelf — правит RPATH бинарника в standalone-режиме;
|
||||||
|
# ccache — ускоряет повторные сборки при пересборке образа
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends build-essential patchelf ccache \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY requirements-api.txt .
|
# OFFLINE=1: ставить зависимости только из wheels/ (make wheels), без обращения к PyPI — см. docs/Deployment.md
|
||||||
RUN pip install -r requirements-api.txt \
|
ARG OFFLINE=0
|
||||||
|
WORKDIR /build
|
||||||
|
COPY requirements-api.txt requirements-build.txt ./
|
||||||
|
COPY wheels/ ./wheels/
|
||||||
|
RUN set -e; \
|
||||||
|
if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=./wheels"; else PIP_OPTS=""; fi; \
|
||||||
|
pip install $PIP_OPTS -r requirements-api.txt -r requirements-build.txt
|
||||||
|
|
||||||
|
COPY src/main.py src/entry_api.py ./src/
|
||||||
|
COPY src/tasks/ ./src/tasks/
|
||||||
|
COPY src/db/ ./src/db/
|
||||||
|
COPY src/api/__init__.py src/api/schemas.py ./src/api/
|
||||||
|
|
||||||
|
# standalone: entry_api.dist/ — изолированная папка со своим Python-рантаймом и всеми пакетами, которые
|
||||||
|
# реально нужны entry_api.py (uvicorn, fastapi, db, tasks, api.schemas); world.actors/api.actions сюда не
|
||||||
|
# попадают — entry_api.py их не импортирует. --include-package на первом месте — на случай отложенного
|
||||||
|
# импорта, который статический анализ Nuitka мог не увидеть
|
||||||
|
RUN python -m nuitka --mode=standalone \
|
||||||
|
--include-package=api --include-package=db --include-package=tasks \
|
||||||
|
--output-dir=/build/dist \
|
||||||
|
src/entry_api.py
|
||||||
|
|
||||||
|
# runtime: без Python, pip и исходников — только то, что нужно скомпилированному бинарнику
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
|
# libpq5 — asyncpg/SQLAlchemy ходят в Postgres через системную libpq, Nuitka её не компилирует, а
|
||||||
|
# подключает динамически
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends libpq5 ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& useradd --create-home --uid 1000 app
|
&& useradd --create-home --uid 1000 app
|
||||||
|
|
||||||
COPY --chown=app:app src/main.py ./src/
|
WORKDIR /app
|
||||||
COPY --chown=app:app src/tasks/ ./src/tasks/
|
COPY --from=builder --chown=app:app /build/dist/entry_api.dist/ ./
|
||||||
COPY --chown=app:app src/db/ ./src/db/
|
|
||||||
COPY --chown=app:app src/api/__init__.py src/api/schemas.py ./src/api/
|
|
||||||
|
|
||||||
USER app
|
USER app
|
||||||
WORKDIR /app/src
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
# dbcli — админ-CLI клиентов и ключей (ранее `python -m db.cli`): docker compose exec api ./entry_api.bin dbcli ...
|
||||||
|
CMD ["./entry_api.bin"]
|
||||||
@@ -4,6 +4,7 @@ PIP := $(PYTHON) -m pip
|
|||||||
WHEELS_DIR := wheels
|
WHEELS_DIR := wheels
|
||||||
REQUIREMENTS := requirements.txt
|
REQUIREMENTS := requirements.txt
|
||||||
ver ?= latest
|
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
|
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))
|
PKG := $(filter-out $(MAKECMDGOALS_TARGETS),$(MAKECMDGOALS))
|
||||||
@@ -41,10 +42,11 @@ help:
|
|||||||
@echo "make sync - установить все пакеты из $(REQUIREMENTS) (из индекса)"
|
@echo "make sync - установить все пакеты из $(REQUIREMENTS) (из индекса)"
|
||||||
@echo "make sync-offline - установить все пакеты из $(REQUIREMENTS) только из $(WHEELS_DIR)/, без обращения к индексу"
|
@echo "make sync-offline - установить все пакеты из $(REQUIREMENTS) только из $(WHEELS_DIR)/, без обращения к индексу"
|
||||||
@echo "make freeze - перезаписать $(REQUIREMENTS) текущим состоянием окружения"
|
@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 clean - удалить $(WHEELS_DIR)/"
|
||||||
@echo "make build [ver=latest] - собрать образы $(API_IMAGE):<ver> и $(WORKER_IMAGE):<ver>"
|
@echo "make build [ver=latest] [offline=1] - собрать образы $(API_IMAGE):<ver> и $(WORKER_IMAGE):<ver>;"
|
||||||
@echo "make push ver=<ver> - собрать образы и отправить в $(REGISTRY)"
|
@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 "make release ver=<ver> [out=registry|file] - собрать образы и пакет для деплоя в build/<ver>/ (compose, .env);"
|
||||||
@echo " образы: registry — push в $(REGISTRY) (по умолчанию), file — архив .tar.gz"
|
@echo " образы: registry — push в $(REGISTRY) (по умолчанию), file — архив .tar.gz"
|
||||||
@echo "make up - собрать и запустить локально: api, worker, rabbitmq, redis"
|
@echo "make up - собрать и запустить локально: api, worker, rabbitmq, redis"
|
||||||
@@ -87,14 +89,14 @@ freeze: venv
|
|||||||
|
|
||||||
wheels: venv
|
wheels: venv
|
||||||
@mkdir -p $(WHEELS_DIR)
|
@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:
|
clean:
|
||||||
rm -rf $(WHEELS_DIR)
|
rm -rf $(WHEELS_DIR)
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker build -f Dockerfile.api -t $(API_IMAGE):$(ver) .
|
docker build --build-arg OFFLINE=$(offline) -f Dockerfile.api -t $(API_IMAGE):$(ver) .
|
||||||
docker build -t $(WORKER_IMAGE):$(ver) .
|
docker build --build-arg OFFLINE=$(offline) --target=runtime -t $(WORKER_IMAGE):$(ver) .
|
||||||
|
|
||||||
push: build
|
push: build
|
||||||
for img in $(API_IMAGE) $(WORKER_IMAGE); do \
|
for img in $(API_IMAGE) $(WORKER_IMAGE); do \
|
||||||
@@ -134,17 +136,17 @@ smoke:
|
|||||||
$(PYTHON) tests/smoke.py $(API_URL) $(API_KEY)
|
$(PYTHON) tests/smoke.py $(API_URL) $(API_KEY)
|
||||||
|
|
||||||
key-create:
|
key-create:
|
||||||
docker compose exec api python -m db.cli create "$(name)"
|
docker compose exec api ./entry_api.bin dbcli create "$(name)"
|
||||||
|
|
||||||
key-list:
|
key-list:
|
||||||
docker compose exec api python -m db.cli list
|
docker compose exec api ./entry_api.bin dbcli list
|
||||||
|
|
||||||
key-revoke:
|
key-revoke:
|
||||||
docker compose exec api python -m db.cli revoke $(id)
|
docker compose exec api ./entry_api.bin dbcli revoke $(id)
|
||||||
|
|
||||||
days ?= 7
|
days ?= 7
|
||||||
stats:
|
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
|
sync-dev: venv
|
||||||
$(PIP) install -r requirements-dev.txt
|
$(PIP) install -r requirements-dev.txt
|
||||||
@@ -157,11 +159,15 @@ client-build: venv
|
|||||||
$(PIP) wheel ./client -w client/dist --no-deps
|
$(PIP) wheel ./client -w client/dist --no-deps
|
||||||
@echo "Готово: client/dist/ — pip install client/dist/robotex-*.whl"
|
@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 \
|
docker run --rm --shm-size=2g \
|
||||||
-v $(CURDIR)/tests:/app/tests:ro -v $(CURDIR)/pytest.ini:/app/pytest.ini:ro \
|
-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 \
|
-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'
|
xvfb-run -a --server-args="-screen 0 1920x1080x24" python -m pytest -m browser -p no:cacheprovider -s'
|
||||||
|
|
||||||
RELEASE_DIR := build/$(ver)
|
RELEASE_DIR := build/$(ver)
|
||||||
@@ -206,7 +212,7 @@ endif
|
|||||||
'```' \
|
'```' \
|
||||||
'' \
|
'' \
|
||||||
'Настройки (порт, слоты, CPU и память воркера, пароли RabbitMQ и Postgres) — в .env.' \
|
'Настройки (порт, слоты, 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)/"
|
@echo "Готово: $(RELEASE_DIR)/"
|
||||||
|
|
||||||
# позволяет передавать имя пакета как аргумент: make install redis
|
# позволяет передавать имя пакета как аргумент: make install redis
|
||||||
|
|||||||
+7
-1
@@ -10,6 +10,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
|
args:
|
||||||
|
OFFLINE: ${OFFLINE:-0}
|
||||||
image: robotex-api:latest
|
image: robotex-api:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -24,7 +26,11 @@ services:
|
|||||||
postgres: {condition: service_healthy}
|
postgres: {condition: service_healthy}
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
build: .
|
build:
|
||||||
|
context: .
|
||||||
|
target: runtime
|
||||||
|
args:
|
||||||
|
OFFLINE: ${OFFLINE:-0}
|
||||||
image: robotex-worker:latest
|
image: robotex-worker:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
+17
-6
@@ -47,7 +47,7 @@ make key-list # клиенты: начало ключа, акти
|
|||||||
make key-revoke id=3 # отозвать
|
make key-revoke id=3 # отозвать
|
||||||
```
|
```
|
||||||
|
|
||||||
На сервере без make: `docker compose exec api python -m db.cli create acme` (и `list`, `revoke <id>`, `stats`).
|
На сервере без make: `docker compose exec api ./entry_api.bin dbcli create acme` (и `list`, `revoke <id>`, `stats`).
|
||||||
|
|
||||||
## Статистика использования
|
## Статистика использования
|
||||||
|
|
||||||
@@ -70,17 +70,28 @@ make stats days=7
|
|||||||
|
|
||||||
## Что внутри образа worker
|
## Что внутри образа worker
|
||||||
|
|
||||||
|
Сборка двухстадийная (`Dockerfile`): `builder` компилирует `src/` (через `entry_worker.py`) в самодостаточный
|
||||||
|
standalone-бинарник Nuitka, `runtime` берёт из него только готовую папку `entry_worker.dist/` — исходники `.py`,
|
||||||
|
`requirements.txt`, gcc и pip в финальный образ не попадают.
|
||||||
|
|
||||||
| Компонент | Зачем |
|
| Компонент | Зачем |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `python:3.12-slim-bookworm` | базовый образ, Python той же версии, что и в `.venv` |
|
| `entry_worker.bin` (standalone, builder) | скомпилированный Nuitka код `src/` + его Python-зависимости; оборачивает `dramatiq worker.actors --processes 1 --threads N` |
|
||||||
| `requirements.txt` | ставится из PyPI (папка `wheels/` в образ не попадает — она под Ubuntu Noble и неполная) |
|
| `python:3.12-slim-bookworm` (runtime) | базовый образ — Python тут нужен только на шаге установки Chrome (`patchright install`), сам бинарник от него не зависит |
|
||||||
| Google Chrome stable (`patchright install --with-deps chrome`) | сессия запускается с `real_chrome=True` → channel `chrome` |
|
| Google Chrome stable (`patchright install --with-deps chrome`) | сессия запускается с `real_chrome=True` → channel `chrome`; после установки пакет `patchright` из runtime удаляется |
|
||||||
| Xvfb (`xvfb-run`) | браузер работает в headful-режиме (`headless=False`), нужен виртуальный дисплей 1920x1080x24 |
|
| Xvfb (`xvfb-run`) | браузер работает в headful-режиме (`headless=False`), нужен виртуальный дисплей 1920x1080x24 |
|
||||||
| `tini` (ENTRYPOINT, PID 1) | пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb — без него копятся зомби-процессы |
|
| `tini` (ENTRYPOINT, PID 1) | пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb — без него копятся зомби-процессы |
|
||||||
| пользователь `app` (uid 1000) | Chrome не запускается от root без `--no-sandbox` |
|
| пользователь `app` (uid 1000) | Chrome не запускается от root без `--no-sandbox` |
|
||||||
|
|
||||||
Рабочая директория — `/app/src`. Воркер — один процесс dramatiq (`--processes 1`: открытые окна живут в его памяти),
|
`make build offline=1` ставит python-зависимости builder-стадии только из `wheels/` (`make wheels`), без обращения
|
||||||
потоков `WORKER_THREADS` (по умолчанию `2 × BROWSER_SLOTS`).
|
к PyPI — установку самого Chrome (apt + скачивание) это не затрагивает, сеть для неё нужна всегда. Wheels, собранные
|
||||||
|
`pip wheel`, переносимы между дистрибутивами, только если для пакета есть готовый `manylinux`-wheel на PyPI; если
|
||||||
|
для какого-то пакета его нет и pip собирает колесо из исходников на хосте — используйте для `make wheels` машину
|
||||||
|
или контейнер на той же базе, что и образ (`python:3.12-slim-bookworm`), иначе колесо может не завестись в сборке.
|
||||||
|
|
||||||
|
Рабочая директория — `/app`. Воркер — один процесс dramatiq (`--processes 1`: открытые окна живут в его памяти),
|
||||||
|
потоков `WORKER_THREADS` (по умолчанию `2 × BROWSER_SLOTS`) — читает `entry_worker.py` сам, в `CMD` эти флаги больше
|
||||||
|
не передаются.
|
||||||
|
|
||||||
## Данные и volume
|
## Данные и volume
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# зависимости builder-стадии (Dockerfile, Dockerfile.api): компиляция src/ в standalone-бинарник.
|
||||||
|
# В финальный образ не попадает — ставится только в стадии builder.
|
||||||
|
Nuitka==4.3.4
|
||||||
@@ -1,85 +1,33 @@
|
|||||||
2captcha-python==2.1.1
|
2captcha-python==2.1.1
|
||||||
aiofiles==25.1.0
|
aiofiles==25.1.0
|
||||||
aiohappyeyeballs==2.7.1
|
|
||||||
aiohttp==3.14.3
|
|
||||||
aiosignal==1.4.0
|
|
||||||
annotated-doc==0.0.5
|
annotated-doc==0.0.5
|
||||||
annotated-types==0.8.0
|
annotated-types==0.8.0
|
||||||
anyio==4.15.1
|
anyio==4.15.1
|
||||||
apify_fingerprint_datapoints==0.15.0
|
apify_fingerprint_datapoints==0.15.0
|
||||||
asyncpg==0.32.0
|
asyncpg==0.32.0
|
||||||
attrs==26.1.0
|
|
||||||
blessed==1.49.0
|
|
||||||
browserforge==1.2.4
|
browserforge==1.2.4
|
||||||
camoufox==0.5.6
|
|
||||||
certifi==2026.7.22
|
certifi==2026.7.22
|
||||||
charset-normalizer==3.5.1
|
charset-normalizer==3.5.1
|
||||||
click==8.5.0
|
click==8.5.0
|
||||||
distro==1.9.0
|
|
||||||
dramatiq==2.2.1
|
dramatiq==2.2.1
|
||||||
editor==1.8.0
|
|
||||||
fastapi==0.141.1
|
fastapi==0.141.1
|
||||||
frozenlist==1.8.0
|
|
||||||
geoip2==5.3.0
|
|
||||||
greenlet==3.5.5
|
greenlet==3.5.5
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
httpcore==1.0.9
|
httpcore==1.0.9
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
idna==3.19
|
idna==3.19
|
||||||
inquirer==3.4.1
|
|
||||||
jinxed==2.1.0
|
|
||||||
language-tags==1.3.1
|
|
||||||
lxml==6.1.3
|
|
||||||
marionette_driver==3.7.1
|
|
||||||
markdown-it-py==4.2.0
|
|
||||||
maxminddb==3.1.1
|
|
||||||
mdurl==0.1.2
|
|
||||||
mozdevice==4.2.0
|
|
||||||
mozfile==3.0.0
|
|
||||||
mozinfo==1.2.3
|
|
||||||
mozlog==8.1.0
|
|
||||||
mozprocess==1.4.0
|
|
||||||
mozprofile==3.0.0
|
|
||||||
mozrunner==8.4.0
|
|
||||||
mozsystemmonitor==1.0.1
|
|
||||||
mozterm==1.0.0
|
|
||||||
mozversion==2.4.0
|
|
||||||
msgspec==0.21.1
|
msgspec==0.21.1
|
||||||
multidict==6.7.1
|
|
||||||
numpy==2.5.3
|
|
||||||
orjson==3.12.0
|
|
||||||
patchright==1.63.0
|
patchright==1.63.0
|
||||||
pika==1.4.4
|
pika==1.4.4
|
||||||
platformdirs==4.11.7
|
|
||||||
playwright==1.62.0
|
playwright==1.62.0
|
||||||
propcache==0.5.2
|
|
||||||
psutil==7.2.2
|
|
||||||
pydantic==2.13.5
|
pydantic==2.13.5
|
||||||
pydantic_core==2.46.5
|
pydantic_core==2.46.5
|
||||||
pyee==13.0.1
|
pyee==13.0.1
|
||||||
Pygments==2.21.0
|
|
||||||
PySide6==6.11.2
|
|
||||||
PySide6_Addons==6.11.2
|
|
||||||
PySide6_Essentials==6.11.2
|
|
||||||
PySocks==1.7.1
|
|
||||||
PyYAML==6.0.3
|
|
||||||
readchar==4.2.2
|
|
||||||
redis==8.1.0
|
redis==8.1.0
|
||||||
requests==2.34.2
|
requests==2.34.2
|
||||||
rich==15.0.0
|
|
||||||
rich-click==1.9.9
|
|
||||||
runs==1.3.0
|
|
||||||
screeninfo==0.8.1
|
|
||||||
shiboken6==6.11.2
|
|
||||||
six==1.17.0
|
|
||||||
SQLAlchemy==2.1.4
|
SQLAlchemy==2.1.4
|
||||||
starlette==1.6.0
|
starlette==1.6.0
|
||||||
typing_extensions==4.16.0
|
typing_extensions==4.16.0
|
||||||
typing-inspection==0.4.4
|
typing-inspection==0.4.4
|
||||||
ua-parser==1.0.2
|
|
||||||
ua-parser-builtins==202606
|
|
||||||
urllib3==2.7.0
|
urllib3==2.7.0
|
||||||
uvicorn==0.52.4
|
uvicorn==0.52.4
|
||||||
wcwidth==0.8.3
|
|
||||||
xmod==1.10.0
|
|
||||||
yarl==1.24.5
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Единая точка входа для Nuitka-сборки образа api: без аргументов — FastAPI-сервер (uvicorn), с первым
|
||||||
|
аргументом `dbcli` — админ-CLI клиентов и ключей (ранее `python -m db.cli`, см. db/cli.py). Один бинарник
|
||||||
|
вместо двух: standalone-сборка Nuitka не делит общий рантайм между несколькими точками входа.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
import db.cli
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "dbcli":
|
||||||
|
sys.argv = sys.argv[1:]
|
||||||
|
db.cli.main()
|
||||||
|
return
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
Точка входа для Nuitka-сборки образа worker: оборачивает `dramatiq worker.actors --processes 1
|
||||||
|
--threads N` (dramatiq.cli.main() сам разбирает sys.argv, как и обычный CLI). Прямой `import worker.actors`
|
||||||
|
обязателен: dramatiq импортирует брокер по строке "worker.actors" динамически через importlib, а статический
|
||||||
|
анализ Nuitka такое не видит и без явного импорта не включит модуль в standalone-сборку.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import worker.actors # noqa: F401
|
||||||
|
from dramatiq.cli import main as dramatiq_main
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
slots = int(os.getenv("BROWSER_SLOTS", "4"))
|
||||||
|
threads = os.getenv("WORKER_THREADS") or str(slots * 2)
|
||||||
|
sys.argv = ["dramatiq", "worker.actors", "--processes", "1", "--threads", threads]
|
||||||
|
sys.exit(dramatiq_main())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Whitespace-only changes.
Reference in new issue
Block a user