maestro/Dockerfile
oss-sync ef44e1a5d9
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (11e2d76)
2026-06-08 08:43:20 +00:00

116 lines
4.5 KiB
Docker

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"]