Take a Website Screenshot with Playwright in Node.js Inside a Docker Container
Capture viewport, full-page, or element screenshots with Playwright in Node.js inside Docker, with runnable setup, security guidance, and fixes for common errors.
Use Playwright’s Node.js API to launch Chromium, navigate to a URL, and call page.screenshot(). In Docker, use a Playwright browser image whose version matches the project’s Playwright package, install that package separately, and mount an output directory if the screenshot needs to persist on the host.
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.screenshot({ path: '/out/screenshot.png' });
} finally {
await browser.close();
}
The code below assumes an ES module project and uses the official Playwright Docker image. That image supplies browser binaries and system dependencies, but your project still needs the Playwright package installed. Keep the image tag and package version aligned. See the Playwright screenshot guide, Page API, and Docker guidance.
1. Create a minimal Node.js project
Make a directory with these three files. The version shown is an example pinned version; check the current Playwright documentation and use the same version for the npm package and Docker image in your own project.
{
"name": "playwright-docker-shot",
"private": true,
"type": "module",
"scripts": {
"shot": "node screenshot.js"
},
"dependencies": {
"playwright": "1.63.0"
}
}
Save this as screenshot.js:
import { chromium } from 'playwright';
const targetUrl = process.env.TARGET_URL ?? 'https://example.com';
const outputPath = process.env.OUTPUT_PATH ?? '/out/screenshot.png';
const fullPage = process.env.FULL_PAGE === '1';
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1
});
const response = await page.goto(targetUrl, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
if (response && !response.ok()) {
console.error(`Navigation returned HTTP ${response.status()} ${response.statusText()}`);
}
await page.screenshot({ path: outputPath, fullPage });
console.log(`Saved ${outputPath}`);
} finally {
await browser.close();
}
page.screenshot({ path }) writes an image file. Without path, it returns a Node.js Buffer, useful when uploading the image or passing it to an image-processing step. The default screenshot is the current viewport; set fullPage: true to capture the full scrollable page. See Playwright’s screenshot examples.
Save this as Dockerfile:
FROM mcr.microsoft.com/playwright:v1.63.0-noble
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY screenshot.js ./
RUN mkdir -p /out
CMD ["npm", "run", "shot"]
Build and run it, mounting a host directory at /out so the file remains available after the container exits:
mkdir -p out
docker build -t playwright-docker-shot .
docker run --rm \
-e TARGET_URL=https://example.com \
-e OUTPUT_PATH=/out/example.png \
-v "$PWD/out:/out" \
playwright-docker-shot
The screenshot will be at out/example.png on the host. To capture the entire page, add -e FULL_PAGE=1 to the docker run command. The volume mapping is important: files written only inside a container that is removed with --rm are not retained on the host.
2. Choose the capture area and output
Viewport screenshot
The basic call captures what is visible in the page viewport:
await page.screenshot({ path: '/out/viewport.png' });
Set the viewport when creating the page or context to control the dimensions. The screenshot’s pixel dimensions also depend on device scale factor and screenshot options.
Full-page screenshot
await page.screenshot({ path: '/out/full-page.png', fullPage: true });
This captures the full scrollable page, not just the initially visible viewport. Long pages may produce large images and consume more memory. If the page loads content only when scrolled, full-page capture may not trigger every lazy-loaded element; see the edge cases below.
One element
Use a locator screenshot to isolate a component. The locator must resolve to a visible element:
const card = page.locator('[data-testid="product-card"]').first();
await card.screenshot({ path: '/out/product-card.png' });
For selector-driven scripts, wait for the target before capture:
const target = page.locator('#report');
await target.waitFor({ state: 'visible', timeout: 10000 });
await target.screenshot({ path: '/out/report.png' });
Save to a buffer
const bytes = await page.screenshot({ type: 'png' });
// Example: pass bytes to an upload or image-processing function.
Use path when you want Playwright to save a durable artifact. Use the returned buffer when the next step consumes bytes directly. Supported screenshot output formats include PNG, JPEG, and WebP; set type when selecting JPEG or WebP, and use a matching file extension for path-based output.
3. Control when the page is ready
A screenshot taken immediately after navigation can catch an incomplete page. Choose a readiness condition that matches the site rather than adding a long fixed sleep by default.
waitUntil: 'domcontentloaded'waits for the document to be parsed, but not necessarily all images or application data.waitUntil: 'load'waits for the load event, which may be delayed by resources.waitUntil: 'networkidle'waits for network activity to settle; sites with polling or persistent connections may never become idle.- Wait for a specific selector when a known page element indicates the content is ready.
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.locator('main h1').waitFor({ state: 'visible', timeout: 15000 });
await page.screenshot({ path: '/out/ready.png' });
Navigation can return an HTTP error response without throwing an exception. Check the response status if the calling job should treat non-success status codes as failures. Some single-page apps also update content after the initial navigation response, so waiting for an application-specific selector is often more reliable than assuming navigation completion means the page is visually ready.
4. Configure Docker and browser compatibility
Use a versioned Playwright image
The official Playwright image includes browsers and their system dependencies, but not the Playwright package your project imports. Pin a versioned image tag and install the same Playwright version in the project. A mismatch can make Playwright unable to find the expected browser executable. The Docker documentation displayed tags such as v1.63.0-noble at the time this guide’s research was collected; check the current documentation for available tags before adopting a version.
Build a custom image
If you need a different base image, it must include Node.js, the Playwright browsers, and the browser system dependencies. Playwright’s Docker guidance shows installing browsers and dependencies with the Playwright CLI. Keep the CLI version aligned with the project package as well:
FROM node:20-bookworm
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
RUN npx playwright install --with-deps chromium
COPY screenshot.js ./
RUN mkdir -p /out
CMD ["npm", "run", "shot"]
This illustrates the required pieces; adapt the Node base, package manager, browser choice, and install command to the version and deployment environment you maintain. Installing browser dependencies can make custom images larger, so prefer a pinned official image unless you have a concrete need for a custom base.
Keep screenshots on the host
Write to a container path covered by a bind mount or named volume. With a bind mount such as -v "$PWD/out:/out", the host directory and container directory refer to the same files. Ensure the container user can write to the mounted directory if you run as a non-root user.
5. Run Chromium with an appropriate security model
The official Playwright Docker image runs as root by default. In that documented configuration, Chromium’s sandbox is disabled. This can suit trusted end-to-end test pages, but web scraping or crawling untrusted sites needs stronger isolation. Playwright’s Docker guidance recommends a separate user and a seccomp profile for untrusted sites so Chromium can run sandboxed.
Do not treat a browser container as a security boundary by itself. Avoid passing secrets into jobs that visit untrusted URLs, restrict network access where your deployment allows it, and use the documented non-root and seccomp approach for the workload. Check the current Playwright Docker security instructions before configuring a production crawler, since the required container options depend on the runtime.
6. Handle common edge cases
Lazy-loaded images and content
A page may defer images or sections until they are near the viewport. A full-page screenshot does not guarantee that every application’s lazy content has been requested. For a known page, scroll through it or wait for the relevant content before capturing, then return to the intended scroll position if the final screenshot is viewport-only.
Animations and changing content
Animations, rotating banners, timestamps, and live data can make captures inconsistent. Where suitable, disable animations through the screenshot option or add a page-specific style before capture. Avoid hiding meaningful content merely to force a match.
Large pages
Full-page screenshots of very long pages can produce large files and use substantial browser memory. Capture only the required element or viewport when that meets the task. If a report needs multiple sections, consider capturing those sections separately rather than creating one extremely tall bitmap.
Authentication and consent
Sites behind login may require a storage state, cookies, or headers. Protect those credentials and do not bake secrets into an image layer or source repository. Consent banners and chat widgets can cover content; for a one-off Playwright script, handle them as a visitor would or use a site-specific selector to dismiss or hide the obstruction when permitted.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| “Executable doesn’t exist” or browser launch fails | The package and Docker image/browser versions do not match, or the browser was not installed in a custom image. | Align the Playwright package, CLI, and image versions. For a custom image, install the browser and system dependencies using the matching Playwright CLI. |
| “Cannot find package ‘playwright’” | The image contains browser binaries but the project dependency was not installed or copied. | Declare playwright in the package manifest and install dependencies during the image build. |
| Missing shared library errors | The custom base image lacks browser system dependencies. | Install the dependencies using the matching Playwright CLI’s install --with-deps command, or use the official image. |
| Screenshot file is missing after container exit | The output was saved inside the container filesystem, which was removed. | Write beneath a mounted path such as /out and bind mount a host directory there. |
| Permission denied writing the screenshot | The mounted host directory is not writable by the container user. | Adjust host directory ownership or permissions for the chosen container user, and confirm the output path exists. |
| Screenshot is blank or page content is missing | Capture ran before client-side content rendered, navigation failed, or the page requires authentication. | Inspect navigation status, wait for a meaningful selector, and provide the required state or credentials securely. |
| Navigation times out | The site is slow, has persistent connections, or the chosen readiness event never occurs. | Set an explicit timeout and use a more appropriate readiness condition such as domcontentloaded followed by a selector wait. |
| Different image from local or CI baseline | Rendering can vary with operating system, browser version, settings, hardware, power source, and headless mode. | Capture comparisons in the same environment as the baseline and keep the browser version and settings consistent. |
| Full-page screenshot omits deferred content | The application loads content only after scrolling or interaction. | Trigger the page’s normal loading behavior, wait for its content, and then take the full-page screenshot. |
8. Performance, reliability, and cost
Each browser launch and page navigation adds work. For one screenshot per short-lived job, launch once, capture, and close in a finally block as shown. For batches, reuse a browser process and create separate pages or contexts as needed, while limiting concurrency to what the container’s CPU and memory can support. Close pages and browsers so failed jobs do not leave processes running.
Screenshot output size depends on dimensions, page length, and format. Full-page capture and high device scale factors increase pixel count and memory use. Choose the smallest capture area and resolution that meets the downstream requirement. For stable visual comparisons, use the same operating system, browser version, settings, and headless configuration as the baseline; Playwright notes that rendering may vary across environments. Its visual comparison guidance also waits for two consecutive screenshots to match before an assertion comparison, which helps avoid transient capture differences. See Visual comparisons.
Self-hosting means your direct costs are the compute, storage, and network resources used by your container environment; the cited Playwright docs do not provide a per-screenshot price. Budget for image artifacts and retries, and decide whether timeouts, navigation errors, or non-success HTTP responses should be retried. A retry policy should be bounded because some failures, such as access restrictions or invalid URLs, will not improve by repeating the same request.
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF, so you do not need to package browser binaries and system dependencies in your container for this capture.
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 would, and known consent platforms, newsletter popups, and chat widgets are 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 say what happened. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.
10. FAQ
Does Playwright save screenshots as PNG by default?
PNG is the default screenshot format. You can choose JPEG or WebP with the screenshot options when those formats fit your storage or delivery needs.
Can I use a CSS selector instead of capturing the whole page?
Yes. Create a locator for the element and call its screenshot() method. Wait for it to be visible if its appearance depends on asynchronous page rendering.
Why use a Docker image instead of installing a browser on the host?
A container packages the browser environment and its operating system dependencies for repeatable runs. Version alignment and security configuration still matter, especially when visiting untrusted sites.
Where can I confirm the current image tags and Docker security settings?
Use the official Playwright Docker documentation, which is updated as supported images and guidance change.


