ScreenshotNeo

BlogHow-to

Create a Website Screenshot in PHP with Headless Chrome on Debian

Capture a website screenshot from PHP on Debian with headless Chrome. Configure the browser, choose viewport dimensions, handle timing and errors, and save the image safely.

By the ScreenshotNeo team4 October 20269 min read

To capture a website screenshot from PHP on Debian, install Chrome or Chromium, then start its headless command-line mode with PHP’s proc_open(). On PHP 7.4 or later, pass the command as an argument array so it runs without shell parsing. Chrome writes screenshot.png to its current working directory; use --window-size=WIDTH,HEIGHT to set the viewport.

This guide captures a viewport screenshot. A tall viewport is not a reliable full-page capture: full-page screenshots need additional handling, such as scrolling and stitching or browser automation. The examples below make the output directory, browser path, timeout, and failure checks explicit.

1. Install or locate Chrome or Chromium

Debian Reference lists Chromium, but package availability and commands vary by Debian release and architecture. Confirm the browser package or official browser download instructions for the Debian release you actually deploy; do not copy an assumed package command or executable path across systems.

Find the installed executable and configure that path for your application. Chrome and Chromium installations can use different binary names and paths. The example accepts the executable path through an environment variable:

CHROME_BIN=/usr/bin/chromium php screenshot.php https://example.com/

Replace /usr/bin/chromium with the path on your host. Ensure the PHP process user can execute the browser and write to the output directory. Headless mode renders without displaying a browser UI. Current Chrome Headless, introduced in Chrome 112, shares the browser implementation with headful Chrome; the older implementation is available separately as chrome-headless-shell starting with Chrome 132.0.6793.0. Use the full browser when fidelity and feature coverage matter; the shell can suit simpler screenshot jobs with fewer dependencies. See the [Chrome Headless documentation](https://developer.chrome.com/docs/automation-and-testing/headless?authuser=160).

2. Capture a viewport screenshot with PHP 7.4+

Save this as screenshot.php. It takes the URL as its first command-line argument, writes the screenshot into a chosen directory, checks the process exit status, and reports Chrome’s output if capture fails.

<?php
declare(strict_types=1);

$url = $argv[1] ?? '';
if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
    fwrite(STDERR, "Usage: php screenshot.php https://example.com/\n");
    exit(2);
}

$chrome = getenv('CHROME_BIN') ?: '/usr/bin/chromium';
$outputDir = __DIR__ . '/screenshots';
$width = 1280;
$height = 900;

if (!is_file($chrome) || !is_executable($chrome)) {
    fwrite(STDERR, "Browser executable not found or not executable: {$chrome}\n");
    exit(2);
}
if (!is_dir($outputDir) && !mkdir($outputDir, 0770, true) && !is_dir($outputDir)) {
    fwrite(STDERR, "Could not create output directory: {$outputDir}\n");
    exit(2);
}
if (!is_writable($outputDir)) {
    fwrite(STDERR, "Output directory is not writable: {$outputDir}\n");
    exit(2);
}

$outputFile = $outputDir . '/screenshot.png';
$command = [
    $chrome,
    '--headless',
    '--screenshot=' . $outputFile,
    "--window-size={$width},{$height}",
    '--timeout=15000',
    $url,
];
$descriptors = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w'],
];
$pipes = [];
$process = proc_open($command, $descriptors, $pipes, $outputDir);
if (!is_resource($process)) {
    fwrite(STDERR, "Could not start Chrome\n");
    exit(1);
}

fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);

if ($exitCode !== 0 || !is_file($outputFile) || filesize($outputFile) === 0) {
    fwrite(STDERR, "Chrome screenshot failed (exit {$exitCode}).\n{$stderr}\n{$stdout}\n");
    exit(1);
}

fwrite(STDOUT, "Saved screenshot to {$outputFile}\n");

Run it with the browser path for your host:

CHROME_BIN=/path/to/chrome-or-chromium php screenshot.php https://example.com/

The Chrome CLI reference documents --screenshot and the current-directory output behavior, plus the --timeout capture deadline. This example uses an explicit screenshot path and sets the process working directory to the output directory so the location remains predictable. Confirm that your installed browser version accepts the options you use. See the [Chrome Headless command-line reference](https://developer.chrome.com/docs/automation-and-testing/headless-cli?authuser=0000&hl=en).

3. Choose the right viewport and wait policy

Viewport dimensions

--window-size=WIDTH,HEIGHT sets the viewport dimensions for the CLI screenshot. For example, --window-size=412,892 approximates a narrow mobile viewport, while --window-size=1280,900 is a desktop-sized viewport. The output image dimensions follow the capture viewport, subject to browser and device scaling behavior.

A viewport screenshot captures the visible page area. Setting an unusually tall window does not guarantee that lazy-loaded content below the fold will load or that the entire document will be captured. For full-page output, use browser automation or protocol commands that scroll/load the document and capture its full dimensions, or capture sections and stitch them.

Timeout and page readiness

--timeout=15000 gives Chrome a maximum of 15 seconds before capture proceeds even if the page is still loading. This is a deadline, not a readiness guarantee. A page can render content after JavaScript, fonts, images, or network activity settle, and different sites have different readiness signals.

Choose a timeout that fits the target site and job budget. For dynamic pages that need a specific element or application state, a browser automation library can wait for that condition before issuing a screenshot. Avoid waiting indefinitely for network idle on pages with long-lived connections. Keep the final deadline bounded so a slow page cannot occupy a worker forever.

4. PHP version and process safety

PHP documents array-form commands for proc_open() from PHP 7.4. It starts the executable directly without a shell, avoiding shell metacharacter interpretation and reducing quoting mistakes. Pass the URL and all options as separate array elements, and validate any values that can come from an untrusted caller. See [PHP’s proc_open() manual](https://www.php.net/proc-open).

On PHP versions earlier than 7.4, proc_open() requires a command string. Quote each individual argument with escapeshellarg(); do not concatenate untrusted input into the command. This is a compatibility path, not as robust as the argument-array form:

<?php
$chrome = '/path/to/chrome-or-chromium';
$url = 'https://example.com/';
$outputDir = '/absolute/path/to/output';
$parts = [
    escapeshellarg($chrome),
    escapeshellarg('--headless'),
    escapeshellarg('--screenshot'),
    escapeshellarg('--window-size=1280,900'),
    escapeshellarg('--timeout=15000'),
    escapeshellarg($url),
];
$command = implode(' ', $parts);
$process = proc_open($command, $descriptors, $pipes, $outputDir);
// Close pipes, read output, and check proc_close() as in the PHP 7.4+ example.

escapeshellarg() quotes one argument for a shell; it is not a general command sanitizer. See [PHP’s escapeshellarg() manual](https://www.php.net/manual/en/function.escapeshellarg.php). If possible, upgrade to PHP 7.4 or later and use array-form invocation.

Operational safeguards

  • Use a fixed, trusted browser executable path rather than accepting a binary path from a request.
  • Restrict which URLs your application is allowed to capture. If callers can supply arbitrary URLs, enforce an allowlist or network policy to prevent access to internal services and local files.
  • Run browser processes as an unprivileged account with only the filesystem access they need.
  • Use unique output filenames for concurrent jobs; a shared screenshot.png can be overwritten by another request.
  • Set an outer process or job deadline as well as Chrome’s capture timeout so hung processes do not consume workers indefinitely.
  • Clean up old output files and avoid placing sensitive captures in publicly served directories.

5. cURL, Python, and Node.js alternatives

The browser invocation can be run directly from a shell, or from another language when the application is not PHP. These examples produce a viewport screenshot in the current directory:

cURL (shell)

mkdir -p screenshots
cd screenshots
/path/to/chrome-or-chromium --headless --screenshot=shot.png --window-size=1280,900 --timeout=15000 https://example.com/

Python

import subprocess

chrome = "/path/to/chrome-or-chromium"
url = "https://example.com/"
subprocess.run(
    [chrome, "--headless", "--screenshot=shot.png", "--window-size=1280,900", "--timeout=15000", url],
    cwd="/absolute/path/to/output",
    check=True,
    timeout=30,
)

Node.js

import { spawn } from 'node:child_process';

const chrome = '/path/to/chrome-or-chromium';
const url = 'https://example.com/';
const child = spawn(chrome, [
  '--headless',
  '--screenshot=shot.png',
  '--window-size=1280,900',
  '--timeout=15000',
  url,
], { cwd: '/absolute/path/to/output', stdio: 'inherit' });
child.on('error', (error) => { console.error(error); process.exitCode = 1; });
child.on('close', (code) => { if (code !== 0) process.exitCode = code ?? 1; });

These examples rely on Chrome or Chromium being installed and executable in the target environment. The Chrome process, rather than cURL itself, performs the page render.

6. Troubleshooting

Symptom Likely cause Fix
Could not start Chrome or a missing executable error The configured path is wrong, the package is absent, or PHP’s user cannot execute it. Locate the installed binary, set CHROME_BIN or update the configured path, and check execute permissions as the PHP service user.
Permission denied when writing the image The output directory is not writable by the web or worker account. Create a dedicated output directory and grant that account the minimum required write permission.
The image is missing or empty Chrome exited with an error, the output path was not writable, or the page/capture failed. Check the exit code and captured stderr/stdout, verify the directory, and try the command manually under the same account.
The screenshot is blank or incomplete The timeout elapsed before useful content rendered, or the page relies on scripts, fonts, or delayed content. Increase the bounded timeout and use a page-specific readiness condition with browser automation when necessary.
Only the visible area appears The CLI screenshot is viewport-oriented. Use an explicit full-page capture workflow; a tall window alone is not a guarantee.
PHP reports that proc_open() is unavailable Process-control functions can be disabled in the PHP runtime configuration. Enable it in the appropriate PHP configuration if permitted by the host, or move capture to a worker/service where process execution is allowed.
Capture works in a terminal but fails in the web app The service runs with a different user, environment, working directory, or permissions. Use absolute paths, configure the browser path explicitly, and compare access rights under the actual service account.

7. Performance, reliability, and cost

Each local capture starts a browser process, which consumes CPU and memory while it loads and renders the page. Limit concurrent jobs to the capacity of the Debian host, set a bounded timeout, and reuse a managed browser process only if your chosen automation stack supports it safely. Large viewports and heavy pages can increase resource use and output size.

For reliability, record the URL, duration, browser exit code, and diagnostic output for failed jobs. Retry only transient failures, with a bounded retry count and delay; repeated retries against a consistently broken URL waste worker time. Use unique output files and check both process status and file existence before publishing the image.

The local method has no per-screenshot API charge, but it does require a maintained Debian host, browser installation, disk space, and worker capacity. Account for infrastructure and operations costs when capture volume grows.

8. Or skip the browser setup

If you do not want to install and operate Chrome on Debian, ScreenshotNeo accepts one GET request with a URL and returns a screenshot or PDF. The API supports PNG, JPEG, and WebP screenshots and has 63 options, including viewport and device presets, full-page capture with lazy images loaded, element capture, custom waits, and PDF settings. See the [ScreenshotNeo documentation](https://screenshotneo.com/docs/) for request options.

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)
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. See [ScreenshotNeo](https://screenshotneo.com) and [create a free account](https://screenshotneo.com/account/sign-up/) to get started.

9. FAQ

Does this method need a visible desktop session?

No. Headless mode runs without displaying a browser UI.

Does changing the viewport capture the whole page?

No. The documented CLI screenshot is viewport-oriented. Use a full-page browser automation workflow for the complete document.

Can I use Chromium instead of Chrome?

Yes, provided the installed Chromium executable runs on the Debian host and the configured path points to it. Package names and paths depend on the release and installation.

Where does Chrome save the screenshot?

The CLI writes screenshot.png in its current working directory by default. Set the process working directory or use an explicit output path to control where your file goes.