Puppeteer with Docker and CI: Production-Ready Setup
Running Puppeteer in CI is harder than it should be. Headless Chrome needs specific system dependencies, and those dependencies vary across base images, CI environments, and Chrome versions. This guide covers the working configurations — not the ones that look right but fail at runtime.
Why Puppeteer Fails in CI
The most common failure mode:
Error: Failed to launch the browser process!
/usr/bin/google-chrome: error while loading shared libraries: libnss3.so: cannot open shared object fileHeadless Chrome requires:
- NSS (libnss3)
- X11 libraries (even in headless mode)
- Font rendering libraries
- Several other system libraries
Stock CI images (Ubuntu minimal, Alpine, Debian slim) don't include these. You either install them manually or use a base image that includes them.
Option 1: Official Puppeteer Docker Image
The simplest approach — use ghcr.io/puppeteer/puppeteer:
FROM ghcr.io/puppeteer/puppeteer:21.0.2
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "scraper.js"]This image includes Chrome and all dependencies. No additional system package installs needed.
docker build -t my-puppeteer-app .
docker run --rm my-puppeteer-appWhen using this image, tell Puppeteer not to download another Chrome:
// package.json
{
"config": {
"puppeteer": {
"skipDownload": true
}
}
}Or via env var:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm ciAnd point to the system Chrome:
const browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/google-chrome-stable',
});Option 2: Install Dependencies Manually
For existing base images (Node official, custom images):
FROM node:20-slim
# Install Chromium dependencies
RUN apt-get update && apt-get install -y \
chromium \
fonts-liberation \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
lsb-release \
wget \
xdg-utils \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "index.js"]Note: using system chromium rather than Puppeteer's bundled Chrome. This avoids the Puppeteer download but may have version mismatches with newer Puppeteer APIs. For most use cases this is fine; for cutting-edge CDP features, use the official Puppeteer image instead.
Required Launch Arguments
In Docker (no root, no sandbox), you need:
const browser = await puppeteer.launch({
headless: true,
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage', // Avoid /dev/shm size issues in Docker
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--single-process', // Reduces memory usage in containers
'--disable-gpu',
],
});--disable-dev-shm-usage is critical in Docker — by default Chrome uses /dev/shm for shared memory, which Docker limits to 64MB. This flag uses /tmp instead.
GitHub Actions
name: Puppeteer Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
- name: Install Chrome
run: |
sudo apt-get update
sudo apt-get install -y chromium-browser
- name: Run tests
run: npm test
env:
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium-browser
BASE_URL: https://staging.example.comAlternatively, use the setup-chrome action:
- name: Setup Chrome
uses: browser-actions/setup-chrome@v1
with:
chrome-version: stable
- name: Run tests
run: npm test
env:
PUPPETEER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}CircleCI
version: 2.1
jobs:
test:
docker:
- image: cimg/node:20.0-browsers # Includes Chrome
steps:
- checkout
- run:
name: Install dependencies
command: |
npm ci
environment:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true"
- run:
name: Run tests
command: npm test
environment:
PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome-stablecimg/node:*-browsers includes a full browser stack — the easiest CircleCI path.
GitLab CI
image: node:20
variables:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true"
PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome-stable
before_script:
- apt-get update && apt-get install -y wget gnupg
- wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
- echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list
- apt-get update && apt-get install -y google-chrome-stable fonts-liberation --no-install-recommends
- npm ci
test:
script:
- npm testDocker Compose for Local Development
For development environments where you want consistent browser behavior:
# docker-compose.yml
version: '3.8'
services:
puppeteer:
build: .
environment:
- NODE_ENV=development
- BASE_URL=http://app:3000
volumes:
- ./src:/app/src
- ./tests:/app/tests
- ./reports:/app/reports
depends_on:
- app
command: npm test
app:
image: your-app-image
ports:
- "3000:3000"Debugging CI Failures
Screenshot on Failure
process.on('unhandledRejection', async (reason) => {
if (page) {
await page.screenshot({ path: `failure-${Date.now()}.png`, fullPage: true });
}
throw reason;
});Upload screenshots as CI artifacts:
# GitHub Actions
- name: Upload failure screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: failure-screenshots
path: '*.png'Verbose Logging
const browser = await puppeteer.launch({
dumpio: true, // Log Chrome's stdout/stderr to Node's stdout/stderr
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
});dumpio: true prints Chrome's internal logs — useful for diagnosing GPU, sandbox, or shared memory issues.
Check Chrome Version
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
const version = await browser.version();
console.log('Chrome version:', version);
await browser.close();Version mismatches between Puppeteer and Chrome cause API compatibility issues. If you're using system Chrome, verify the version matches Puppeteer's expected Chrome version (documented in Puppeteer's release notes).
Memory Limits in Container Environments
Kubernetes and Docker impose memory limits. Chrome is memory-hungry:
- Base Chrome process: ~80MB
- Each tab: ~50-150MB depending on page complexity
- Multiple tabs: ~300-500MB for typical scraping
Set container resource limits accordingly:
# Kubernetes deployment
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"For tighter limits, use --single-process and --no-zygote launch args (trade-off: reduced stability for lower memory use).
Continuous Test Execution
Container-based Puppeteer tests run reliably in CI, but you also want to run them continuously against production/staging outside of the PR cycle. HelpMeTest handles scheduled execution without you managing the Docker/Chrome infrastructure — tests run in managed containers, with results tracked over time and alerts sent when failures appear.
Summary
The two reliable paths are: use the official Puppeteer Docker image, or manually install Chrome's system dependencies. Always include --no-sandbox, --disable-setuid-sandbox, and --disable-dev-shm-usage in Docker environments. Use PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true and PUPPETEER_EXECUTABLE_PATH to point at the system Chrome. Capture screenshots on failure and upload them as CI artifacts — they're the fastest path to understanding headless failures.