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
+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
|
||||
Reference in new issue
Block a user