diff --git a/Dockerfile b/Dockerfile index b8ea55c..a3eefbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,17 +33,32 @@ COPY scripts ./scripts RUN npm run build:server RUN npm run build:ui -FROM node:22-alpine AS runtime +# Runtime is Debian (NOT Alpine): the noVNC headed-browser stack needs glibc. +# Playwright's bundled Chromium does not run on Alpine/musl, and Xvfb/x11vnc/ +# websockify + Chromium are well-supported on Debian. The builder stage stays +# Alpine because it only emits portable artifacts (dist/, ui/dist, vendor/). +FROM node:22-bookworm-slim AS runtime -RUN apk add --no-cache \ +# System deps: +# - git/ca-certificates/tzdata/bash: app + git ops + the bash sandbox shell +# - bubblewrap/python3/python3-pip: sandboxed bash + pre-baked python tools +# - xvfb/x11vnc/websockify: the noVNC display stack (display_mode: novnc) that +# powers the Browser tab live view, InteractiveBrowse, and the CAPTCHA pool +# - fonts-*: legible text (incl. CJK) in the headed browser / screenshots +RUN apt-get update && apt-get install -y --no-install-recommends \ git \ ca-certificates \ tzdata \ bash \ bubblewrap \ python3 \ - py3-pip \ - && apk add --no-cache --virtual .native-build-deps build-base + python3-pip \ + xvfb \ + x11vnc \ + websockify \ + fonts-liberation \ + fonts-noto-cjk \ + && rm -rf /var/lib/apt/lists/* # Pre-bake python packages into the system site-packages (read-only bind-mounted # into every bash sandbox). Runtime `pip install` is intentionally unsupported. @@ -53,10 +68,24 @@ RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/python-requireme WORKDIR /app +# Shared, world-readable Playwright browser cache so the non-root `node` user can +# launch Chromium (npm ci's playwright postinstall downloads it here). +ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright + COPY package.json package-lock.json* ./ -RUN npm ci --omit=dev \ +# build-essential compiles better-sqlite3's native addon (removed afterward to +# stay lean). `npm ci` also runs Playwright's postinstall → downloads Chromium +# into PLAYWRIGHT_BROWSERS_PATH. `playwright install-deps chromium` then adds the +# Chromium shared-library apt deps. chmod makes the browser readable by `node`. +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && npm ci --omit=dev \ + && npx playwright install-deps chromium \ + && chmod -R go+rX /ms-playwright \ && npm cache clean --force \ - && apk del .native-build-deps + && apt-get purge -y build-essential \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/dist ./dist COPY --from=builder /app/ui/dist ./ui/dist diff --git a/docker-compose.yml b/docker-compose.yml index 68012ad..85aa39b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,10 @@ services: image: maestro:latest container_name: maestro restart: unless-stopped + # Headed Chromium (display_mode: novnc) crashes with Docker's default 64MB + # /dev/shm. Give it room so the Browser tab / InteractiveBrowse / CAPTCHA + # pool work. Harmless when display_mode is headless. + shm_size: "1gb" ports: # Auth is optional, so keep the default deployment local-only. - "127.0.0.1:9876:9876" diff --git a/src/engine/browser-session.ts b/src/engine/browser-session.ts index 085d9aa..96b8fdd 100644 --- a/src/engine/browser-session.ts +++ b/src/engine/browser-session.ts @@ -108,14 +108,30 @@ export class SessionManager extends EventEmitter { } static isAvailable(): boolean { - try { - execSync('which Xvfb', { stdio: 'ignore' }); - execSync('which x11vnc', { stdio: 'ignore' }); - execSync('which websockify', { stdio: 'ignore' }); - return true; - } catch { + // Use the POSIX shell builtin `command -v`, NOT the external `which` binary. + // `which` is absent in some minimal images (e.g. debian-slim dropped it from + // debianutils) and depends on PATH; relying on it caused "deps missing → + // headless fallback" even when Xvfb/x11vnc/websockify were actually installed. + // `command -v` is built into /bin/sh, so execSync (which runs via `/bin/sh -c`) + // resolves each binary from the server process's own PATH. + const required = ['Xvfb', 'x11vnc', 'websockify']; + const missing: string[] = []; + for (const bin of required) { + try { + execSync(`command -v ${bin}`, { stdio: 'ignore' }); + } catch { + missing.push(bin); + } + } + if (missing.length > 0) { + logger.warn( + `[SessionManager] noVNC display stack unavailable; not found on the server PATH: ${missing.join(', ')}. ` + + `Browser/CAPTCHA live view (display_mode: novnc) falls back to headless. ` + + `Install them (scripts/setup-novnc.sh) and ensure they are on the server process's PATH.`, + ); return false; } + return true; } private cleanupOrphanedProcesses(): void {