Puppeteer Screenshot Is Blank on Ubuntu Server: Troubleshooting
A blank Puppeteer screenshot is a symptom, not a diagnosis. Check the page Chrome reached, readiness, Ubuntu dependencies, sandboxing, capture settings, and saved image bytes.
A blank Puppeteer screenshot on an Ubuntu server can mean Chrome captured the wrong page, captured before an app finished rendering, could not load a resource, or produced an image that your code saved or displayed incorrectly. It can also point to missing browser libraries or a Linux sandbox problem. Start by inspecting the URL, response, title, and expected page content before changing server packages.
The quickest way to separate these causes is to capture a minimal local page, then compare its result with the target site. Use the diagnostic script below to log what Chrome actually reached and wait for a page-specific element before taking the screenshot.
1. Log the page Puppeteer actually reached
A resolved page.goto() promise does not prove that the expected page appeared. The navigation may have ended at a redirect, login screen, bot challenge, proxy response, browser warning, or HTTP error page. Log the final URL, response status, title, a short text sample, and a selector that should exist on the target.
Page.goto() returns the main resource response; after redirects, that is the response for the final destination. It can return null for about:blank and same-document hash navigation. Also inspect status explicitly: headless shell does not necessarily throw for valid HTTP statuses such as 404 or 500. See the Puppeteer Page.goto API.
// diagnose.mjs
import puppeteer from 'puppeteer';
const target = process.env.TARGET_URL ?? 'https://example.com';
const readySelector = process.env.READY_SELECTOR; // Set to a selector meaningful for your page.
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
page.on('console', message => console.log(`[console:${message.type()}]`, message.text()));
page.on('pageerror', error => console.error('[pageerror]', error.message));
page.on('requestfailed', request => console.error('[requestfailed]', request.url(), request.failure()?.errorText));
const response = await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60000 });
console.log({
requestedUrl: target,
finalUrl: page.url(),
status: response?.status() ?? null,
title: await page.title(),
bodyText: (await page.locator('body').innerText().catch(() => '')).slice(0, 500),
});
if (readySelector) {
await page.waitForSelector(readySelector, { visible: true, timeout: 30000 });
}
await page.screenshot({ path: 'shot.png', fullPage: true });
console.log('Saved shot.png');
} finally {
await browser.close();
}
Run it with TARGET_URL=https://your-site.example READY_SELECTOR='main' node diagnose.mjs. If your shell does not support that environment-variable syntax, set those variables through your process manager or deployment configuration. Replace main with a selector that represents the content you expect, such as a page-specific heading or result container.
Chrome for Testing can also show an interstitial for some HTTP navigations. Puppeteer documents cases that result in net::ERR_BLOCKED_BY_CLIENT; inspect the final URL and body rather than assuming the page rendered as intended. See Puppeteer troubleshooting.
2. Wait for the page’s real readiness condition
Navigation lifecycle events describe browser loading milestones; they do not promise that a client-rendered app has populated its final content. Prefer a meaningful selector or app-specific readiness condition:
await page.goto('https://your-site.example', {
waitUntil: 'domcontentloaded',
timeout: 60000,
});
await page.waitForSelector('[data-testid="report-ready"]', {
visible: true,
timeout: 30000,
});
await page.screenshot({ path: 'report.png', fullPage: true });
Use waitForNetworkIdle() if network quiet is a useful signal for the page, but do not treat it as a universal definition of ready. The method waits for network idleness for at least its configured idle time; an app may still need fonts, animations, client-side work, or a particular data state. Conversely, analytics or long polling can prevent a page from becoming idle. The Page API documents navigation and waiting methods.
For diagnosis, compare the output after the expected selector appears with a capture after network idle. Log console errors and failed requests alongside both captures. A fixed sleep can be useful as a brief experiment, but it is not a dependable readiness condition: pages and server response times vary.
3. Check Chrome installation and runtime compatibility
The puppeteer package normally downloads a compatible Chrome for Testing browser. If install scripts were blocked by a package manager or deployment build, that download may have been skipped. Install the browser explicitly with npx puppeteer browsers install or configure the package manager to allow Puppeteer’s install script, following the current Puppeteer installation guide.
puppeteer-core does not download a browser. When using it, provide the path to a browser you manage or a supported channel. Log the exact binary path and versions from the same deployed environment that runs the screenshot job.
node --version
npm ls puppeteer puppeteer-core
which google-chrome || true
which chromium || true
npx puppeteer browsers list
Compare the deployed Node and browser versions with the current Puppeteer system requirements. At the time covered by the research for this article, that guide lists Node 22.12 or newer and Debian/Ubuntu x64 and arm64 support for Chrome for Testing. Requirements can change; use the current guide when updating a deployment rather than relying on a copied version number.
4. Check Ubuntu libraries and fonts
Run the dependency check against the actual Chrome executable used by Puppeteer:
ldd /path/to/chrome | grep 'not found' || true
If it reports missing libraries, compare those exact names and the browser build with the current Puppeteer Linux guidance and Chromium’s package manifest. Commonly listed Debian and Ubuntu dependencies include NSS, GBM, GTK, Pango, X11-related libraries, and Liberation fonts, but the right package set depends on the browser version and image. Avoid installing a stale package list by guesswork. See Puppeteer’s Linux troubleshooting guide and system requirements.
Missing fonts usually cause absent glyphs, fallback typography, or changed line wrapping rather than a completely white page. Treat fonts as one rendering check, especially if the page background and layout appear but text does not.
5. Diagnose sandbox and AppArmor errors separately
If Chrome’s logs say No usable sandbox!, investigate the sandbox setup rather than changing screenshot timing. Chrome’s Linux sandbox isolates web content. Puppeteer documents an Ubuntu 23.10-and-newer AppArmor interaction: an AppArmor profile for Chrome stable at /opt/google/chrome/chrome can interfere with user namespaces used by Puppeteer-downloaded Chrome for Testing. Check the Ubuntu release, actual browser path, and current upstream AppArmor guidance alongside the Puppeteer troubleshooting instructions.
Puppeteer warns: “Running without a sandbox is strongly discouraged. Consider configuring a sandbox instead.” Do not make --no-sandbox a routine production fix. If you use it as a narrowly scoped diagnostic with trusted content, understand that it reduces isolation and do not leave it as an unexplained default.
6. Verify headless mode and display requirements
Puppeteer runs headless by default. A server does not need a physical monitor for normal headless screenshots. If your launch options explicitly set headless: false, Chrome is headful and a non-graphical Ubuntu host may need a display server such as Xvfb. That is a separate issue from a blank capture in headless mode. See the Puppeteer headless modes guide.
7. Inspect viewport, clipping, and saved image bytes
A page can render correctly while the screenshot excludes its content or the application mishandles the result. Check all of these:
viewport: Set a deliberate width and height before navigation or capture if responsive breakpoints matter.clip: Remove or verify any clip rectangle that may be outside the content.fullPage: It defaults tofalse; set it totruewhen the page extends below the viewport.captureBeyondViewport: Check this when using a clip or relying on content outside the viewport.omitBackground: This controls background omission and transparency; view the output against a contrasting background.typeandencoding: PNG is the default output type. Screenshot data is binary by default; base64 mode returns a string that must be decoded before writing a file.path: Confirm the process writes where you expect, has permission, and that a later job is not replacing the file.- Capture timing: Do not close or change the page while a screenshot call is in progress.
These defaults and controls are documented in ScreenshotOptions. Save the returned bytes directly in binary mode, then inspect the file type and dimensions with tools available in your environment, for example file shot.png. Open it in a known image viewer. A transparent image can look blank on a viewer with an unexpected background.
8. Isolate the environment from the target site
Capture a tiny local document using the same browser, process user, and deployment image. If it works, that is evidence that browser startup and basic screenshot output work in that environment; investigate target-specific navigation, scripts, assets, authentication, or access controls next. If the local document is also blank or Chrome fails to launch, focus on the browser installation, shared libraries, sandbox, and output handling.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent('<!doctype html><body style="margin:0;background:#f2f2f2"><h1>Capture check</h1></body>');
await page.screenshot({ path: 'local-check.png' });
} finally {
await browser.close();
}
This is a diagnostic comparison, not proof of a single root cause. If the local image works but the target fails, revisit the logged final URL, response, body text, selector wait, console, and failed requests.
9. Common errors and fixes
| Symptom | Likely area | What to check |
|---|---|---|
| Image is entirely white, but Chrome completed | Wrong page, early capture, or output interpretation | Log final URL, status, title, body text and expected selector; wait for app readiness; inspect bytes and transparency. |
| Screenshot shows login, challenge, or browser warning | Redirect, access control, or interstitial | Check response status, final URL, cookies/authentication, proxy behavior, and actual body content. |
| App shell appears without data | Client app not ready or API requests failed | Wait for a meaningful ready selector; inspect console errors and failed requests; verify the browser can reach the app’s APIs. |
Navigation timeout |
Slow or never-ending navigation | Choose an appropriate lifecycle event, set a justified timeout, and wait separately for the content you need. Check network access and redirects. |
No usable sandbox! |
Linux sandbox or AppArmor configuration | Check Ubuntu version, actual Chrome binary, and current Puppeteer/AppArmor guidance. Do not reflexively disable the sandbox. |
error while loading shared libraries |
Missing Ubuntu runtime dependency | Run ldd on the actual browser and install confirmed dependencies based on current browser guidance. |
| Text is missing or different | Font availability or font readiness | Check installed fonts and whether web fonts loaded before capture; distinguish glyph issues from a fully blank page. |
| Only part of the page is present | Viewport, clip, or full-page setting | Inspect viewport and clip; set fullPage: true when needed. |
| File is zero bytes, corrupt, or unexpectedly old | Write path, encoding, permissions, or concurrent job | Await screenshot completion, write binary bytes correctly, verify path and permissions, and avoid concurrent overwrite. |
| Headful browser cannot start on server | No display server | Use normal headless mode or configure a display server if headful behavior is required. |
10. Reliability, performance, and cost considerations
For a reliable capture job, log the browser and Puppeteer versions, executable path, requested and final URL, response status, timing, page errors, failed requests, and output dimensions. Wait for a condition tied to the content you need. Set timeouts based on the page and job deadline, and handle a missing selector as a failed capture rather than silently saving an image that looks valid but is empty.
Full-page screenshots may require more rendering and memory than viewport captures, especially on very long pages. Use only the dimensions and format you need, and avoid launching more simultaneous browser work than the server can support. Reuse decisions should account for cleanup and isolation: always close pages and browsers when jobs finish, and do not let one failed page stall unrelated jobs.
No topic-specific benchmark or failure-rate figure is available in the cited documentation, so capacity and timeouts should be measured against your own pages and deployment. Puppeteer itself has no per-screenshot charge stated in the sources used here; operational costs come from the compute, memory, storage, and maintenance required to run the browser infrastructure.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF, without installing and maintaining Chrome on your Ubuntu server. Its capture flow removes cookie banners, popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card, with paid plans starting at $5 for 3,000.
For the available parameters and options, see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Get started with 1,000 free screenshots a month, with no card required.
FAQ
Does a server need a monitor to take a Puppeteer screenshot?
No. Normal Puppeteer headless mode runs without a physical monitor. A display server is relevant when explicitly running headful Chrome.
Should I always use networkidle before a screenshot?
No. Use it when network quiet is meaningful for the target. A page-specific selector or application readiness signal is often more directly tied to the content you need.
Can a successful navigation still show an error page?
Yes. Check the response status and actual rendered content; a resolved navigation is not proof that the intended page loaded successfully.
What should I collect before changing the server image?
Record the requested and final URL, status, title, body text, expected selector state, console and request errors, Node/Puppeteer/browser versions, executable path, and screenshot file details.


