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:
s0k0landClaude Sonnet 5 committed 2026-11-06 13:52:28 +03:00
1 parent f21483489a
commit 0a4da80ca9
12 files changed
+218 -100

No files matched your search

+6 -1
View File
@@ -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
View File
@@ -2,7 +2,8 @@ extra/
.venv/
.idea/
__pycache__/
wheels/
wheels/*
!wheels/.gitkeep
docs/*.bkp
docs/*.dtmp
build/
+74 -15
View File
@@ -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
View File
@@ -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"]
+19 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# зависимости builder-стадии (Dockerfile, Dockerfile.api): компиляция src/ в standalone-бинарник.
# В финальный образ не попадает — ставится только в стадии builder.
Nuitka==4.3.4
-52
View File
@@ -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
+23
View File
@@ -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()
+22
View File
@@ -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()
View File
Whitespace-only changes.