Stop shipping Firefox and WebKit this server never launches

Microsoft's own Playwright image carries all three engines at 3.5 GB; this
server calls chromium.launch and nothing else, ever. Node's own slim base plus
Chromium installed on its own comes to 1.84 GB - most of that is the OS
libraries --with-deps pulls in for a sandboxed Chromium to run at all, not the
browser binary itself.

One mistake made and caught while building this: installing under
HOME=/tmp/browser-home, which is gone the instant the container starts, because
/tmp is mounted as tmpfs. Moved to PLAYWRIGHT_BROWSERS_PATH=/ms-playwright,
outside it - which is also where Microsoft's own image keeps browsers, and for
the same reason.

Verified under the production security constraints, not assumed to still hold
on a different base: seccomp profile, cap_drop ALL plus cap_add SYS_CHROOT,
read-only root, tmpfs /tmp, chromiumSandbox: true. Chromium launched sandboxed
and navigated a real page under all of them together.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-24 15:36:08 +02:00
parent 44ca1c5435
commit b06c9df73c
1 changed files with 24 additions and 3 deletions

View File

@ -1,14 +1,35 @@
FROM mcr.microsoft.com/playwright:v1.63.0-noble
# A slim Node base plus Chromium alone, not Microsoft's own image: that one ships Chromium,
# Firefox and WebKit together at 3.5 GB, when this server only ever launches Chromium
# (browser-service.js calls chromium.launch, never firefox or webkit). Built and verified this way
# comes to 1.84 GB - Chromium's own weight plus the OS libraries it needs, nothing else - and every
# security constraint below (seccomp, cap_drop, the read-only root, chromiumSandbox: true) was
# checked against this base before it replaced the old one, not assumed to still hold.
FROM node:24-bookworm-slim
ENV NODE_ENV=production
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# Outside /tmp on purpose: the container mounts /tmp as tmpfs (see mcp-stack.compose.yml), so a
# browser installed under HOME=/tmp/... at build time would be gone the instant the real container
# started - a mistake made once while building this image, and the reason it is called out here.
# /ms-playwright is where Microsoft's own image keeps browsers, for the identical reason.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
ENV HOME=/tmp/browser-home
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts \
&& npm cache clean --force \
&& chown -R pwuser:pwuser /app
&& npm cache clean --force
# --with-deps installs the apt packages Chromium needs to run headless and sandboxed - fonts,
# libgtk, libnss and the rest - which is most of what this layer costs; the browser binaries
# themselves are a few hundred MB. chromium.launch({ headless: true }) reaches for the headless
# shell build Playwright ships alongside full Chromium, so both are needed, not just one.
RUN npx --yes playwright@1.63.0 install --with-deps chromium \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& chmod -R o+rX /ms-playwright
RUN groupadd -r pwuser && useradd -r -g pwuser -G audio,video pwuser \
&& mkdir -p /tmp/browser-home && chown -R pwuser:pwuser /app
COPY --chown=pwuser:pwuser src ./src
USER pwuser