# ベースイメージは digest で固定する。タグは同じ名前のまま中身が差し替わるため、 # タグだけでは「いつビルドしても同じもの」を保証できない。バージョンを上げるときは # タグと digest を同時に更新すること。 FROM node:22-alpine@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 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 ではなく ENV にする。ARG は `docker build --build-arg` で外から上書きでき、 # 下の SHA-256 の固定を無効化できてしまう。 ENV NOVNC_VERSION=1.6.0 # Pinned SHA256 of the v${NOVNC_VERSION} source tarball. Verify the integrity of # the download before extracting so a compromised/MITM'd tarball can't inject # code into the image. To bump NOVNC_VERSION, recompute via: # curl -fSL "https://github.com/novnc/noVNC/archive/refs/tags/v.tar.gz" | sha256sum ENV NOVNC_SHA256=5066103959ef4e9b10f37e5a148627360dd8414e4cf8a7db92bdbd022e728aaa 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" -o /tmp/novnc.tar.gz \ && echo "${NOVNC_SHA256} /tmp/novnc.tar.gz" | sha256sum -c - \ && tar -xz -C /app/vendor/noVNC --strip-components=1 -f /tmp/novnc.tar.gz \ && rm -f /tmp/novnc.tar.gz \ && 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@sha256:d649c27dae7ba0137b3cef5dd75baa422c08dc3d9e3fc0c23dfb172dc3cc6436 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 # - libreoffice-impress: headless .pptx/.ppt -> PDF conversion for previews # - libreoffice-calc/python3-uno: trusted XLSX recalculation and formula checks # through runtime/excel-recalculate.py. --no-install-recommends keeps the # image lean; CJK glyphs come from the fonts-noto-cjk package above. 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 \ libreoffice-impress \ libreoffice-calc \ python3-uno \ && 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. The browser binary is fetched explicitly below. ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright COPY package.json package-lock.json* ./ # The install-script allowlist and the rebuild driver must ship BEFORE the # install RUN below, because the runtime install is `--ignore-scripts` and the # native addon is rebuilt explicitly from this allowlist. Without these two the # install below cannot produce a working better-sqlite3. COPY security/install-scripts-allowlist.json ./security/install-scripts-allowlist.json COPY scripts/security/rebuild-native.mjs ./scripts/security/rebuild-native.mjs # build-essential compiles better-sqlite3's native addon (removed afterward to # stay lean). `playwright install --with-deps chromium` downloads the Chromium # binary INTO PLAYWRIGHT_BROWSERS_PATH and installs its shared-library apt deps # in one step. NOTE: `npm ci` alone does NOT download the browser — Playwright # dropped the npm-install postinstall browser download, so relying on it left # /ms-playwright absent and the chmod below failing on a clean build (issue # #528). chmod makes the browser readable by the non-root `node` user. # # We invoke the CLI as `node node_modules/playwright/cli.js` rather than # `npx playwright`: prod `playwright` and dev `@playwright/test` both declare a # `playwright` bin, and lockfile generation resolves that collision in favour of # `@playwright/test`. That ownership is baked into package-lock.json, so even a # CLEAN `npm ci --omit=dev` (with @playwright/test absent) does NOT relink # node_modules/.bin/playwright to the prod package — the bin is simply missing # and `npx playwright` fails with exit 127 (issue #007). Calling the package's # own cli.js sidesteps bin resolution entirely and is version-matched to the # installed prod `playwright`. # # `--ignore-scripts` here, not just in the builder stage. This is the SHIPPED # image, and build-essential is present on the very same layer, so a hijacked # dependency's install script would run with a compiler in reach — the most # permissive place in the whole pipeline. The builder stage has been guarded # since the start; runtime was the hole (found reviewing PR #1074). # The allowlisted native addon is rebuilt right after, from the same allowlist # CI uses, and the rebuild driver verifies it by actually opening a database. RUN apt-get update \ && apt-get install -y --no-install-recommends build-essential \ && npm ci --omit=dev --ignore-scripts \ && node scripts/security/rebuild-native.mjs \ && node node_modules/playwright/cli.js install --with-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 # 利用者向けヘルプの Markdown 原本。UI は vite が ui/dist にバンドルするので、 # raw なファイルはここにしか無い。ReadAppDoc / ListAppDocs (help/) が # 実行時にこれを読むため、置かないと本番だけヘルプが 1 本も引けなくなる。 # .md だけ(同じディレクトリに置いてある文言固定テストは持ち込まない)。 COPY ui/src/content/help/*.md ./ui/src/content/help/ # Browser setup wizard shares its pure LLM logic (probe / parse / worker build / # isLlmConfigured) with the CLI wizard via this single .mjs. The server imports # it at runtime as `../../scripts/setup-lib.mjs` (resolves to /app/scripts from # /app/dist/bridge), so it MUST ship in the runtime image — without it the setup # endpoints 500 on first `docker compose up` (Codex P2 #7). The .d.mts is # build-time only and intentionally omitted. COPY scripts/setup-lib.mjs ./scripts/setup-lib.mjs # ブラウザにファイル選択の画面を出させない管理ポリシー (#1148)。noVNC の画面は # 人がブラウザをそのまま触れるので、ファイルを選ぶボタンからサーバー上の # ファイルを持ち出せてしまう。銘柄ごとに置き場所が違うため 3 か所に置く # (Playwright 同梱 = Google Chrome for Testing / 本家 Chrome / Chromium)。 # msedge は未対応 (置き場所の裏が取れていないので推測で足さない)。 # エージェント側は壊れない。エージェントがブラウザへファイルを送る経路が # そもそも無いため (src/ に filechooser / setInputFiles は 1 か所も無い)。 # 将来足すなら setInputFiles を使うこと。選択画面を通らないので影響を受けない。 COPY deploy/browser-policy/agent-orchestrator.json /tmp/browser-policy.json RUN for d in /etc/opt/chrome_for_testing/policies/managed \ /etc/opt/chrome/policies/managed \ /etc/chromium/policies/managed; do \ mkdir -p "$d" && cp /tmp/browser-policy.json "$d/agent-orchestrator.json" \ && chmod 644 "$d/agent-orchestrator.json"; \ done \ && rm /tmp/browser-policy.json # Trusted helper only: agents do not edit or supply this Python source. COPY runtime/excel-recalculate.py ./runtime/excel-recalculate.py # Ship config.yaml.example as a REFERENCE, plus a MINIMAL unconfigured # config.yaml. A fresh `docker compose up` (no ./config.yaml bind-mount, no # OLLAMA_* env) must detect "no LLM configured" and show the browser setup # wizard — baking the fully-worked example (which has an enabled localhost # worker) would mark the install "configured", suppress the wizard, and leave # the user pointed at a localhost Ollama that isn't running. Mounting # ./config.yaml or setting OLLAMA_BASE_URL/OLLAMA_MODEL overrides this default # and skips the wizard. COPY config.yaml.example ./config.yaml.example RUN printf 'config_version: 2\n' > ./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 # HOST=0.0.0.0 is required INSIDE the container so the published port is # reachable. docker-compose.yml publishes that port on ALL host interfaces by # default, so a Docker deployment is reachable from the LAN out of the box — # which is exactly the condition that makes a FRESH install generate an admin # login on first boot instead of coming up open (#1024, initial-admin.ts). TLS # and the rest of the hardening are still the operator's responsibility (see # docs/docker.md, SECURITY.md). The app's own default (when HOST is unset, e.g. # bare-metal) is 127.0.0.1; see createCoreServer in src/bridge/server.ts. ENV NODE_ENV=production \ PORT=9876 \ HOST=0.0.0.0 \ DB_PATH=/app/data/maestro.db EXPOSE 9876 USER node CMD ["node", "dist/main.js"]