Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use PHP as the controller and Node.js as the browser worker. Puppeteer is a JavaScript library, so PHP should invoke a fixed Node.js script, then read a small machine-readable result. Keep the executable and script path under your control, escape every value that crosses the shell boundary, and use exec() or proc_open() when you need a trustworthy exit code or separate error stream.

The architecture that works

The reliable pattern is a two-process design:

  1. PHP receives a request and validates its inputs.
  2. PHP starts a Node.js script with shell_exec() (or a process API).
  3. The Node script launches Puppeteer, performs the browser work, writes one JSON result to standard output, and sends diagnostics to standard error.
  4. PHP parses the JSON and decides whether the operation succeeded.

Puppeteer itself is not a PHP package. Install it in a Node project with npm i puppeteer. The standard puppeteer package downloads a compatible Chrome during installation; puppeteer-core does not download or manage a browser, so your deployment must supply one.

Install Node.js and Puppeteer

Create an isolated project

On the server, create a directory owned by the account that will run the automation:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
mkdir -p /var/www/myapp/puppeteer-worker
cd /var/www/myapp/puppeteer-worker
npm init -y
npm install puppeteer

Some package managers disable npm install scripts. If installation completed without a browser, install the browser explicitly with:

npx puppeteer browsers install

Use puppeteer-core only when you intentionally manage a system or container browser yourself. Record the absolute Node executable path (for example, /usr/bin/node) and the worker directory; a web server often has a different PATH and home directory from your interactive shell.

Build the Node.js worker

Save this as automation.js. It accepts a URL as one argument, returns exactly one JSON object on standard output, and writes failures to standard error. The URL is still validated in PHP; the Node check is a second boundary.

const puppeteer = require('puppeteer');

function fail(message, code = 1) {
  process.stderr.write(`${message}n`);
  process.exit(code);
}

(async () => {
  const target = process.argv[2];
  if (!target) fail('Missing URL', 2);

  let parsed;
  try {
    parsed = new URL(target);
  } catch {
    fail('Invalid URL', 2);
  }
  if (!['http:', 'https:'].includes(parsed.protocol)) {
    fail('Only HTTP and HTTPS URLs are allowed', 2);
  }

  let browser;
  try {
    browser = await puppeteer.launch({
      headless: true
    });
    const page = await browser.newPage();
    await page.goto(parsed.href, {
      waitUntil: 'networkidle2',
      timeout: 60000
    });

    const result = {
      ok: true,
      title: await page.title(),
      url: page.url(),
      text: await page.locator('body').innerText()
    };
    process.stdout.write(JSON.stringify(result));
  } catch (error) {
    fail(error instanceof Error ? error.message : String(error));
  } finally {
    if (browser) await browser.close();
  }
})();

browser.close() belongs in finally, so navigation errors do not leave Chrome processes running. Keep the result compact: returning an entire page, screenshot, or arbitrary HTML can exceed buffers and creates a data-handling risk. If you need a screenshot, write it to a controlled temporary directory or return a deliberately encoded value.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Call the worker from PHP with shell_exec()

This is the basic composition of a fixed executable and fixed script path:

<?php
declare(strict_types=1);

$url = $_POST['url'] ?? '';
if (!filter_var($url, FILTER_VALIDATE_URL)) {
    http_response_code(400);
    exit('Invalid URL');
}
$scheme = parse_url($url, PHP_URL_SCHEME);
if (!in_array($scheme, ['http', 'https'], true)) {
    http_response_code(400);
    exit('Only HTTP and HTTPS URLs are allowed');
}

$node = '/usr/bin/node';
$script = __DIR__ . '/puppeteer-worker/automation.js';
$command = escapeshellarg($node) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($url);

$output = shell_exec($command);
if ($output === null || $output === false) {
    http_response_code(500);
    exit('The worker produced no readable output');
}

$data = json_decode($output, true);
if (!is_array($data) || ($data['ok'] ?? false) !== true) {
    http_response_code(502);
    exit('The worker returned an invalid result');
}
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_SLASHES);

Replace /usr/bin/node with the actual absolute path on your host. Confirm that the PHP service account can execute Node, read the script and its node_modules, create the browser profile and access the required temporary directories.

Why every argument is escaped

Never concatenate a request value directly into shell syntax. A URL containing shell metacharacters, quotes or substitutions can become a command-injection path. escapeshellarg() makes each value one argument; keeping the executable and script fixed prevents a caller from selecting a different program. Do not accept a complete command from a user, and do not use untrusted text to build redirection, pipes or shell operators.

Understand shell_exec() output and its limits

shell_exec() captures command output as a string. It can return false if the pipe cannot be established, while null can mean an error or simply that the command produced no output. It does not provide the process exit status, so output text alone cannot prove that the browser succeeded.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a diagnostic-only combined stream, a fixed redirection can be appended:

$command = escapeshellarg($node) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($url) . ' 2>&1';

That merges diagnostics into JSON output, so use it only if you also change the parser or are collecting a human-readable log. Do not derive the redirection from request data.

Use exec() when you need an exit code

exec() returns output lines and fills an exit-code variable. Keep standard error separate or redirect it to a fixed log:

<?php
$lines = [];
$status = 0;
$command = escapeshellarg($node) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($url) . ' 2>/var/log/myapp/puppeteer-worker.err';
exec($command, $lines, $status);

if ($status !== 0) {
    http_response_code(502);
    exit('Browser worker failed');
}
$json = implode("n", $lines);
$result = json_decode($json, true);
if (!is_array($result)) {
    http_response_code(502);
    exit('Worker output was not valid JSON');
}

Check permissions on the log directory and rotate logs so a repeated failure cannot fill the disk.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When proc_open() is the better boundary

Choose proc_open() when you need separate standard output and standard error, streamed input, timeouts, process lifecycle control or a design that can avoid an intermediate shell. PHP documents proc_open() as the process-I/O option. On Windows, execution functions normally go through cmd.exe; proc_open() with bypass_shell is the documented exception. Test that option on the exact PHP and Windows version you deploy.

A process API also lets you enforce a wall-clock limit, close pipes deterministically and terminate a stuck worker. For a simple one-shot call, shell_exec() is adequate only when you accept its ambiguous status reporting and have an external timeout or queue policy.

Security controls for production

  • Constrain destinations. Allow only http and https, and consider an allowlist. Block loopback, link-local and private network targets if users can submit URLs; otherwise the browser may become an SSRF pivot.
  • Limit resources. Apply navigation and overall process timeouts, cap response size where practical, and limit concurrent workers. A new browser per request is expensive; a queue or carefully managed browser pool can protect PHP workers.
  • Separate privileges. Run the web process and browser with the minimum filesystem and network permissions. Never run the worker as root.
  • Protect data. Cookies, headers, downloaded files and page text can contain secrets. Keep profiles temporary, avoid logging sensitive output and treat page content as untrusted.
  • Keep installation deterministic. Pin package versions in the lockfile, install dependencies during deployment and verify that the expected browser exists before accepting traffic.
  • Remember Puppeteer’s responsibility boundary. The project’s security policy places responsibility on calling code to use browser installation, automation and inspection safely and as intended.

Reliability and performance decisions

Browser lifetime

Launching Chromium for every PHP request is simple but adds startup cost. For low volume, it is often the safest isolation model. For sustained volume, move work to a queue and reuse a browser process while creating a fresh incognito context or page per job; monitor memory and recycle the browser after a bounded number of jobs.

Navigation waits

networkidle2 waits for a relatively quiet network, but analytics, advertisements and long polling can prevent it from becoming quiet. Use a selector wait for the element that proves the page is ready, or a bounded delay for known client-side rendering. Always retain a hard timeout.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

PHP request limits

Web-server, PHP-FPM and reverse-proxy timeouts can be shorter than the browser timeout. If a capture can take longer than a normal HTTP request, enqueue it and let PHP return a job identifier instead of holding the request open.

Troubleshooting checklist

PHP returns null or an empty response

The worker may have produced no standard output, failed before printing JSON, or PHP may be unable to start the process. Confirm that the script reaches its success or error path, verify the absolute Node path, and run the command as the same service account. Remember that no output also maps to null.

“Command not found” or works only in SSH

Web workers often have a minimal PATH. Replace node with its absolute path, use absolute project paths, and do not rely on shell startup files.

Chrome executable is missing

The npm install script may have been blocked, or you installed puppeteer-core without supplying a browser. Run npx puppeteer browsers install for the standard package, or configure the managed executable explicitly when using core.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Permission or sandbox errors

Check ownership of the project, cache and temporary directories. Ensure the service account can execute the browser and that required Linux browser dependencies are installed. Do not “fix” this by running the browser as root.

The process hangs

Set a navigation timeout and an outer process timeout, inspect pages that keep connections open, and ensure browser.close() executes in finally. A queue with cancellation is safer than unlimited synchronous web requests.

JSON parsing fails

Any debug console.log() on standard output corrupts the protocol. Emit only one JSON object on standard output; send logs to console.error() or standard error. Also cap or sanitize page-derived text before returning it.

Windows behaves differently

PHP normally invokes commands through cmd.exe. Quote paths carefully, use Windows absolute paths and evaluate proc_open() with bypass_shell when avoiding the shell is important. Test under the actual IIS or service account, not an administrator terminal.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server when your PHP application needs an image or PDF rather than custom browser logic. A single GET request returns PNG, JPEG, WebP or PDF. It accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; those cleanup steps can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status.

Use the documented endpoint and options at https://screenshotneo.com/docs/:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

From PHP, the same request can be made without launching Node:

<?php
$query = http_build_query([
    'access_key' => 'YOUR_API_KEY',
    'url' => 'https://stripe.com'
]);
$body = file_get_contents('https://api.screenshotneo.com/v1/shot?' . $query);
if ($body === false) {
    throw new RuntimeException('Screenshot request failed');
}
file_put_contents(__DIR__ . '/shot.webp', $body);

For scripts or other services, the equivalent examples are:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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}`);

ScreenshotNeo also offers full-page lazy-image loading, CSS-element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks, selector waits, delay or network-idle waits, ad/tracker/request blocking, custom headers, cookies, user agent, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to try it.

FAQ

Can PHP import Puppeteer directly?

No. Puppeteer is a Node.js library; PHP must communicate with a Node process or use a separate browser-automation service.

Should I return HTML from the Node script?

Usually no. Return a small JSON envelope and store large artifacts in controlled storage. Treat all page content as untrusted.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Is shell_exec() asynchronous?

No. It waits for the command to finish. Use a queue or job worker when browser work can exceed a normal PHP request.

Frequently Asked Questions

What is the minimum deployment check before enabling this endpoint?

Run the fixed command as the PHP service account and verify Node, the Puppeteer browser, filesystem permissions, network access and a bounded timeout before accepting user-supplied URLs.

Why does a successful screenshot still need application-level validation?

An HTTP response or image file does not prove that the intended page loaded. Validate status headers or page-specific markers and record failures separately from transport errors.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.