Generate Website Screenshots in Node.js with Puppeteer on a Low-Cost Indian VPS
Install Puppeteer and Chrome on an Indian VPS, capture pages safely in Node.js, and handle Linux dependencies, timeouts, concurrency, and cost.
To generate website screenshots with Puppeteer on an Indian VPS, use a supported Debian or Ubuntu image, install a Node.js version supported by your Puppeteer release, install the regular puppeteer package so it downloads its compatible Chrome for Testing browser, install any missing Linux libraries, and run a Node script that navigates to the target page and saves a screenshot. Keep Chrome’s sandbox enabled where possible, set explicit navigation and operation timeouts, and limit concurrent browser work to what your own workload can sustain.
This guide builds a working PNG capture and explains viewport versus full-page output, selectors, readiness, browser installation, Linux dependencies, security, reliability, and VPS cost. Puppeteer’s current system requirements page reports version 25.12.0 and Node 22.12 or newer; check the [current requirements](https://pptr.dev/guides/system-requirements) against the exact version and VPS image you choose. Debian and Ubuntu Linux on x64 and arm64 are listed as supported Chrome for Testing platforms.
1. Choose a VPS and operating system
Choose a VPS with root access or equivalent administrative access, a supported Debian or Ubuntu release, enough memory for the operating system and Chromium processes, and storage for the browser download, Node dependencies, and output files. A server in India may reduce network distance to Indian target sites, but the right region depends on where the sites and users are. A server’s advertised resources do not establish a particular screenshot rate.
One current provider example in the research is Hostinger India’s KVM 1 listing: ₹599/month advertised with 1 vCPU, 4 GB RAM, 50 GB NVMe storage, and 4 TB bandwidth; its page states renewal at ₹999/month for a two-year term. Treat this as a provider offer, not a market-wide cheapest price or a performance guarantee. Check the checkout total, term, renewal, taxes, backups, and available data-center locations before buying. For any provider, compare recurring and renewal price, vCPU and RAM, disk and bandwidth, location, recovery options, and restrictions.
2. Install Node.js, project files, and Puppeteer
Connect to your Debian or Ubuntu server over SSH. Install a Node.js release that meets the requirements of your selected Puppeteer version. The following uses NodeSource’s setup script as an example for Node 22; follow the current installation instructions for your operating system and the Node release you select.
# Check the operating system and architecture
cat /etc/os-release
uname -m
# Install basic tools
sudo apt-get update
sudo apt-get install -y ca-certificates curl
# Example Node.js 22 installation on Debian/Ubuntu
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version
npm --version
# Create a small project
mkdir -p ~/puppeteer-shot
cd ~/puppeteer-shot
npm init -y
npm install puppeteer
The regular puppeteer package downloads a compatible Chrome for Testing browser during installation. This browser download is large, so allow for download time, disk use, and outbound network access. Package managers or deployment policies can block install scripts. If that happens, Puppeteer may install without its browser; install it explicitly with npx puppeteer browsers install. By contrast, puppeteer-core does not download Chrome. Use it when you deliberately manage the browser yourself or connect to a remote browser, and provide an explicit executable path or connection configuration.
# If the Puppeteer install script was blocked, install its browser explicitly
npx puppeteer browsers install
# Check the browser installation
npx puppeteer browsers list
Pin your dependencies with the generated lockfile and deploy using npm ci so production installs resolve the versions recorded by your project. When upgrading Puppeteer, deploy its compatible browser and dependencies together rather than assuming an independently installed system Chrome will match.
3. Install Linux libraries and check Chrome startup
Chrome requires system libraries and fonts. The required set can vary with the Chrome release and Linux image. Puppeteer’s [Linux troubleshooting guide](https://pptr.dev/troubleshooting) lists common Debian packages and recommends checking Chrome for missing shared libraries. On an Ubuntu or Debian image, this package set is a starting point:
sudo apt-get update
sudo apt-get install -y \
ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 \
libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 \
libfontconfig1 libgbm1 libgcc-s1 libglib2.0-0 libgtk-3-0 \
libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 \
libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
libxdamage1 libxext6 libxfixes3 libxrandr2 libxrender1 \
libxss1 libxtst6 lsb-release wget xdg-utils
Package names may differ by release. If a package is unavailable, consult the current Chrome dependency list for the distribution rather than substituting a random library. Find Puppeteer’s downloaded Chrome executable with npx puppeteer browsers list, then run ldd /path/to/chrome | grep 'not found' to identify missing shared libraries. Install the corresponding distribution packages and retry.
4. Capture a page in Node.js
Create shot.mjs in the project directory. This runnable example accepts a URL and optional output path, sets the viewport before navigation, waits for the page’s load event, saves a PNG, and closes both page and browser even if capture fails.
import puppeteer from 'puppeteer';
const target = process.argv[2] ?? 'https://example.com';
const output = process.argv[3] ?? 'page.png';
let browser;
try {
browser = await puppeteer.launch({
headless: true,
// Keep Chrome's sandbox enabled. Run as a non-root account.
});
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(target, {
waitUntil: 'load',
timeout: 45_000,
});
await page.screenshot({ path: output, type: 'png' });
console.log(`Saved ${output}`);
} finally {
if (browser) await browser.close();
}
Run it with:
node shot.mjs https://example.com example.png
file example.png
In production, validate or allowlist target URLs if users provide them, choose an output directory your service can write to, and return an error status to the caller when navigation or capture fails. Do not expose a general-purpose screenshot endpoint that can fetch arbitrary internal or private network addresses.
5. Choose screenshot size, format, and page readiness
Viewport and full-page captures
The default screenshot captures the current viewport. Set page.setViewport() before navigation when dimensions affect responsive layout. Set deviceScaleFactor to 2 for higher-density output, at the cost of more pixels, memory, and a larger image. A full-page screenshot captures the full document height and can consume substantially more memory on very long pages.
// Full-page PNG
await page.screenshot({ path: 'full.png', fullPage: true, type: 'png' });
// Viewport JPEG with quality (JPEG quality applies to JPEG output)
await page.screenshot({ path: 'viewport.jpg', type: 'jpeg', quality: 85 });
// Capture one element after locating it
const card = await page.waitForSelector('.product-card', { timeout: 10_000 });
if (!card) throw new Error('Product card was not found');
await card.screenshot({ path: 'card.png' });
Wait for the right condition
waitUntil: 'load' waits for the load event. For applications that continue fetching data, wait for a specific selector that represents completed content. domcontentloaded can be quicker when only initial markup is needed. networkidle0 and networkidle2 can be useful for quiet pages, but analytics, long polling, or streaming connections can prevent network idle from occurring. Match the condition to the page rather than treating it as a guarantee that all visual content is ready.
// Wait for the meaningful content, with a bounded timeout
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 45_000 });
await page.waitForSelector('main article', { timeout: 20_000 });
// Optional: wait a short time for animation or client-side paint
await new Promise(resolve => setTimeout(resolve, 500));
await page.screenshot({ path: 'article.png', fullPage: true });
Lazy-loaded images may appear only after scrolling. If the target requires them, scroll through the document in controlled increments, allow images to load, then capture. This adds time and can make very tall pages expensive; test on representative target pages.
Useful screenshot and browser options
| Need | Option or API | Consideration |
|---|---|---|
| Viewport dimensions | page.setViewport({width, height, deviceScaleFactor}) |
Set before navigation when responsive breakpoints matter. |
| Entire document | page.screenshot({fullPage: true}) |
Very long documents can use substantial memory. |
| Element image | elementHandle.screenshot() |
Wait for the selector and ensure it is visible. |
| Image format | type: 'png' or 'jpeg' |
PNG preserves crisp edges; JPEG supports a quality setting. |
| Transparent background | omitBackground: true |
Useful for PNG when the page background should be transparent. |
| Device emulation | page.setViewport() and page.emulate() |
Emulate viewport and device characteristics when a mobile layout is needed. |
| Navigation readiness | page.goto(url, {waitUntil, timeout}) |
Use a page-specific selector for client-rendered content. |
| Browser behavior | puppeteer.launch({headless, args}) |
Avoid unsafe flags and only add arguments for a specific operational need. |
6. Run captures reliably on a small server
A browser launch is a heavyweight operation compared with a single page operation. For a one-off script, launch once, capture, and close as shown above. For a service, consider reusing a browser process while creating and closing a fresh page per job; monitor it and recycle it periodically if your workload or observed stability requires that. Always close pages after each job and close the browser on shutdown.
- Set a navigation timeout and an overall job deadline. A page can hang on navigation or never satisfy a readiness condition.
- Bound concurrent pages. Chromium uses CPU and memory, and full-page images can raise memory use. No sourced benchmark establishes a safe concurrency number for a given VPS; measure your actual target pages and workload.
- Queue excess jobs instead of starting unbounded captures. Track process memory, CPU, capture duration, and failure rate under realistic load.
- Use a non-root service account with only the file and network permissions it needs. Keep browser sandboxing enabled.
- Write output to a controlled directory. Apply retention or deletion rules so screenshots do not fill the VPS disk.
- Retry only transient failures, with a small bounded retry count and backoff. Do not retry deterministic errors such as an invalid URL indefinitely.
- Use a process manager or service supervisor to restart a crashed worker, and send logs to a location with rotation and disk limits.
Do not expose Chrome’s remote debugging port to the public internet. Treat pages and screenshots as potentially sensitive: they may contain private account data, tokens in URLs, or personal information. Avoid logging full URLs when they contain credentials, and secure generated files and any download endpoint.
7. Security: keep Chrome’s sandbox enabled
Chrome’s sandbox is a security boundary for untrusted web content. Puppeteer’s troubleshooting guide strongly discourages launching with --no-sandbox; it should not be a routine VPS fix. Run the service as a non-root user and fix the host’s sandbox configuration or permissions when Chrome reports No usable sandbox!. Ubuntu AppArmor policies can also affect Chrome for Testing user namespaces on some releases; use the documented distribution-specific guidance.
Only if the pages are fully trusted and you have deliberately accepted the reduced isolation should you consider disabling the sandbox. A publicly callable screenshot service processes attacker-controlled URLs, so disabling this protection increases the consequences of a browser compromise. Prefer sandboxed browser execution and isolate the worker from secrets and internal services.
8. Troubleshooting common errors
| Error or symptom | Likely cause | Fix |
|---|---|---|
Could not find Chrome |
Install scripts were blocked or the browser cache was not included in deployment. | Run npx puppeteer browsers install in the deployed project and ensure the browser cache is available to the service user. |
error while loading shared libraries or Chrome exits immediately |
A required Linux library is missing. | Run ldd /path/to/chrome | grep 'not found', install the matching OS packages, and consult the current [Puppeteer troubleshooting guide](https://pptr.dev/troubleshooting). |
No usable sandbox! |
Host sandbox support, user namespaces, permissions, or an AppArmor policy prevents Chrome sandbox startup. | Run as a non-root user and fix the host sandbox setup; check the troubleshooting guide for distro-specific notes. Avoid making --no-sandbox the default. |
| Navigation timeout | Slow target, stalled request, or a wait condition that never occurs. | Choose a relevant readiness condition, allow a suitable bounded timeout, and report timeout failures clearly. Do not wait for network idle on pages with ongoing connections. |
| Screenshot is blank or content is missing | Capture ran before client-rendered content or images appeared, or a page challenge blocked content. | Wait for a meaningful selector, check the response and final page state, and handle blocked or empty pages as failures rather than successful captures. |
| Fonts or layout differ from a desktop | Fonts are unavailable, viewport differs, or device scale and responsive layout changed. | Install appropriate fonts, set viewport and scale explicitly, and verify the target page at those dimensions. |
| Out of memory or worker killed | Too many concurrent pages, unusually tall pages, or large images. | Reduce concurrency, avoid full-page capture when unnecessary, cap page dimensions where possible, and inspect memory under representative workload. |
| Permission denied writing screenshot | The service account cannot write to the chosen path. | Create an output directory owned by the service user and pass an absolute writable path. |
| Install works locally but not in production | Different Node version, architecture, missing browser cache, or missing system packages. | Compare production Node version and architecture with Puppeteer’s requirements, run browser installation in deployment, and install system dependencies on the target image. |
9. Performance, reliability, and cost
Cost is more than the VPS sticker price. Include renewal rates and billing commitment, backups, storage growth, network transfer, and the engineering time required to patch Node, Puppeteer, Chrome, and system libraries. A small server may be appropriate for low-volume, queued jobs, but neither the sourced plan specifications nor Puppeteer’s documentation establish a capture-per-second figure. Measure using your own mix of pages, image lengths, readiness waits, and concurrency before setting service limits.
For reliability, pin compatible dependencies, keep the lockfile, monitor disk and memory, enforce deadlines, and make failures visible to the caller. A target page can change markup, block automation, serve region-specific content, or fail independently of your VPS. Store enough diagnostic metadata to distinguish navigation errors, selector timeouts, browser startup failures, and successful captures without retaining sensitive page contents unnecessarily.
10. Or skip the browser setup
If you do not want to install and maintain Node, Chrome, and Linux browser libraries on the VPS, [ScreenshotNeo](https://screenshotneo.com) is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for request options.
// Node.js: save the returned screenshot
import { writeFile } from 'node:fs/promises';
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.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 -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,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Cookie and consent banners are accepted before capture, and more than 60 known consent platforms, newsletter popups, and chat widgets are removed; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with verdict and billing information in response headers. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Every feature is on every plan.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
11. FAQ
Can I run Puppeteer without a desktop environment?
Yes. Puppeteer runs Chrome in headless mode by default, so a graphical desktop is not required for this screenshot workflow.
Should I use puppeteer or puppeteer-core?
Use puppeteer when you want its install process to download a compatible browser. Use puppeteer-core when you manage Chrome yourself or connect to a remote browser.
Does a VPS in India guarantee faster screenshots of Indian sites?
No. Location can affect network path, but target response time, rendering work, VPS contention, and page assets also matter. Measure from the region you plan to use.
Is 4 GB RAM enough?
The cited provider plan includes 4 GB, but that specification alone cannot establish suitability. Test your page mix and concurrency while watching memory use; reduce concurrent work if the worker approaches its memory limit.
Can I capture a page that requires login?
Puppeteer can interact with pages using browser automation, but protect credentials and session data, use a dedicated account where appropriate, and ensure you are authorized to capture the content. Do not put secrets in logs or publicly accessible screenshots.


