ScreenshotNeo

BlogHow-to

How to Capture Webpage Screenshots on a Headless Ubuntu Server

Capture reliable webpage screenshots on Ubuntu without a desktop using Chrome, Puppeteer, or Playwright, with timing, sandbox, and troubleshooting guidance.

By the ScreenshotNeo team1 October 202610 min read

Yes, you can capture webpage screenshots on an Ubuntu server without a desktop environment. For a one-off capture, run Chrome in headless mode:

chrome --headless --screenshot --window-size=412,892 https://developer.chrome.com/

Chrome writes screenshot.png to the current working directory. For repeatable automation, use Puppeteer or Playwright so you can control navigation, waits, cookies, authentication, and output paths from code.

This guide covers the command-line method first, then complete Node.js examples, Ubuntu dependencies, timing, browser selection, sandboxing, reliability, performance, costs, and common failures.

1. Capture a screenshot with Chrome headless

Install Google Chrome or Chromium in your Ubuntu image, then run:

chrome --headless --screenshot --window-size=412,892 https://developer.chrome.com/

The --headless flag runs Chrome without displaying a browser UI. --screenshot captures the rendered page, and --window-size=WIDTH,HEIGHT sets the viewport in CSS pixels. The documented default output filename is screenshot.png in the current directory. See the Chrome Headless documentation for the current command-line reference.

Choose an explicit output directory

Chrome’s command-line default is the working directory. In a service, change into a job directory or move the result after capture so concurrent jobs cannot overwrite one another:

#!/usr/bin/env bash
set -euo pipefail

url="https://developer.chrome.com/"
out_dir="/var/tmp/screenshot-job"
mkdir -p "$out_dir"
cd "$out_dir"

chrome \
  --headless \
  --screenshot="page.png" \
  --window-size=1440,900 \
  "$url"

printf 'Wrote %s\n' "$out_dir/page.png"

Use a unique directory or filename when multiple workers run at once.

Wait for pages that need more time

Chrome documents --timeout as a maximum wait in milliseconds before capture flags proceed:

chrome \
  --headless \
  --screenshot=page.png \
  --window-size=1440,900 \
  --timeout=10000 \
  https://example.com/

--virtual-time-budget can fast-forward timer-driven page code:

chrome \
  --headless \
  --screenshot=page.png \
  --virtual-time-budget=5000 \
  https://example.com/

Neither option proves that every API request, animation, font, or lazy image is ready. Validate the chosen wait against the target application. A fixed delay is only a fallback when you cannot observe a real readiness condition.

2. Control the browser with Puppeteer

Puppeteer is useful when the screenshot is part of an application or scheduled job. Its normal installation flow downloads a compatible Chrome for Testing browser and a headless-shell binary. The project lists an approximate Linux browser download of about 282 MB; that figure is a download estimate, not a memory requirement.

Install

mkdir screenshot-job
cd screenshot-job
npm init -y
npm install puppeteer

Check the current Puppeteer installation guide and system requirements before pinning a production image. The current requirements page specifies Node 22.12 or later for the current Puppeteer release and lists Debian/Ubuntu on x64 and arm64 for Chrome for Testing.

Complete Puppeteer script

const puppeteer = require('puppeteer');

async function main() {
  const url = process.argv[2] || 'https://developer.chrome.com/';
  const output = process.argv[3] || 'screenshot.png';
  let browser;

  try {
    browser = await puppeteer.launch({
      headless: true,
      // Keep the browser sandbox enabled in normal deployments.
      args: []
    });

    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
    await page.screenshot({ path: output, fullPage: true, type: 'png' });
    console.log(`Saved ${output}`);
  } finally {
    if (browser) await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

networkidle2 waits until there are no more than two active network connections. Some sites keep analytics or streaming connections open, so use a selector or a bounded delay when network idle never occurs.

Wait for a page-specific readiness signal

await page.goto('https://example.com/dashboard', {
  waitUntil: 'domcontentloaded',
  timeout: 60000
});
await page.waitForSelector('[data-render-complete]', { timeout: 30000 });
await page.screenshot({ path: 'dashboard.png', fullPage: true });

Prefer a selector that your application sets after data and critical images are rendered. This is more reliable than guessing a universal sleep.

Useful Puppeteer options

Need Example
Viewport page.setViewport({width: 1280, height: 800, deviceScaleFactor: 2})
Full page page.screenshot({fullPage: true})
Element only const el = await page.$('.invoice'); await el.screenshot({path: 'invoice.png'});
JPEG quality page.screenshot({path: 'page.jpg', type: 'jpeg', quality: 85})
WebP page.screenshot({path: 'page.webp', type: 'webp', quality: 85})
Cookies await page.setCookie({name: 'session', value: 'TOKEN', domain: 'example.com'});
Custom header await page.setExtraHTTPHeaders({'Authorization': 'Bearer TOKEN'});
Hide an element await page.addStyleTag({content: '.cookie-banner{display:none!important}'})

3. Use Playwright when browser management or language choice matters

Playwright manages browser builds separately from your application. Install the Node package and a browser:

npm install playwright
npx playwright install --with-deps chromium

Playwright documents two relevant installation modes. npx playwright install --with-deps --only-shell installs only Chromium’s headless shell. npx playwright install --with-deps --no-shell installs regular Chromium; select the chromium channel when using the newer regular headless mode. Branded Google Chrome and Microsoft Edge channels are available but are not installed by default. Read the browser management documentation and validate fidelity when switching between a headless shell and branded Chrome or Edge.

Complete Playwright script

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  try {
    const page = await browser.newPage({
      viewport: { width: 1440, height: 900 },
      deviceScaleFactor: 1
    });
    await page.goto('https://developer.chrome.com/', {
      waitUntil: 'domcontentloaded',
      timeout: 60000
    });
    await page.screenshot({ path: 'playwright.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

Use a branded browser channel

const { chromium } = require('playwright');
const browser = await chromium.launch({ channel: 'chrome', headless: true });

Chrome and Edge headless behavior can differ from Playwright’s default Chromium headless shell. Pin the browser choice in your deployment and compare representative pages before changing it.

4. Ubuntu dependencies, versions, and sandboxing

Install dependencies from current documentation

Browser packages need system libraries for certificates, fonts, audio, accessibility, GTK, NSS, Pango, X11, and related components. The exact package list changes with Ubuntu releases and browser packaging. Follow the current Puppeteer troubleshooting guide and the requirements for the browser version you install instead of copying an old list blindly.

If installation scripts were disabled, the browser binary may be missing. Puppeteer’s documented manual recovery is:

npx puppeteer browsers install

Find a missing shared library

When Chrome fails with a shared-library error, inspect the actual executable:

ldd /path/to/chrome | grep not

Also verify the executable path, browser version, CPU architecture, and the Ubuntu image. A mismatch between those values often explains a launch failure.

Keep the Chrome sandbox enabled

Run Chrome with its sandbox in normal deployments. Puppeteer strongly discourages disabling it. Treat --no-sandbox as an emergency option only for absolutely trusted content and an isolated environment, not as a routine fix for public URLs.

Ubuntu 23.10 and later may ship an AppArmor profile for Chrome stable at /opt/google/chrome/chrome. That policy can prevent Puppeteer-downloaded Chrome for Testing binaries from using user namespaces and lead to No usable sandbox!. Diagnose the installed binary, host policy, and current upstream guidance before changing security settings.

5. Rendering details that affect screenshot fidelity

Viewport and device scale

Viewport dimensions control responsive breakpoints. Device scale controls the pixel density of the output. A 1440×900 viewport at scale 2 produces a larger bitmap than scale 1; choose deliberately because memory and file size increase.

Lazy-loaded content

Full-page capture does not guarantee that every lazy image has loaded. Scroll through the page before capture when using Puppeteer or Playwright:

await page.evaluate(async () => {
  await new Promise((resolve) => {
    let y = 0;
    const step = 600;
    const timer = setInterval(() => {
      window.scrollBy(0, step);
      y += step;
      if (y >= document.body.scrollHeight) {
        clearInterval(timer);
        window.scrollTo(0, 0);
        resolve();
      }
    }, 100);
  });
});
await page.screenshot({ path: 'lazy-loaded.png', fullPage: true });

For a production workflow, wait for image completion or an application-specific ready marker as well.

Fonts, animations, and dynamic content

Missing fonts change line wrapping and therefore page height. Install the fonts your design uses or wait for document.fonts.ready. Disable animations when deterministic output matters:

await page.addStyleTag({
  content: '* { animation: none !important; transition: none !important; }'
});
await page.evaluate(() => document.fonts.ready);

Authentication and private pages

Supply cookies or headers before navigation, and never print credentials into logs. Use a short-lived service account where possible. If the page redirects to a login screen, inspect the final URL and response status before saving the screenshot.

6. cURL, Python, and Node.js alternatives

For a self-hosted browser, the Chrome CLI is the shortest shell interface. For an API-driven workflow, these generic HTTP examples show how a client might download an image from a screenshot service. Replace the endpoint and parameters with the service you operate.

cURL

curl -L "https://example-screenshot-service.invalid/capture?url=https%3A%2F%2Fexample.com" -o shot.png

Python

import requests

r = requests.get(
    "https://example-screenshot-service.invalid/capture",
    params={"url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.png", "wb") as f:
    f.write(r.content)

Node.js

const q = new URLSearchParams({ url: 'https://example.com' });
const res = await fetch(`https://example-screenshot-service.invalid/capture?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('fs').writeFileSync('shot.png', Buffer.from(await res.arrayBuffer()));

7. Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF, so your Ubuntu server does not need Chrome binaries, GUI packages, or browser sandbox configuration.

See the ScreenshotNeo API documentation for all options. Basic calls:

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}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and whether the request was billed. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

8. Reliability and performance checklist

  • Pin Node, browser, and automation-library versions in the deployment image.
  • Use a unique output path per job.
  • Set navigation and overall job timeouts.
  • Close the browser in a finally block.
  • Wait for a selector, font readiness, image completion, or another page-specific signal.
  • Record the final URL, browser version, viewport, and failure reason.
  • Limit concurrent browsers according to available CPU and memory.
  • Reuse a browser process for batches, while creating isolated contexts or pages for separate users.
  • Keep sandboxing enabled and investigate dependency or AppArmor errors first.
  • Use JPEG or WebP when smaller files matter; use PNG for lossless output and sharp text.

Launching a fresh browser for every URL adds startup time and memory use. Reusing a controlled browser process improves throughput, but reset cookies, local storage, permissions, and pages between jobs to prevent data leakage.

9. Common errors and fixes

Error or symptom Likely cause Fix
chrome: command not found Chrome is absent or not on PATH. Install Chrome/Chromium, use its absolute path, or let Puppeteer/Playwright install a managed browser.
Could not find Chrome Puppeteer browser download was skipped. Run npx puppeteer browsers install and verify the configured executable.
Missing shared library Ubuntu dependencies do not match the browser package. Run ldd /path/to/chrome | grep not; install dependencies from current project and browser documentation.
No usable sandbox! User namespaces or AppArmor policy prevent the browser sandbox from starting. Check the binary path, host policy, and current Puppeteer guidance. Keep the sandbox enabled; do not make --no-sandbox the default.
Blank or partially rendered image Capture happened before application data, fonts, or lazy images finished. Wait for a readiness selector, document.fonts.ready, image completion, or a validated bounded delay.
Wrong mobile or desktop layout Viewport is missing or too small for the intended breakpoint. Set explicit width and height before navigation.
Full-page image is unexpectedly short Content is virtualized or lazy-loaded only near the viewport. Scroll to load content, wait for the list to settle, or use an application export.
Navigation timeout Slow origin, never-ending requests, or blocked third-party resources. Set a realistic timeout, use domcontentloaded, then wait for a page-specific signal.
Different output after browser upgrade Rendering engines, fonts, or headless modes changed. Pin versions, compare representative pages, and update visual baselines deliberately.

10. Cost and operational trade-offs

Self-hosting has no per-screenshot vendor charge, but you operate browser downloads, Ubuntu dependencies, CPU, memory, storage, updates, sandbox policy, retries, and monitoring. Browser binaries can be large, and concurrency increases resource use.

A hosted API exchanges that maintenance for usage pricing. ScreenshotNeo’s plans are Free (1,000 shots/month), Starter ($5 for 3,000), Growth ($15 for 15,000), Pro ($39 for 60,000), Scale ($99 for 250,000), and Business ($249 for 1,000,000); yearly billing gives two months free. Every feature is available on every plan. Because failed loads, blank pages, bot checks, timeouts, and cache hits are not billed, inspect the response verdict and billing headers when estimating usage.

FAQ

Does headless mode require X11 or a desktop?

No. Modern Chrome headless runs without a visible browser UI. You still need the browser’s shared libraries and fonts.

Should I choose Puppeteer or Playwright?

Choose the framework that matches your language, existing tests, browser-version policy, and required Chrome or Edge fidelity. Both require managed browser binaries and Linux dependencies.

Is a fixed five-second delay enough?

Not universally. A selector, network condition, font readiness, or application event is a better readiness signal. Validate any delay against the target page.

Can I capture a private page?

Yes, when your automation supplies the required cookies or headers and the page is accessible from the server. Protect credentials and verify that redirects do not lead to a login page.

When should I use an API instead of running Chrome?

Use an API when you want to avoid browser installation and maintenance, need many captures, or want an MCP workflow for AI agents. Use a local browser when you need complete control over the runtime and network.