Taking Screenshots in a Headless Linux Environment
Capture reliable webpage screenshots on Linux without a display server using Chrome, Playwright, Puppeteer, or ScreenshotNeo.
Chrome and Chromium can capture a webpage directly from a Linux command line without a display server:
chrome --headless --screenshot --window-size=1280,900 https://example.com
The command writes screenshot.png to the current directory. Use --window-size to choose the viewport and --timeout to limit how long Chrome waits before capturing. For scripted navigation, full-page output, element screenshots, clipping, authentication, or readiness checks, use Playwright or Puppeteer.
1. Capture a page with Chrome or Chromium
Chrome Headless does not require X11, Wayland, or another display server for webpage capture. Check which browser binary is installed:
command -v google-chrome || command -v chromium || command -v chromium-browser
chrome --version 2>/dev/null || chromium --version
Basic capture:
chrome --headless --screenshot --window-size=1280,900 https://example.com
Choose an output filename with a shell move:
chrome --headless --screenshot --window-size=1280,900 https://example.com
mv screenshot.png example-home.png
Wait up to 10 seconds before capturing:
chrome --headless --screenshot --window-size=1280,900 --timeout=10000 https://example.com
Use an explicit URL scheme. Quote URLs containing ampersands, question marks, or shell metacharacters:
chrome --headless --screenshot --window-size=1440,900 'https://example.com/search?q=linux&sort=new'
The screenshot represents the rendered viewport at the selected size. A viewport capture does not automatically mean the entire document is included. For full-page output or application-specific waiting, use an automation API.
2. Full-page and element screenshots with Playwright
Playwright exposes viewport, element, and full-page screenshot controls. A minimal Node.js script:
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({
viewport: { width: 1280, height: 900 },
deviceScaleFactor: 1
});
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();
Install Playwright in a project, then install its browser binaries according to the Playwright screenshot documentation. Capture one element instead of the whole page:
const card = page.locator('[data-testid="pricing-card"]');
await card.screenshot({ path: 'pricing-card.png' });
Wait for a known application state rather than relying only on a fixed delay:
await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
await page.locator('#dashboard-ready').waitFor({ state: 'visible' });
await page.screenshot({ path: 'dashboard.png', fullPage: true });
Other useful controls include clipping a rectangle, setting a transparent background where supported, and selecting an image type such as PNG or JPEG. Keep the browser version, operating system, viewport, scale factor, fonts, and power conditions consistent when comparing screenshots; Playwright documents that rendering can vary across these factors in its visual comparison guidance.
3. Full-page and clipped screenshots with Puppeteer
Puppeteer provides equivalent controls through its screenshot API. The following script captures a full document:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'page.png', fullPage: true, type: 'png' });
await browser.close();
Capture a specific region:
await page.screenshot({
path: 'header.jpg',
type: 'jpeg',
quality: 85,
clip: { x: 0, y: 0, width: 1280, height: 240 }
});
See Puppeteer’s ScreenshotOptions reference for full-page, clipping, transparency, output type, quality, and path options.
4. Make dynamic pages deterministic
Dynamic pages may still be rendering when a command-line capture occurs. Prefer a readiness condition tied to the page:
- Wait for a selector that appears after the main content is rendered.
- Wait for a specific network state when the application has predictable requests.
- Use a short delay only for animations, lazy images, or third-party widgets that have no reliable selector.
- Disable animations in test captures with injected CSS.
await page.addStyleTag({ content: `
*, *::before, *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}
` });
Lazy-loaded images often require scrolling before a full-page capture. A simple Playwright approach:
await page.evaluate(async () => {
await new Promise(resolve => {
const step = 600;
const timer = setInterval(() => {
window.scrollBy(0, step);
if (window.innerHeight + window.scrollY >= document.body.scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
await page.waitForTimeout(500);
await page.screenshot({ path: 'lazy-loaded.png', fullPage: true });
5. Viewport, device scale, and output choices
| Decision | What it changes | Typical use |
|---|---|---|
| Viewport size | Responsive layout and visible area | Desktop, tablet, or mobile snapshots |
| Device scale factor | Pixel density and output dimensions | Retina-style assets or visual tests |
| Viewport capture | Only the visible viewport | Hero sections and above-the-fold previews |
| Full-page capture | Scrollable document length | Documentation, invoices, and audit archives |
| Element capture | Bounds of one selector or locator | Cards, charts, and components |
| PNG | Lossless output, larger files | Pixel comparison and text-heavy images |
| JPEG | Lossy output with quality control | Photographic pages and smaller files |
6. Containers, permissions, and resource limits
In containers or CI, the browser user needs a writable temporary directory and permission to launch the sandbox. Prefer running as a non-root user and follow your browser package’s security guidance. If your environment cannot use the sandbox, Chromium may require an environment-specific launch configuration; treat --no-sandbox as a last resort because it reduces isolation.
Allocate enough shared memory and CPU for large pages. Crashes, missing fonts, and incomplete screenshots are common when the container is undersized. Install the fonts required by the page and keep browser binaries pinned for repeatable output.
7. Chrome Headless version differences
Chromium documents that, as of M132, the old Headless shell functionality is no longer part of the regular Chrome binary; projects depending on that old behavior should migrate to chrome-headless-shell. Verify the binary and version available in the Linux image before copying a command into production. See the Chromium Headless documentation.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
command not found |
Chrome or Chromium is not installed, or the binary has another name. | Use command -v to locate it, then call that path explicitly. |
| Blank or half-rendered image | Capture happened before application content or fonts loaded. | Use Playwright or Puppeteer and wait for a selector, network state, or a short page-specific delay. |
| Full page is truncated | A viewport screenshot was requested. | Use Playwright fullPage: true or Puppeteer fullPage: true. |
| Lazy images are missing | Images load only after scrolling into view. | Scroll through the document, wait for image requests, then capture. |
| Fonts differ from local screenshots | Fonts are absent or rendering environments differ. | Install and pin required fonts and browser versions; use the same OS image for comparisons. |
| Browser exits in CI | Insufficient shared memory, CPU, permissions, or sandbox support. | Increase resources, run as a suitable non-root user, and inspect browser stderr and container limits. |
| Consent banner or chat widget covers content | The page displays third-party overlays during capture. | Dismiss or hide the overlay in automation, or use a capture service that handles common consent and widget layers. |
| CAPTCHA or bot check appears | The target is challenging automated browsers. | Do not attempt to bypass access controls; capture only pages you are authorized to access and handle the failure explicitly. |
9. Reliability, performance, and cost
- Reliability: Pin browser versions, fonts, viewport settings, and timezone. Record the URL, commit or build identifier, and capture timestamp with each artifact.
- Performance: Reuse a browser process for batches, create isolated pages or contexts, block unnecessary resources when they are irrelevant, and avoid waiting for a global network-idle state on pages with long-lived connections.
- Large pages: Full-page screenshots consume memory proportional to document dimensions. Capture sections or elements when an entire document is unnecessary.
- Cost: Self-hosted Chrome, Playwright, and Puppeteer have no per-shot API fee, but they consume CI or server CPU, memory, storage, and maintenance time. A hosted API trades browser operations for a request-based price and simpler scaling.
10. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF, while the service can handle browser setup and capture options for you. The API supports full-page shots with lazy images loaded, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, clicks, selector or delay waits, network-idle waits, request blocking, headers, cookies, user agents, Authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage data, and PDF settings.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const bytes = await res.arrayBuffer();
await Bun.write('shot.webp', bytes);
See the ScreenshotNeo documentation for request parameters and response handling. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers identify the page verdict and whether the shot was billed. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan.
Create a free ScreenshotNeo account with 1,000 screenshots per month and no card.
11. FAQ
Do I need Xvfb for Chrome Headless?
No. Chrome Headless webpage capture does not require a display server. Xvfb is relevant to workflows that need a graphical desktop or non-headless capture tools.
Which tool should I choose for one URL?
Use the Chrome command for a quick viewport image. Choose Playwright or Puppeteer when the page needs scripted waits, interactions, full-page output, or element targeting.
Why does the same page differ between machines?
Browser version, operating system, fonts, viewport, scale factor, hardware, power conditions, and headless mode can affect rendering. Standardize those inputs for comparisons.
Can a screenshot include a PDF instead of an image?
Chrome and browser libraries focus on rendered browser output. ScreenshotNeo also supports PDF capture with paper size, margins, landscape mode, and page ranges.


