Puppeteer with Docker and CI: Production-Ready Setup

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 file

Headless 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-app

When 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 ci

And 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.com

Alternatively, 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-stable

cimg/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 test

Docker 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.

Read more

Start now free