Capture a Website Screenshot in Node.js with Puppeteer on an Ubuntu VPS in India
Install Puppeteer and its browser on Ubuntu, capture a page or element in Node.js, and fix common headless Chrome launch problems on a VPS.
To capture a website screenshot with Puppeteer on an Ubuntu VPS, install a supported Node.js version and the puppeteer package, ensure Chrome for Testing and its Linux libraries are available, then launch a headless browser, navigate to the URL, wait for the page state you need, and call page.screenshot(). Puppeteer runs headless by default. Keep Chrome’s sandbox enabled where possible; investigate missing libraries and host sandbox configuration before considering any sandbox-disabling workaround.
The steps are the same on an India-based VPS as elsewhere. The server’s network location can affect websites that vary content by region, so check the rendered result from the actual VPS if location-sensitive content matters. Puppeteer’s current system requirements list Node.js 22.12+ and Chrome for Testing on Debian/Ubuntu x64 and arm64; requirements can change, so check the current Puppeteer system requirements before deployment.
1. Check the Ubuntu VPS and install prerequisites
Connect to the VPS over SSH and check the operating system, CPU architecture, and Node.js version:
cat /etc/os-release
uname -m
node --version
npm --version
The documented Linux architectures are x64 and arm64. If Node.js is missing or older than the current Puppeteer requirement, install a supported Node.js release using your preferred maintained installation method, then reconnect or reload your shell and check node --version. Avoid assuming a package named nodejs from an old Ubuntu repository meets the current version floor.
On a minimal server, Chrome may need system libraries and fonts that are not installed by default. Puppeteer’s troubleshooting documentation lists Debian/Ubuntu dependencies and explains how to check a Chrome binary for missing shared libraries. The exact packages can vary with the Ubuntu release and installed software, so use the current Linux troubleshooting instructions when launch fails.
2. Create a Node.js project and install Puppeteer
In a new project directory, initialize npm and install puppeteer:
mkdir website-shot
cd website-shot
npm init -y
npm install puppeteer
The puppeteer package downloads a compatible Chrome for Testing browser during installation by default. On Linux, the browser download is substantial, so allow enough disk space and network access during deployment. Package managers or deployment pipelines that block install scripts can skip this download. If that happens, run the documented manual browser installation command:
npx puppeteer browsers install
See the official Puppeteer installation guide for package-manager and browser-download configuration. If you choose puppeteer-core instead, it does not download Chrome; you must manage the browser yourself and supply an executable path or connect to a browser that supports the DevTools protocol.
3. Capture a full-page screenshot
Create screenshot.mjs in the project directory. This runnable example accepts the target URL and optional output filename from the command line, waits for navigation, and closes Chrome even if navigation or capture fails:
import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://example.com';
const outputPath = process.argv[3] ?? 'screenshot.png';
let browser;
try {
browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({
width: 1440,
height: 1000,
deviceScaleFactor: 1,
});
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 60_000,
});
await page.screenshot({
path: outputPath,
fullPage: true,
type: 'png',
});
console.log(`Saved ${outputPath}`);
} finally {
if (browser) await browser.close();
}
Run it with:
node screenshot.mjs https://example.com example.png
Page.screenshot() is Puppeteer’s page capture method. The official example uses networkidle2 as one possible navigation readiness condition; it is not proof that every site has finished rendering. Pages that poll continuously, load content after an interaction, or defer images need a site-specific readiness condition. The Puppeteer screenshot guide also documents capturing a specific element.
Choose when the page is ready
page.goto() supports lifecycle conditions such as load, domcontentloaded, networkidle0, and networkidle2. Use a condition that matches the site rather than mechanically choosing the longest wait. Network-idle conditions can time out on pages with persistent requests. For a known page, waiting for a meaningful selector is often more reliable:
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForSelector('[data-page-ready="true"]', { timeout: 15_000 });
If the site has no readiness marker, wait for a specific heading, image, or other stable element that signals the content you need is rendered. A fixed delay is possible but less reliable: it can waste time on fast runs and still be too short on slow ones.
Set the viewport and output type
The viewport controls responsive layout; it does not set the full-page output dimensions. Use page.setViewport() before navigation when the site reads viewport information during startup. A larger deviceScaleFactor creates a higher-density image and uses more memory and disk. PNG is lossless; JPEG is usually smaller for photographic content and accepts a quality value; WebP can also be requested when supported by the Puppeteer version in use. Example JPEG capture:
await page.screenshot({
path: 'screenshot.jpg',
type: 'jpeg',
quality: 85,
fullPage: true,
});
For a viewport-only screenshot, omit fullPage or set it to false. Full-page capture can produce very tall images, and extremely long pages may consume substantial memory or encounter browser limits. For long pages, consider capturing selected sections or using PDF output if that better fits the task.
4. Capture one element instead of the whole page
Wait for the element, then call its screenshot method. This example writes the first matching element to a PNG file:
const card = await page.waitForSelector('.product-card', { timeout: 15_000 });
if (!card) throw new Error('Product card was not found');
await card.screenshot({ path: 'product-card.png' });
ElementHandle.screenshot() scrolls the element into view when needed. Check that your CSS selector uniquely identifies the intended element; when several elements match, select the right one explicitly. See the official screenshot guide for page and element capture examples.
5. Account for dynamic pages and repeatable output
A successful navigation does not guarantee the screenshot contains the final application state. Common sources of variation include client-side rendering, lazy-loaded images, animation, consent dialogs, personalized content, and fonts that load after the initial document. Choose the steps appropriate to the target:
- Wait for a stable selector or application-specific ready signal.
- Scroll through the page if content or images load only when they approach the viewport, then wait for the relevant images to complete.
- Use a consistent viewport, device scale factor, locale, and timezone if you need comparable captures.
- Disable animation or hide known overlays with page-level CSS only when doing so matches your capture requirements.
- Use a fresh page or browser context when cookies and local storage from earlier captures could change the result.
For example, after scrolling to the bottom to trigger lazy loading, wait for image elements to finish or fail before capture:
await page.evaluate(async () => {
const step = Math.max(300, window.innerHeight);
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 100));
}
window.scrollTo(0, 0);
});
await page.waitForFunction(() =>
[...document.images].every(image => image.complete),
{ timeout: 15_000 },
);
This is a practical pattern, not a guarantee for every lazy-loading implementation. Some sites load additional content in response to scrolling or use background images, so check the page’s own behavior.
6. Run the script as a VPS service or scheduled job
For a one-off job, run the script from the project directory as the same Linux user that installed the package and browser. For a recurring job, use a process manager or scheduler appropriate to your environment, set an explicit working directory, and ensure the service user can read the project and write the screenshot destination.
Puppeteer’s browser cache is normally under the installing user’s home cache directory. If installation runs as one user and capture runs as another, the runtime user may not find the browser. Keep installation and execution users consistent, or configure and provision a shared browser cache deliberately. Ensure output directories are writable, and avoid writing untrusted URL-derived filenames without validation.
Run Chrome as an unprivileged service user where practical, preserve its sandbox, and limit which URLs the script can visit if callers can supply URLs. A screenshot endpoint that accepts arbitrary URLs can otherwise be misused to request internal services; allowlist destinations or apply network controls appropriate to the application.
7. Troubleshooting Puppeteer on Ubuntu
| Symptom | Likely cause | What to do |
|---|---|---|
Could not find Chrome (ver. ...) |
The Puppeteer install script was blocked, the browser was not downloaded, or runtime uses a different user/cache. | Run npx puppeteer browsers install during deployment. Check the install logs and confirm the execution user can access the Puppeteer browser cache. |
| Chrome exits immediately or reports a shared library error | A required Ubuntu library is missing, or the browser architecture does not match the VPS. | Check uname -m, verify supported architecture, and inspect missing libraries with ldd /path/to/chrome | grep 'not found'. Install the current Debian/Ubuntu dependencies listed in the Puppeteer troubleshooting guide. |
No usable sandbox! |
The host sandbox configuration may prevent Chrome from creating its sandbox. Puppeteer documents a possible AppArmor interaction on Ubuntu 23.10+ for downloaded Chrome for Testing binaries. | Keep the sandbox enabled and investigate the host’s user-namespace/AppArmor configuration using the documented Ubuntu AppArmor notes. Disabling the sandbox is strongly discouraged because it removes an isolation layer for web content. |
| Works in SSH, fails under systemd or a scheduler | The service has a different user, home directory, environment, working directory, permissions, or cache path. | Set the service’s working directory and runtime user explicitly. Log process.cwd(), process.env.HOME, and the browser launch error; ensure the service user has a provisioned browser cache and writable output directory. |
| Navigation times out on an otherwise visible page | The page keeps network connections open, or the selected lifecycle condition is too strict for the site. | Navigate with domcontentloaded or load, then wait for a meaningful selector. Set a timeout appropriate to the workload; do not treat a timeout extension as a substitute for a readiness condition. |
| Screenshot is blank, missing content, or shows a spinner | The app renders after navigation, content is lazy-loaded, or capture happened before a relevant selector appeared. | Wait for a page-specific ready selector or content condition. Scroll to trigger lazy content where needed, and check the result from the target VPS. |
| Fonts or non-Latin characters render incorrectly | Required fonts are not installed on the minimal server. | Install appropriate fonts for the scripts and languages on the page, then restart the browser process. Verify glyphs in the captured output. |
| Process hangs or memory usage grows | Browser/page cleanup is missing, too many captures run concurrently, or full-page images are unusually large. | Close each browser in a finally block, bound concurrency, and reduce viewport scale or capture only the needed area. Monitor memory and disk use for the actual pages being processed. |
Do not add --no-sandbox as a routine launch flag. Puppeteer’s documentation calls running without the sandbox strongly discouraged. If you are forced to consider it for a trusted, isolated workload, understand that it weakens protection against untrusted page content and treat it as a deliberate security tradeoff, not a generic Ubuntu fix.
8. Performance, reliability, and cost considerations
- Browser startup: Launching Chrome has a fixed cost. For repeated captures in one trusted process, reusing a browser can reduce startup work, but isolate pages or browser contexts so cookies and state do not leak between jobs. Restart browsers periodically according to observed resource behavior.
- Concurrency: Each active page and browser consumes CPU and memory. Start with bounded concurrency and adjust from measurements on the selected VPS; no single safe number applies across page complexity and server sizes.
- Image size: Full-page, high-scale screenshots use more memory and storage. Choose viewport, scale, format, and JPEG quality based on whether crisp text or smaller photographic output matters more.
- Network and region: Each capture loads the target site from the VPS. The India location may affect latency or geographically varied content; test the exact deployment path for pages where that matters.
- Reliability: Use finite navigation and selector timeouts, close browsers on all code paths, record the target URL and failure stage, and retry only transient failures with a bounded policy. A retry should not repeat forever against a broken page.
- Cost: Self-hosting means accounting for VPS compute, disk, network transfer, browser downloads, and maintenance. The research sources do not establish a VPS provider, India-region price, or workload-specific cost, so compare current provider terms against your expected capture volume.
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 install or maintain Chrome on this VPS. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Node.js:
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 Bun.write('shot.webp', res);
For standard Node.js without Bun, write the response body with Node’s filesystem API:
import { writeFile } from 'node:fs/promises';
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 writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
See the ScreenshotNeo API documentation for request options. The API supports full-page and element capture, PDF, custom CSS and JavaScript, viewport and device presets, waits, request blocking, headers and cookies, caching, async jobs, bulk capture, signed links, and more. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month—no card required.
10. Frequently asked questions
Does Puppeteer need a desktop environment on an Ubuntu VPS?
No. Puppeteer runs headless by default, so a graphical desktop is not required for ordinary screenshot capture. Chrome still needs its system libraries and an appropriate sandbox configuration.
Should I use puppeteer or puppeteer-core?
Use puppeteer when you want the package to download a compatible Chrome for Testing browser. Use puppeteer-core when you manage the browser separately or connect to a remote browser; it does not download Chrome.
Does hosting the VPS in India require different Puppeteer code?
The reviewed Puppeteer documentation does not describe a separate India-specific installation path. A site may return different content based on where requests originate, so validate the screenshot from the VPS that will run the script.
Can Puppeteer capture a PDF instead of an image?
Yes. Puppeteer provides page PDF generation for print output; use it when a paginated document is a better fit than a tall screenshot. ScreenshotNeo also supports PDF through its API and MCP tool capture_pdf.


