FROM node:22-alpine AS builder WORKDIR /app # build:server runs `bash scripts/generate-version.sh` (set -o pipefail), and # the alpine base ships only ash/sh — without bash the build fails with # "bash: not found" (exit 127). The runtime stage installs bash separately. RUN apk add --no-cache bash COPY package.json package-lock.json* ./ COPY ui/package.json ui/package-lock.json* ./ui/ RUN npm ci --ignore-scripts RUN npm --prefix ui ci --ignore-scripts # noVNC スタンドアロン (vnc.html を含む Web 配布物) を取得。 # npm の @novnc/novnc は lib のみで vnc.html を含まないため、 # Browser タブの iframe 用に GitHub から tarball を取得する。 ARG NOVNC_VERSION=1.6.0 RUN apk add --no-cache --virtual .novnc-fetch curl tar \ && mkdir -p /app/vendor/noVNC \ && curl -fSL "https://github.com/novnc/noVNC/archive/refs/tags/v${NOVNC_VERSION}.tar.gz" \ | tar -xz -C /app/vendor/noVNC --strip-components=1 \ && test -f /app/vendor/noVNC/vnc.html \ && apk del .novnc-fetch COPY tsconfig.json ./ COPY src ./src COPY ui ./ui # build:server runs scripts/generate-version.sh; build:ui runs # ../scripts/validate-help-docs.mjs — both live under scripts/, so the build # context needs it (without this: "scripts/generate-version.sh: No such file", exit 127). COPY scripts ./scripts RUN npm run build:server RUN npm run build:ui # 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 # 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 \ 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. COPY runtime/python-requirements.txt /tmp/python-requirements.txt RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/python-requirements.txt \ && rm /tmp/python-requirements.txt 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* ./ # 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 \ && 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 COPY --from=builder /app/vendor ./vendor COPY pieces ./pieces COPY docs ./docs # Ship a runnable default while still allowing a config bind-mount. COPY config.yaml.example ./config.yaml # The app runs as the non-root `node` user and writes its state under ./data # (db, users, skills, secrets) — relative to WORKDIR /app, i.e. /app/data — plus # /workspaces (worktree) and config.yaml (Settings save). Create and own those # so a fresh deploy doesn't hit EACCES. /app/data and /workspaces are the volume # mount points in docker-compose. RUN mkdir -p /app/data /workspaces \ && chown -R node:node /app/data /workspaces config.yaml ENV NODE_ENV=production \ PORT=9876 \ DB_PATH=/app/data/maestro.db EXPOSE 9876 USER node CMD ["node", "dist/main.js"]