ScreenshotNeo

BlogHow-to

How to capture a webpage screenshot with Puppeteer in Docker

Build a Dockerized Puppeteer screenshot script, choose a safe browser setup, and fix common launch and output problems.

By the ScreenshotNeo team4 October 20267 min read

To capture a webpage screenshot with Puppeteer in Docker, run a Node.js script that launches Puppeteer, opens a page, waits for the page to reach a suitable ready state, saves a screenshot to a mounted output directory, and closes the browser. Puppeteer’s official Docker image includes Chrome for Testing and its dependencies. Its documented setup runs Chrome sandboxed, requires Docker’s SYS_ADMIN capability, and uses --init to manage child processes. See the Puppeteer Docker guide and screenshot guide.

1. Create a screenshot script

Make a project directory and create screenshot.js inside it. This example writes a full-page PNG to /output/page.png. Replace the target URL as needed.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', {
      waitUntil: 'networkidle2',
      timeout: 60000,
    });
    await page.screenshot({
      path: '/output/page.png',
      fullPage: true,
      type: 'png',
    });
  } finally {
    await browser.close();
  }
})();

The call to browser.close() runs even when navigation or capture throws, preventing a normal error path from leaving the browser open. The /output directory must exist and be writable inside the container, and the host directory mounted there must be writable by the container user.

2. Run it with Puppeteer’s Docker image

Puppeteer documents an image containing Chrome for Testing, the required dependencies, and a preinstalled Puppeteer version. Pin a tag that matches the Puppeteer release you use for repeatable builds; latest can change.

mkdir -p output
docker run --init --cap-add=SYS_ADMIN --rm \
  -v "$PWD:/app" \
  -v "$PWD/output:/output" \
  -w /app \
  ghcr.io/puppeteer/puppeteer:latest \
  node screenshot.js

Replace latest with a version tag from the current Docker guide for a pinned deployment. The guide is labeled “Next,” so confirm its current image tags and invocation when setting up a build. After the command completes, find the image at output/page.png on the host.

3. Choose when the page is ready

page.goto() supports navigation conditions such as load, domcontentloaded, networkidle0, and networkidle2. Network idle can be useful for ordinary pages, but it does not guarantee that every site-specific image, chart, animation, or client-rendered widget is ready. Pages with persistent network connections may never become idle.

For a page with a known content marker, wait for that selector after navigation:

await page.goto('https://example.com/report', {
  waitUntil: 'domcontentloaded',
  timeout: 60000,
});
await page.waitForSelector('[data-report-ready="true"]', {
  timeout: 30000,
});
await page.screenshot({ path: '/output/report.png', fullPage: true });

Use a selector that means the content you need is rendered, rather than waiting an arbitrary fixed delay. If the page has no reliable marker, a short delay can accommodate a known animation or client-side render, but it adds latency and is less reliable across network conditions.

4. Select capture scope and output

Need Puppeteer option Notes
Visible viewport page.screenshot({ path }) Captures the current viewport dimensions.
Entire document fullPage: true Captures the full scrollable page; very long pages can produce large images.
One component page.$(selector) then element.screenshot({ path }) Wait for the element first. Puppeteer scrolls a hidden element into view by default.
JPEG or WebP type: 'jpeg' or type: 'webp' Check the installed Puppeteer version’s screenshot options for supported formats and quality settings.

Example for one element:

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.product-card');
const card = await page.$('.product-card');
await card.screenshot({ path: '/output/product-card.png' });

For a custom viewport, set it before navigation or capture:

await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });

5. Build a custom Docker image

A custom image is useful when you need a pinned application environment or additional dependencies. Puppeteer’s Docker guide is the best starting point. In a different base image, ensure that the Chrome build’s shared libraries are installed, that Puppeteer can find a compatible browser, and that the runtime user can write its browser state and output paths. See the official troubleshooting guide and installation guide.

Puppeteer normally downloads a compatible Chrome for Testing browser during installation. If your package manager blocks install scripts, explicitly install the browser with npx puppeteer browsers install or allow the Puppeteer install script. Use puppeteer-core when you manage the browser separately, and provide an executable path or channel to puppeteer.launch().

The Linux Chrome for Testing download is approximately 282 MB according to the Puppeteer installation guide; that is an install-size estimate, not a runtime memory estimate. Alpine Linux does not support Chrome out of the box: verify that the selected browser and system dependencies are compatible rather than assuming a Debian-based setup will work unchanged.

6. Keep the browser sandbox enabled

The official Puppeteer image is intended to run Chrome sandboxed and calls for SYS_ADMIN. Keep that sandbox enabled when the container supports it, especially if the browser can visit untrusted pages. Puppeteer documents --no-sandbox as a limited workaround and explicitly discourages it as a general setup. Do not add it to a screenshot service without assessing the security consequences of rendering arbitrary web content.

For custom containers, use a non-root browser user where practical and make the browser cache, configuration, profile, and output paths writable by that user. In a read-only container, mount writable state directories and configure environment paths such as XDG_CACHE_HOME and XDG_CONFIG_HOME to point to them. A writable userDataDir can also prevent profile startup failures.

7. Troubleshoot common failures

Symptom Likely cause What to do
Browser fails to launch; shared library error The custom image lacks a library required by the chosen Chrome build. Install the libraries listed for the selected Linux base and browser in Puppeteer’s troubleshooting guidance, or use the official image.
Could not find Chrome The install script that downloads Chrome was blocked or did not run. Allow the Puppeteer install script or run npx puppeteer browsers install; confirm the installed browser matches the Puppeteer setup.
Read-only filesystem, crashpad, or profile startup error Chrome cannot write browser state or its profile. Point XDG cache/config and userDataDir to writable directories or mount correctly owned writable paths.
Sandbox initialization error The runtime does not provide the sandbox configuration expected by the image. Use the documented sandbox setup and capability for that image. Do not disable the sandbox as a default fix.
Container exits but Chrome child processes linger Child processes are not being reaped by an init process. Run Docker with --init or configure a suitable init entrypoint.
Navigation times out on a page that appears usable The page keeps connections open or the selected readiness condition is too strict. Use domcontentloaded or load, then wait for a page-specific selector that confirms the required content.
Output file is missing or permission denied The output path is not mounted, does not exist, or is not writable by the runtime user. Create the host directory, mount it at the script’s output path, and check ownership and permissions.

8. Performance, reliability, and cost

Launching Chrome has setup and memory costs, so a service taking many screenshots can reuse a browser process while creating a fresh page per job. Close pages after each capture and close the browser during shutdown. Bound concurrency to the CPU and memory available: each additional page adds work, and full-page images can consume more memory and disk than viewport captures. Measure resource use in your own container and workload; the cited documentation does not provide a general runtime benchmark.

For repeatable results, pin the Puppeteer package and matching Docker image tag, use a stable viewport and device scale factor, and wait for a meaningful selector when page content is dynamic. Web pages can still change independently, fail to load, or show region-specific and consent-specific content, so capture code should report navigation and screenshot errors and allow retries for transient failures. Avoid retrying indefinitely on a deterministic selector or configuration error.

Self-hosted captures use your compute, storage, and network resources; their cost depends on your infrastructure and workload. Keep output retention intentional, since full-page screenshots can be large. Puppeteer itself is an open-source browser automation library; this setup has no per-shot API pricing, but does require maintaining the container, browser dependencies, and operational limits.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. Send one request to capture a page; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Cookie banners are accepted or removed before capture, and known newsletter popups and chat widgets are removed too. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. AI agents can take screenshots through the MCP server. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for free and get 1,000 screenshots a month with no card.

FAQ

Can I capture only an element?

Yes. Wait for the element, select it with page.$(), then call its screenshot() method. Puppeteer scrolls it into view if needed.

Why does network idle not mean every image is ready?

Network idle is a navigation condition, not a promise that a particular image or client-rendered component has finished. Wait for a meaningful selector or other page-specific signal.

Should I use --no-sandbox in Docker?

Not as a general fix. The official image is designed for sandboxed Chrome; check its required capability and runtime configuration first.