Files
robotex/Dockerfile
T
s0k0landClaude Sonnet 5 0a4da80ca9 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>
2026-11-06 13:52:28 +03:00

99 lines
6.1 KiB
Docker
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Сервис 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