# Сервис worker (#78): выполняет задачи из очереди в Google Chrome под Xvfb. # # builder: компилирует src/ (через entry_worker.py) в самодостаточный standalone-бинарник Nuitka — # в финальный образ не попадают ни исходники .py, ни gcc/pip, ни сам requirements.txt. FROM python:3.12-slim-bookworm AS builder ENV PYTHONDONTWRITEBYTECODE=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/* # 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 WORKDIR /app COPY --from=builder --chown=app:app /build/dist/entry_worker.dist/ ./ USER app # профили браузера (extra/user_data_dir) и geoip-кэш — в volume VOLUME ["/app/extra"] # tini как PID 1: пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb (иначе — зомби) 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"] # 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