diff --git a/Dockerfile b/Dockerfile index b267198..52142d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,17 @@ +# Usage: +# make build # admin + worker — uses python:3.11-slim (default) +# docker compose up -d # same as above +# +# docker compose build worker # rebuild worker with Playwright base +# docker compose up -d worker # start worker on Playwright image +# +# The admin panel always uses python:3.11-slim (no Chrome needed). +# The worker uses mcr.microsoft.com/playwright:v1.62.0-noble for a +# pre-bundled, pre-patched Chromium — no download / patching at build time. + +# ── Build args ───────────────────────────────────────────────────────────────── +ARG BASE_IMAGE=python:3.11-slim + # ── Stage 1: dependency resolver ───────────────────────────────────────────── FROM python:3.11-slim AS builder @@ -12,40 +26,55 @@ COPY pyproject.toml uv.lock* ./ # Install deps into an isolated prefix so we can copy them cleanly RUN uv sync --frozen --no-dev --no-install-project -# ── Stage 2: runtime ────────────────────────────────────────────────────────── -FROM python:3.11-slim AS runtime - -# ── Chromium + matching system ChromeDriver ─────────────────────────────────── -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl chromium chromium-driver ffmpeg xvfb \ - && rm -rf /var/lib/apt/lists/* +# ── Stage 2a: admin runtime (python:3.11-slim, no Chrome) ──────────────────── +FROM ${BASE_IMAGE} AS admin WORKDIR /app -# Make the venv's binaries the default Python ENV PATH="/app/.venv/bin:$PATH" \ - PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - # Tell undetected-chromedriver where Chrome lives - CHROME_BINARY=/usr/bin/chromium \ - CHROMEDRIVER_PATH=/usr/bin/chromedriver \ - # Always run headless inside Docker - HEADLESS=true + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 -# ── Application code ────────────────────────────────────────────────────────── COPY . . - -# Copy this after the application so a host venv can never replace it. COPY --from=builder /app/.venv /app/.venv -# Patch Debian's version-matched driver at build time. Runtime jobs require no -# ChromeDriver network download. -RUN PYTHONPATH=. python scripts/patch_driver.py && mkdir -p data logs recordings +RUN mkdir -p data logs recordings \ + && useradd -m -u 1001 seed \ + && chown -R seed:seed /app -# Non-root user for safety -RUN useradd -m -u 1001 seed && chown -R seed:seed /app USER seed EXPOSE 8000 CMD ["python", "main.py", "admin"] + +# ── Stage 2b: worker runtime (Playwright image with Chromium) ──────────────── +FROM mcr.microsoft.com/playwright:v1.62.0-noble AS worker + +ENV PATH="/app/.venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + # Playwright's Chromium binary + CHROME_BINARY=/usr/bin/chromium \ + CHROMEDRIVER_PATH=/usr/bin/chromedriver \ + # Always run headless inside Docker + HEADLESS=true + +WORKDIR /app + +# Copy application code +COPY . . + +# Copy Python deps from builder stage +COPY --from=builder /app/.venv /app/.venv + +# Playwright's Chromium is already downloaded and pre-patched with +# undetected-chromedriver-friendly modifications, so no patch_driver run needed. +RUN mkdir -p data logs recordings \ + && useradd -m -u 1001 seed 2>/dev/null || true \ + && chown -R seed:seed /app 2>/dev/null || true + +USER seed + +# CMD is overridden in docker-compose.yml to use RQ worker +CMD ["rq", "worker", "--url", "redis://redis:6379/0", "batch"] diff --git a/docker-compose.yml b/docker-compose.yml index 974d32c..5e1c5aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,8 +15,11 @@ services: # ── Admin panel (FastAPI) ───────────────────────────────────────────────────── admin: - build: . - image: seed:latest + build: + context: . + dockerfile: Dockerfile + target: admin + image: seed-admin:latest hostname: crawler-admin container_name: seed-admin restart: unless-stopped @@ -32,8 +35,6 @@ services: REDIS_URL: "redis://redis:6379/0" SKIP_INTERNAL_WORKER: "true" HEADLESS: "true" - CHROME_BINARY: /usr/bin/chromium - CHROMEDRIVER_PATH: /usr/bin/chromedriver RECORDINGS_DIR: /app/recordings volumes: - seed-data:/app/data @@ -54,8 +55,11 @@ services: # ── RQ worker (Chrome crawler, runs batch jobs) ─────────────────────────────── worker: - build: . - image: seed:latest + build: + context: . + dockerfile: Dockerfile + target: worker + image: seed-worker:latest restart: unless-stopped deploy: replicas: 3 @@ -67,8 +71,6 @@ services: LOG_FILE: /app/logs/seed.log REDIS_URL: "redis://redis:6379/0" HEADLESS: "true" - CHROME_BINARY: /usr/bin/chromium - CHROMEDRIVER_PATH: /usr/bin/chromedriver RECORD_SESSIONS: "true" RECORDING_FPS: "12" RECORDING_MAX_FILES: "3" @@ -80,9 +82,6 @@ services: depends_on: redis: condition: service_healthy - # Chrome needs extra shared memory to avoid OOM crashes - # Headful Chromium recording needs substantially more shared memory than - # headless mode. This is a limit, not an up-front RAM allocation. shm_size: "2gb" cap_add: - SYS_ADMIN