How to Run Puppeteer Screenshot Tests in Docker
Run repeatable Puppeteer screenshot tests in Docker with a pinned browser image, deliberate page state, and screenshots saved as CI artifacts.
Run Puppeteer screenshot tests in Docker by using a Puppeteer image whose version matches your project, launching Chrome with the container’s supported sandbox setup, setting the viewport and page state explicitly, and saving screenshots to a mounted artifact directory. The official image includes Chrome for Testing and its required dependencies; Puppeteer’s Docker guide documents sandboxed execution with --cap-add=SYS_ADMIN and recommends an init process such as Docker’s --init. Check the current Docker guide and image tag before choosing a version, since that guide is labeled Next.
This walkthrough uses Node.js and Puppeteer. It shows both a published image workflow and a custom Dockerfile, then covers deterministic capture, element screenshots, CI artifact collection, troubleshooting, and an API option when you do not need browser-driven test logic.
1. Choose a Docker image and pin compatible versions
The published Puppeteer image is the shortest route: it packages Chrome for Testing, dependencies, and a preinstalled Puppeteer version. Use the image tag corresponding to the Puppeteer version your project uses. Avoid a floating tag if repeatable CI output matters; when upgrading, change the Puppeteer dependency and image tag together, then review screenshot differences.
Choose a custom image based on Puppeteer’s Dockerfile when you need project-specific system dependencies or fonts, or want more control over image updates. In either route, verify the precise tag and compatibility against the project lockfile and current Puppeteer Docker instructions.
2. Create a minimal screenshot test
Install Puppeteer in the project and commit the lockfile so CI resolves the same package version. The script below opens a local page, waits for a deliberate readiness condition, sets a fixed viewport, and writes a full-page PNG. It assumes your application is already serving at http://127.0.0.1:3000 inside the container or is otherwise reachable at the URL you configure.
// screenshot-test.mjs
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';
const outputDir = process.env.ARTIFACT_DIR ?? './artifacts';
const targetUrl = process.env.TARGET_URL ?? 'http://127.0.0.1:3000';
await fs.mkdir(outputDir, { recursive: true });
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
await page.goto(targetUrl, { waitUntil: 'networkidle0', timeout: 30000 });
await page.screenshot({
path: `${outputDir}/homepage.png`,
type: 'png',
fullPage: true,
});
} finally {
await browser.close();
}
Use networkidle0 only when the page can actually become idle. Applications with polling, analytics, or long-lived connections may never reach that condition; in that case wait for a meaningful selector or application-ready signal instead:
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector('[data-testid="page-ready"]', { timeout: 15000 });
Replace the example selector with one your application exposes. A test-specific readiness marker is usually clearer than an arbitrary sleep because it expresses the state required before capture.
3. Run it in the official Puppeteer container
Put the test script and package files in a project directory. The following command illustrates the required runtime flags and mounts the current directory at /work. Replace <version> with the matching published image tag from Puppeteer’s guide.
docker run --rm --init \
--cap-add=SYS_ADMIN \
--mount type=bind,source="$PWD",target=/work \
--workdir /work \
--env ARTIFACT_DIR=/work/artifacts \
ghcr.io/puppeteer/puppeteer:<version> \
node screenshot-test.mjs
The official Docker instructions describe the image running Chrome in sandbox mode and document SYS_ADMIN. Use the sandbox configuration required by the image and your host. Puppeteer’s troubleshooting guide strongly discourages disabling Chrome’s sandbox; do not copy older --no-sandbox examples without understanding the security implications and runtime constraints.
Docker’s --init gives the container an init process to manage browser child processes. The mounted artifacts directory survives container removal, so a CI runner can collect the files after the command finishes.
4. Build a custom image when the project needs it
If you need additional fonts or system packages, start from the official Puppeteer Dockerfile or its documented approach and add only the dependencies your application needs. Keep the Puppeteer package and browser image versions aligned.
# Dockerfile
# Replace <version> with a verified Puppeteer version tag.
FROM ghcr.io/puppeteer/puppeteer:<version>
WORKDIR /work
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENV ARTIFACT_DIR=/work/artifacts
CMD ["node", "screenshot-test.mjs"]
docker build -t app-screenshot-tests .
docker run --rm --init \
--cap-add=SYS_ADMIN \
--mount type=bind,source="$PWD/artifacts",target=/work/artifacts \
app-screenshot-tests
Confirm that the chosen base image supports the build and runtime commands shown in its current guide. A custom image can make dependency and font management more explicit, but it also becomes your responsibility to rebuild and update it when Puppeteer or Chrome changes.
5. Make screenshot output comparable
A screenshot test is only useful when the page reaches a known state and the capture settings stay deliberate. For repeatable runs:
- Set the viewport explicitly. Puppeteer documents an 800×600 default headless screen when no window size is specified; set the page viewport anyway when dimensions matter. Screen configuration guide.
- Set device scale factor intentionally. A scale factor of
1avoids an implicit retina-sized image; use another value only when that is what the test is meant to cover. - Use local fixtures or controlled test data, and avoid external services where practical. Puppeteer’s contributor guidance calls out avoiding external services for screenshot tests, but does not promise pixel-identical output across operating systems. Contributing guide.
- Wait for a page condition that represents readiness. Disable or control animations, rotating content, timestamps, randomized data, and any other changing state in the application or test setup.
- Keep fonts and assets available in the container. A missing font or late-loading image can change layout even when the DOM is otherwise ready.
- Choose whether the capture is viewport-only or full-page. Full-page capture may produce very tall images and expose lazy-loaded content that a viewport capture does not.
Puppeteer does not prescribe a universal screenshot-diff algorithm or threshold. Choose comparison rules that fit the rendering variability in your app, and review baseline changes as code changes. The purpose of this workflow is to create a repeatable image; it does not by itself decide whether a visual difference should fail a build.
6. Capture a specific element or tune output
For a component-level visual check, locate the element and call its screenshot method. Puppeteer scrolls an element into view when needed. ElementHandle.screenshot API.
const card = await page.waitForSelector('[data-testid="pricing-card"]');
if (!card) throw new Error('Pricing card was not found');
await card.screenshot({ path: `${outputDir}/pricing-card.png` });
The screenshot options API includes fullPage, clip, path, type, quality, omitBackground, and captureBeyondViewport. PNG is the default and quality does not apply to PNG. The file extension can determine the image type when you do not set it explicitly. Set these options explicitly where output format or dimensions are part of the test contract. ScreenshotOptions API.
await page.screenshot({
path: `${outputDir}/hero.webp`,
type: 'webp',
quality: 85,
fullPage: false,
});
await page.screenshot({
path: `${outputDir}/region.png`,
type: 'png',
clip: { x: 0, y: 0, width: 800, height: 450 },
});
Use omitBackground: true when you specifically need a transparent page background and the page’s rendering supports it. A clip rectangle must fit the intended page area. Avoid changing format or scale between a baseline and a candidate capture.
7. Collect screenshots in CI
Keep the output in a known directory such as artifacts/, mount that directory from the host, and configure your CI provider to upload it after the test command. Artifact upload syntax and retention settings vary by provider, and there is no universal configuration. Make the upload step run even if the screenshot test fails so the captured output and logs remain available for diagnosis.
For CI jobs that start the application and browser in separate containers, put them on a network where the browser can resolve the application service name. localhost inside the browser container refers to that container itself; it does not automatically refer to the host or another service. Use the service hostname or an explicit host mapping appropriate to the Docker runtime.
8. Troubleshooting
| Symptom | Likely cause | What to check or change |
|---|---|---|
| Chrome reports that no usable sandbox is available | The container runtime does not provide the sandbox permissions or configuration expected by the image. | Follow the current Puppeteer Docker guide, including its documented SYS_ADMIN capability, and verify host support. Puppeteer strongly discourages disabling the sandbox. |
| Container exits with browser processes still around or hangs on shutdown | Browser child processes are not being reaped or managed. | Run with Docker --init or use a suitable init entrypoint, as the Docker guide recommends. |
| Screenshot file is missing | The output directory was not created, is not writable, or is not mounted from the container. | Create it with fs.mkdir(..., { recursive: true }), check permissions, and mount the output path to a host directory. |
| Navigation times out waiting for network idle | The page keeps network activity open, for example through polling or a persistent connection. | Wait for domcontentloaded or another suitable navigation condition, then wait for a specific readiness selector. |
| Browser cannot reach the application | The URL uses localhost from inside a container where the app is not running. |
Use the application container’s network hostname, or configure the relevant host mapping. Check connectivity from the browser container. |
| Layout differs between local and Docker images | Different browser versions, viewport settings, device scale factors, fonts, OS libraries, or page data. | Align the Puppeteer and image versions, set viewport and scale explicitly, include required fonts, and control test inputs. |
| Container fails while starting Chrome in a read-only environment | Chrome needs writable profile, configuration, or cache paths during startup. | Provide writable paths or a writable temporary area for browser startup files; Puppeteer’s troubleshooting guide calls out this constraint. |
| Full-page capture is incomplete or extremely tall | Lazy content was not loaded, or the page dimensions are much larger than expected. | Wait for the content needed by the test, inspect page dimensions, and prefer an element or clip capture if the test only concerns one region. |
For Chrome startup and environment-specific errors, consult the Puppeteer troubleshooting guide and the Docker guide for the exact image version.
9. Performance, reliability, and cost
Browser startup and page rendering take time and use memory, so keep each test focused on the page states and viewport sizes the product needs. Reuse one browser for multiple pages within a single test process when that fits the test design, and close pages and the browser in cleanup paths. Avoid parallel captures beyond what the CI runner can support; additional concurrency can increase resource pressure and make timing less predictable.
Pinning the image and package versions makes browser updates deliberate, but it also means scheduling updates rather than assuming the pinned version remains current. Treat a browser or font update as a change that may alter baselines. Keep tests independent of external services where possible to reduce network delays and failures. Docker and browser runtime cost depends on the CI provider, machine size, concurrency, and run time; the cited Puppeteer documentation does not establish a universal price or benchmark.
Or skip the browser setup
If you need a screenshot from a URL rather than browser-driven test logic, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. A single GET request returns PNG, JPEG, WebP, or PDF, and its parameter names also work with those used by other screenshot APIs. The API call does not replace assertions, controlled fixtures, or a visual-diff policy in a Puppeteer test suite.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options. Cookie banners are accepted like a visitor and more than 60 known consent platforms, newsletter popups, and chat widgets can be removed before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does Docker make screenshots pixel-identical across operating systems?
No universal guarantee is documented. A container helps control the browser and dependencies, but fonts, rendering details, and test data still matter. Puppeteer’s contributor guidance recommends cross-platform, self-contained tests without external services; define and review your own comparison policy.
Should every screenshot test use a full-page image?
No. Full-page capture is useful for page-level review, while an element screenshot or clip is more focused and often easier to diagnose for component changes.
Can I use this approach for a site I do not control?
You can navigate to a reachable URL, but external pages may change, rate-limit requests, require consent, or fail unpredictably. For dependable tests, use controlled pages or fixtures where practical.
When is a screenshot API a better fit?
Use an API when the task is to capture a URL and browser-side test logic is unnecessary. Keep Puppeteer when the test must interact with the page, inspect application state, or participate in your existing test suite.


