Convert a Webpage to PDF with Puppeteer Without a Local Chrome Install
Use Puppeteer’s downloaded Chrome for Testing to create PDFs without installing Chrome as a system app, or connect to a managed remote browser.
Yes. You can create a PDF with Puppeteer without installing Chrome as a system application: install the regular puppeteer package, which normally downloads a compatible Chrome for Testing browser, then call page.pdf(). A browser binary is still required; it is commonly stored in Puppeteer’s cache. If you mean that no browser binary can exist on the machine running your code, use puppeteer-core to connect to a remote browser instead. See the Puppeteer installation guide, PDF API, and configuration guide.
1. Create a PDF with Puppeteer’s downloaded browser
This is the simplest option for local development, CI, or a container that can download and run Chrome for Testing.
mkdir webpage-to-pdf
cd webpage-to-pdf
npm init -y
npm install puppeteer
Save the following as make-pdf.mjs:
import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://example.com';
const outputPath = process.argv[3] ?? 'page.pdf';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 60_000,
});
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
margin: {
top: '12mm',
right: '12mm',
bottom: '12mm',
left: '12mm',
},
});
console.log(`Saved ${outputPath}`);
} finally {
await browser.close();
}
Run it with:
node make-pdf.mjs https://example.com example.pdf
The regular package manages a compatible Chrome for Testing download during installation. It does not require a separately installed system Chrome. The browser download is substantial: Puppeteer’s installation guide lists approximate sizes of 170 MB for macOS, 282 MB for Linux, and 280 MB for Windows. These are versioned figures; check the guide for the version and platform you deploy. Chrome for Testing is intended for automation and testing, rather than regular browsing. See Chrome for Testing.
2. Choose how the browser is supplied
| Setup | Who supplies the browser? | Browser files on app host? | Use when |
|---|---|---|---|
puppeteer |
Puppeteer’s install flow downloads a compatible browser | Yes, typically in Puppeteer’s cache | You want the simplest local, CI, or container setup. |
puppeteer-core with executable path |
Your environment or deployment image | Yes | You already manage a Chrome or Chromium binary and can provide its path. |
puppeteer-core with remote connection |
A separately managed browser environment | Not necessarily | The application host should not store or run the browser. |
puppeteer-core does not download Chrome. With it, you must supply a compatible local executable or connect to a remote browser that supports the DevTools protocol. Remote endpoint setup, authentication, network policy, and cost depend on the environment or provider; there is no universal endpoint configuration.
Use an externally managed local browser
Install Chrome or Chromium using your environment’s approved process, then pass its executable path. The path below is an example; replace it with the actual path in your runtime.
npm install puppeteer-core
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.launch({
executablePath: '/absolute/path/to/chrome-or-chromium',
headless: true,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.pdf({ path: 'page.pdf', format: 'A4', printBackground: true });
} finally {
await browser.close();
}
The Puppeteer configuration files and environment variables documented for the full package do not apply to puppeteer-core. Configure its launch options directly.
Connect to a remote browser
Use puppeteer-core and the remote browser’s WebSocket endpoint. The endpoint and credentials are specific to the browser environment; keep credentials in environment variables or a secret manager rather than hard-coding them.
import puppeteer from 'puppeteer-core';
const browserWSEndpoint = process.env.BROWSER_WS_ENDPOINT;
if (!browserWSEndpoint) throw new Error('Set BROWSER_WS_ENDPOINT');
const browser = await puppeteer.connect({ browserWSEndpoint });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.pdf({ path: 'page.pdf', format: 'A4', printBackground: true });
} finally {
await browser.disconnect();
}
For a remote connection, disconnect() closes the client connection but does not necessarily stop the remote browser. Follow the remote environment’s lifecycle instructions if it provides an explicit close or session-release operation.
3. Set up installation and deployment
- Check the runtime. Puppeteer’s current system requirements list Node 22.12 or later. Confirm the requirements for the exact Puppeteer release, operating system, and CPU architecture you deploy. Supported Chrome for Testing platforms and required system libraries can change; see Puppeteer system requirements.
- Allow the browser install step. Package managers or project policies may block dependency install scripts. If the browser is missing after installing
puppeteer, runnpx puppeteer browsers installduring setup. Some package managers also require explicit approval for Puppeteer’s postinstall script. - Install during image or build creation. Download the browser while building the deployment image so a runtime request does not have to fetch it. Keep the browser cache available to the runtime user. Puppeteer’s guide documents
$HOME/.cache/puppeteeras the default cache beginning with v19.0.0; cache configuration is version-sensitive. - Check Linux dependencies. A browser download alone may not provide all operating-system libraries. On Ubuntu or Debian, the Puppeteer browser CLI can attempt dependency installation with
npx puppeteer browsers install chrome --install-deps; this requires root. Use it only where your image build permissions and OS support it. - Pin deliberately. You can install a Chrome for Testing channel or build with
npx @puppeteer/browsers install chrome@stableor a specified version. Align the browser and Puppeteer versions with their documented compatibility, and update them together as part of deployment maintenance. See the browser CLI documentation.
For reproducible deployments, build and retain the browser with the application image or provision it through a controlled browser-management process. Recheck OS libraries, extraction utilities, architecture, and runtime-user permissions when changing the base image.
4. Control PDF layout and page readiness
page.pdf() renders using print CSS by default and returns a Promise<Uint8Array>. Pass path to save the result directly, or omit it and handle the returned bytes. See the Page.pdf() API.
const pdfBytes = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: false,
landscape: false,
margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
});
formatselects a standard paper format such asA4orLetter. You can instead specifywidthandheight.landscapeswitches page orientation.marginaccepts top, right, bottom, and left dimensions.printBackgroundincludes background graphics, which are otherwise not printed by default.preferCSSPageSizelets CSS@pagesize take priority over the configured paper format.displayHeaderFooterenables print header and footer templates; the API also supports header and footer template options.pageRangescan limit output to selected pages when the document is long.
Use only options supported by the Puppeteer version you have installed; consult its API reference for current defaults and full option details. For screen media styling instead of print styling, call await page.emulateMediaType('screen') before generating the PDF. Printing modifies colors by default; use CSS -webkit-print-color-adjust where exact print colors are required.
networkidle2 is a useful starting point, not proof that every page is ready. Pages may load content after API calls, reveal images on scroll, animate indefinitely, or keep network connections open. Use a page-specific readiness condition when the site exposes one. For example, wait for a known selector after navigation:
await page.goto('https://example.com/report', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-report-ready="true"]', { timeout: 30_000 });
await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });
For lazy-loaded content, determine what the target page needs: it may require scrolling through the page, waiting for a specific image or application state, or using the site’s print-specific layout. Avoid a blind fixed delay as the only readiness check when correctness matters.
5. Troubleshoot common failures
| Symptom | Likely cause | What to do |
|---|---|---|
Could not find Chrome (ver. ...) |
The browser download did not run, was blocked, or its cache is unavailable to the runtime. | Run npx puppeteer browsers install during build/setup. Confirm the cache location and that the runtime user can read it. If using puppeteer-core, configure executablePath or connect to a remote browser. |
| Browser executable exists, but launch fails on Linux | Required shared libraries may be missing, or the deployment OS/architecture may not match the downloaded browser. | Check Puppeteer’s system requirements for that release, install the required system packages in the image, and verify architecture. The CLI dependency installation option requires root. |
| Install succeeds locally but fails in CI | Install scripts may be disabled, network access may be restricted, or the cache may not persist between build and runtime. | Explicitly install the browser in the build, allow the relevant install script, and retain the browser cache or image layer. |
| Navigation times out | The site is slow, keeps connections open, or never reaches the selected network-idle condition. | Choose a suitable navigation event such as domcontentloaded, set a deliberate timeout, then wait for a page-specific selector or state. Do not assume a larger timeout fixes an unsuitable readiness condition. |
| PDF is blank or missing images | Content may render after navigation, be lazy-loaded, or require print-specific CSS and resources. | Wait for the content’s actual ready state, load required images, and inspect the page’s print stylesheet. Try screen media only if screen layout is the desired output. |
| Colors or backgrounds differ from the page | PDF output uses print media and print color adjustments; backgrounds are not included by default. | Set printBackground: true. Use page.emulateMediaType('screen') for screen CSS, or apply -webkit-print-color-adjust in page CSS when exact colors are needed. |
| Remote connection fails | The endpoint may be unreachable, credentials invalid, or the WebSocket endpoint incompatible or expired. | Check endpoint reachability from the app host, authentication, firewall rules, and remote session lifecycle. Use the connection details supplied by that browser environment. |
6. Performance, reliability, and cost
- Build and cache the browser. Puppeteer’s browser download consumes disk and build time; the documented approximate archive sizes are large, especially in Linux containers. Avoid downloading a browser on every PDF request.
- Reuse browser processes carefully. A long-running service can keep a browser process available and create pages for jobs, reducing repeated startup work. Bound concurrency and close each page; recycle the browser when it becomes unhealthy. This requires monitoring and lifecycle handling in your application.
- Set limits. Bound navigation and readiness waits, PDF generation time, concurrent pages, and output size according to your workload. A page with unbounded network activity or very large content can consume substantial time and memory.
- Make jobs retryable. Distinguish transient navigation or remote-browser failures from invalid URLs and deterministic page errors. Apply a bounded retry policy and avoid writing partial output as if it were complete.
- Account for operational cost. Local Puppeteer has no per-request browser API price established by these sources, but it requires browser storage, compatible system dependencies, compute, and maintenance. A remote browser shifts some of that operational work to the remote environment; its price and limits depend on the provider. Check current provider terms before choosing one.
7. Or skip the browser setup
ScreenshotNeo can return a PDF from one API request, with the browser managed by the service. Use its API documentation for PDF options and parameters.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-d format=pdf \
-o page.pdf
Cookie and consent banners are accepted and removed before the capture, along with known newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. ScreenshotNeo also has an MCP server for AI agents, with tools including take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up for ScreenshotNeo’s free 1,000 shots per month, with no card required.
8. FAQ
Does Puppeteer render PDFs without any browser?
No. The browser can be downloaded and managed by Puppeteer, supplied by your environment, or run remotely, but PDF generation still uses a browser.
Can I use Puppeteer in a Docker container?
Yes, provided the image includes a compatible browser and required system dependencies, and the runtime user can access them. Check the requirements for the exact Puppeteer release and base image.
Should I use puppeteer or puppeteer-core?
Use puppeteer for the default managed browser download. Use puppeteer-core when your application manages the executable or connects to a remote browser.
Why does the PDF look different from a screenshot?
PDF output defaults to print media CSS. The page’s print styles, paper dimensions, margins, and color adjustment can all change the result.


