DEV Community

Mack
Mack

Posted on • Originally published at rendly-api.fly.dev

Playwright in Docker: The Browser Path Gotcha That'll Waste Your Afternoon

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/...
Enter fullscreen mode Exit fullscreen mode

Classic.

The Problem

Playwright installs browser binaries to a user-specific cache directory. By default, that's ~/.cache/ms-playwright/ on Linux.

In Docker, you typically:

  1. Install Playwright as root during the build
  2. Run the app as a non-root user (like rails, 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.

The Fix

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
Enter fullscreen mode Exit fullscreen mode

The key pieces:

  1. ENV PLAYWRIGHT_BROWSERS_PATH — tells Playwright where to install AND where to look
  2. chmod -R o+rx — makes the directory readable by all users
  3. Set the ENV before the install so it takes effect during npx playwright install

Why --with-deps Matters

On Debian/Ubuntu-based images, Chromium needs system libraries. --with-deps installs them automatically:

  • libnss3
  • libatk1.0-0
  • libgbm1
  • libpangocairo-1.0-0
  • ... about 20 more

Without --with-deps, you'll get a different error about missing shared libraries. Fun.

The Full Pattern

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"]
Enter fullscreen mode Exit fullscreen mode

Quick Sanity Check

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"
Enter fullscreen mode Exit fullscreen mode

One More Thing

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.

Top comments (0)