MackIf your Playwright screenshots work locally but fail in Docker with 'Executable doesn't exist', here's the fix.
You build your screenshot service. It works perfectly on your machine. You deploy it to Docker and get:
browserType.launch: Executable doesn't exist at
/home/rails/.cache/ms-playwright/chromium_headless_shell-1208/...
Classic.
Playwright installs browser binaries to a user-specific cache directory. By default, that's ~/.cache/ms-playwright/ on Linux.
In Docker, you typically:
root during the buildrails, node, or appuser)The browsers get installed to /root/.cache/ms-playwright/, but your app looks in /home/youruser/.cache/ms-playwright/. Empty directory. Crash.
Set PLAYWRIGHT_BROWSERS_PATH to a shared location that both users can access:
# In your final stage
ENV PLAYWRIGHT_BROWSERS_PATH="/opt/playwright-browsers"
RUN npm install -g playwright@latest && \
npx playwright install --with-deps chromium && \
chmod -R o+rx /opt/playwright-browsers && \
npm cache clean --force
# ... later ...
USER rails:rails # Your non-root user can now find the browsers
The key pieces:
ENV PLAYWRIGHT_BROWSERS_PATH — tells Playwright where to install AND where to lookchmod -R o+rx — makes the directory readable by all usersnpx playwright install
--with-deps Matters
On Debian/Ubuntu-based images, Chromium needs system libraries. --with-deps installs them automatically:
libnss3libatk1.0-0libgbm1libpangocairo-1.0-0Without --with-deps, you'll get a different error about missing shared libraries. Fun.
Here's what a clean multi-stage Dockerfile looks like for a service that renders screenshots:
FROM ruby:4.0-slim AS base
WORKDIR /app
# Base deps
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y nodejs npm && \
rm -rf /var/lib/apt/lists/*
# Build stage (install gems, assets, etc.)
FROM base AS build
# ... your build steps ...
# Final stage
FROM base
# Playwright browsers in a shared path
ENV PLAYWRIGHT_BROWSERS_PATH="/opt/playwright-browsers"
RUN npm install -g playwright@latest && \
npx playwright install --with-deps chromium && \
chmod -R o+rx /opt/playwright-browsers && \
npm cache clean --force
COPY --from=build /app /app
# Non-root user
RUN useradd -m appuser
USER appuser
CMD ["node", "server.js"]
If you're debugging this, SSH into your container and verify:
# Check where Playwright expects browsers
echo $PLAYWRIGHT_BROWSERS_PATH
# List what's actually installed
ls -la /opt/playwright-browsers/
# Run as your app user
su - appuser -c "npx playwright install --dry-run"
First render after a cold container start will be slow (~2-5s) because Chromium needs to spin up. Subsequent renders reuse the warm process and are much faster. If you need consistent sub-second times, keep a warm browser instance pool.
I hit this exact bug while building Rendly, a screenshot and OG image API. Wasted 30 minutes staring at logs before the obvious hit me. Now you don't have to.