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
|
||||
.idea
|
||||
.venv
|
||||
wheels
|
||||
extra
|
||||
docs
|
||||
tests
|
||||
**/__pycache__
|
||||
*.pyc
|
||||
.env
|
||||
# артефакты Nuitka при локальных прогонах (вне Docker)
|
||||
*.build
|
||||
*.dist
|
||||
*.onefile-build
|
||||
.nuitka-cache
|
||||
# wheels/ НЕ в игноре: нужен в контексте сборки для OFFLINE=1 (make wheels)
|
||||
+2
-1
@@ -2,7 +2,8 @@ extra/
|
||||
.venv/
|
||||
.idea/
|
||||
__pycache__/
|
||||
wheels/
|
||||
wheels/*
|
||||
!wheels/.gitkeep
|
||||
docs/*.bkp
|
||||
docs/*.dtmp
|
||||
build/
|
||||
|
||||
+74
-15
@@ -1,39 +1,98 @@
|
||||
# Сервис 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 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=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), нужен виртуальный дисплей
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends tini xvfb xauth fonts-liberation fonts-noto-color-emoji ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# real_chrome=True → channel "chrome": ставим Google Chrome stable вместе с системными зависимостями
|
||||
RUN patchright install --with-deps chrome \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# real_chrome=True → channel "chrome": ставим Google Chrome stable вместе с системными зависимостями.
|
||||
# Версия patchright — как в requirements.txt (#109): её CLI и сам пакет worker'у в runtime не нужны,
|
||||
# только Chrome и его системные библиотеки, которые он поставит через apt
|
||||
ARG OFFLINE=0
|
||||
COPY wheels/ /tmp/wheels/
|
||||
RUN set -e; \
|
||||
if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=/tmp/wheels"; else PIP_OPTS=""; fi; \
|
||||
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 \
|
||||
&& mkdir -p /app/extra \
|
||||
&& 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
|
||||
WORKDIR /app/src
|
||||
|
||||
# профили браузера (extra/user_data_dir) и geoip-кэш — в volume
|
||||
VOLUME ["/app/extra"]
|
||||
|
||||
# tini как PID 1: пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb (иначе — зомби)
|
||||
ENTRYPOINT ["/usr/bin/tini", "-g", "--"]
|
||||
# один процесс: открытые окна браузера живут в его памяти; потоков — с запасом над слотами, чтобы
|
||||
# задачи в уже открытые окна не ждали, пока потоки заняты задачами без свободного слота
|
||||
CMD ["sh", "-c", "exec xvfb-run -a --server-args='-screen 0 1920x1080x24' dramatiq worker.actors --processes 1 --threads ${WORKER_THREADS:-$((${BROWSER_SLOTS:-4} * 2))}"]
|
||||
# BROWSER_SLOTS/WORKER_THREADS читает сам entry_worker.bin (--processes 1, потоков — вдвое больше слотов)
|
||||
CMD ["sh", "-c", "exec xvfb-run -a --server-args='-screen 0 1920x1080x24' ./entry_worker.bin"]
|
||||
|
||||
# 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 — лёгкий образ.
|
||||
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 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=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 .
|
||||
RUN pip install -r requirements-api.txt \
|
||||
# OFFLINE=1: ставить зависимости только из wheels/ (make wheels), без обращения к PyPI — см. docs/Deployment.md
|
||||
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
|
||||
|
||||
COPY --chown=app:app src/main.py ./src/
|
||||
COPY --chown=app:app src/tasks/ ./src/tasks/
|
||||
COPY --chown=app:app src/db/ ./src/db/
|
||||
COPY --chown=app:app src/api/__init__.py src/api/schemas.py ./src/api/
|
||||
WORKDIR /app
|
||||
COPY --from=builder --chown=app:app /build/dist/entry_api.dist/ ./
|
||||
|
||||
USER app
|
||||
WORKDIR /app/src
|
||||
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
|
||||
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
|
||||
|
||||
+7
-1
@@ -10,6 +10,8 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
args:
|
||||
OFFLINE: ${OFFLINE:-0}
|
||||
image: robotex-api:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -24,7 +26,11 @@ services:
|
||||
postgres: {condition: service_healthy}
|
||||
|
||||
worker:
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
target: runtime
|
||||
args:
|
||||
OFFLINE: ${OFFLINE:-0}
|
||||
image: robotex-worker:latest
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
|
||||
+17
-6
@@ -47,7 +47,7 @@ make key-list # клиенты: начало ключа, акти
|
||||
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
|
||||
|
||||
Сборка двухстадийная (`Dockerfile`): `builder` компилирует `src/` (через `entry_worker.py`) в самодостаточный
|
||||
standalone-бинарник Nuitka, `runtime` берёт из него только готовую папку `entry_worker.dist/` — исходники `.py`,
|
||||
`requirements.txt`, gcc и pip в финальный образ не попадают.
|
||||
|
||||
| Компонент | Зачем |
|
||||
|---|---|
|
||||
| `python:3.12-slim-bookworm` | базовый образ, Python той же версии, что и в `.venv` |
|
||||
| `requirements.txt` | ставится из PyPI (папка `wheels/` в образ не попадает — она под Ubuntu Noble и неполная) |
|
||||
| Google Chrome stable (`patchright install --with-deps chrome`) | сессия запускается с `real_chrome=True` → channel `chrome` |
|
||||
| `entry_worker.bin` (standalone, builder) | скомпилированный Nuitka код `src/` + его Python-зависимости; оборачивает `dramatiq worker.actors --processes 1 --threads N` |
|
||||
| `python:3.12-slim-bookworm` (runtime) | базовый образ — Python тут нужен только на шаге установки Chrome (`patchright install`), сам бинарник от него не зависит |
|
||||
| Google Chrome stable (`patchright install --with-deps chrome`) | сессия запускается с `real_chrome=True` → channel `chrome`; после установки пакет `patchright` из runtime удаляется |
|
||||
| Xvfb (`xvfb-run`) | браузер работает в headful-режиме (`headless=False`), нужен виртуальный дисплей 1920x1080x24 |
|
||||
| `tini` (ENTRYPOINT, PID 1) | пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb — без него копятся зомби-процессы |
|
||||
| пользователь `app` (uid 1000) | Chrome не запускается от root без `--no-sandbox` |
|
||||
|
||||
Рабочая директория — `/app/src`. Воркер — один процесс dramatiq (`--processes 1`: открытые окна живут в его памяти),
|
||||
потоков `WORKER_THREADS` (по умолчанию `2 × BROWSER_SLOTS`).
|
||||
`make build offline=1` ставит python-зависимости builder-стадии только из `wheels/` (`make wheels`), без обращения
|
||||
к 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
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# зависимости builder-стадии (Dockerfile, Dockerfile.api): компиляция src/ в standalone-бинарник.
|
||||
# В финальный образ не попадает — ставится только в стадии builder.
|
||||
Nuitka==4.3.4
|
||||
@@ -1,85 +1,33 @@
|
||||
2captcha-python==2.1.1
|
||||
aiofiles==25.1.0
|
||||
aiohappyeyeballs==2.7.1
|
||||
aiohttp==3.14.3
|
||||
aiosignal==1.4.0
|
||||
annotated-doc==0.0.5
|
||||
annotated-types==0.8.0
|
||||
anyio==4.15.1
|
||||
apify_fingerprint_datapoints==0.15.0
|
||||
asyncpg==0.32.0
|
||||
attrs==26.1.0
|
||||
blessed==1.49.0
|
||||
browserforge==1.2.4
|
||||
camoufox==0.5.6
|
||||
certifi==2026.7.22
|
||||
charset-normalizer==3.5.1
|
||||
click==8.5.0
|
||||
distro==1.9.0
|
||||
dramatiq==2.2.1
|
||||
editor==1.8.0
|
||||
fastapi==0.141.1
|
||||
frozenlist==1.8.0
|
||||
geoip2==5.3.0
|
||||
greenlet==3.5.5
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
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
|
||||
multidict==6.7.1
|
||||
numpy==2.5.3
|
||||
orjson==3.12.0
|
||||
patchright==1.63.0
|
||||
pika==1.4.4
|
||||
platformdirs==4.11.7
|
||||
playwright==1.62.0
|
||||
propcache==0.5.2
|
||||
psutil==7.2.2
|
||||
pydantic==2.13.5
|
||||
pydantic_core==2.46.5
|
||||
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
|
||||
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
|
||||
starlette==1.6.0
|
||||
typing_extensions==4.16.0
|
||||
typing-inspection==0.4.4
|
||||
ua-parser==1.0.2
|
||||
ua-parser-builtins==202606
|
||||
urllib3==2.7.0
|
||||
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