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

Apache does not use a special version of proc_open(). The same PHP function behaves differently when its parent process has a different SAPI, user, working directory, environment, PHP configuration, or operating-system limits. Fix the mismatch by measuring both runtimes, then invoke the child with an absolute executable path, an explicit working directory, a deliberate environment, separate output pipes, and checked exit status.

Why the same proc_open() call changes between web and CLI

A command started by PHP inherits context from the PHP process that starts it. A terminal command normally runs inside your login account with your shell’s environment and current directory. A web request may run in Apache’s PHP module or through FastCGI/PHP-FPM, often as a service account with a restricted environment and different limits.

That means “works in CLI but not in Apache” is usually a runtime-context problem, not an Apache-specific implementation of proc_open(). The relevant differences are:

  • PHP SAPI and process manager: CLI, Apache module, or PHP-FPM behind FastCGI.
  • Operating system and PHP version.
  • Operating-system user and group.
  • Current working directory.
  • PATH and other child-environment variables.
  • PHP configuration, including open_basedir and disabled functions.
  • Read, write, execute, and traversal permissions.
  • Process, file-descriptor, timeout, and resource limits.

First determine which of these differs. Do not “fix” the problem by adding random Apache variables or changing permissions broadly.

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

1. Capture the effective runtime in both contexts

Create a temporary diagnostic script that is protected by authentication or restricted to an administrator. Never return secrets such as API keys, cookies, or complete environment dumps in a public response.

<?php
header('Content-Type: text/plain');

$keys = ['PATH', 'HOME', 'TMPDIR', 'USER'];

echo 'PHP_VERSION=' . PHP_VERSION . "n";
echo 'PHP_SAPI=' . PHP_SAPI . "n";
echo 'PHP_BINARY=' . PHP_BINARY . "n";
echo 'getcwd()=' . (getcwd() ?: '(false)') . "n";
echo 'open_basedir=' . (ini_get('open_basedir') ?: '(empty)') . "n";
echo 'disable_functions=' . (ini_get('disable_functions') ?: '(empty)') . "n";

foreach ($keys as $key) {
    $value = getenv($key);
    echo $key . '=' . ($value === false ? '(unset)' : $value) . "n";
}

if (function_exists('posix_geteuid')) {
    echo 'posix_geteuid=' . posix_geteuid() . "n";
}

Run the equivalent information from the CLI and compare it with the protected web request. A missing PATH, a different PHP_BINARY, or a different getcwd() immediately explains many failures. The effective user can be checked with the operating system’s process tools or, where available, PHP’s POSIX functions.

2. Make the working directory absolute

The third argument after descriptors is $cwd, the child’s initial working directory. PHP accepts an absolute directory path or null, which means the current working directory of the PHP process. A web request’s current directory is not a safe assumption when your command uses relative input, output, configuration, or script paths.

Use a known directory and verify it before starting:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
$cwd = '/srv/my-app/work';
if (!is_dir($cwd) || !is_readable($cwd) || !is_executable($cwd)) {
    throw new RuntimeException('Invalid or inaccessible working directory');
}

Use absolute paths for the executable and files as well. Directory traversal permission matters: the service account must be able to pass through every parent directory, not merely read the final file.

3. Make executable lookup predictable

With the array form of command, a simple executable name is resolved through the current PATH. If PATH is unset, the operating system uses its default search paths. Those paths commonly differ between a login shell and Apache or PHP-FPM.

Prefer an absolute executable path while diagnosing:

$command = ['/usr/local/bin/my-program', '--format', 'json', '/srv/my-app/input.dat'];

Confirm the path and permissions as the same service user that runs PHP. If you deliberately rely on lookup, provide a child environment containing the required PATH; do not assume Apache’s internal variables are the operating system environment inherited by the child.

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

4. Pass only the environment the child needs

The $env_vars argument controls the child environment. Passing null inherits the current PHP process environment. Passing an array supplies that environment, so an incomplete array can remove variables the program expects. Include PATH when lookup or subprocesses depend on it, and add application-specific values explicitly.

$env = [
    'PATH' => '/usr/local/bin:/usr/bin:/bin',
    'LANG' => 'C.UTF-8',
    'APP_ENV' => 'production',
];

$process = proc_open($command, $descriptors, $pipes, $cwd, $env);

This example is intentionally selective. Do not copy it if the child needs credentials, a home directory, a locale, or another variable; add those through a secure configuration mechanism. Avoid logging their values.

5. Use the modern array command and capture diagnostics

PHP documentation states: “As of PHP 7.4.0, command may be passed as array of command parameters.” The array form starts the program directly without shell parsing, which avoids many quoting and injection errors. Descriptor 1 is stdout and descriptor 2 is stderr.

<?php
$command = ['/absolute/path/to/program', '--option', 'value'];
$descriptors = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w'],
];
$cwd = '/absolute/path/to/working-directory';
$env = ['PATH' => '/usr/local/bin:/usr/bin:/bin'];

$process = proc_open($command, $descriptors, $pipes, $cwd, $env);
if (!is_resource($process)) {
    throw new RuntimeException('proc_open() could not start the process');
}

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) {
    throw new RuntimeException(
        'Child failed with exit code ' . $exitCode . ': ' . trim($stderr)
    );
}

echo $stdout;

Close the child’s stdin when no input is required; otherwise a program waiting for end-of-file can appear to hang. Read stderr before reporting failure. A “not found” message points to the executable path or PATH; “permission denied” points to the service account, parent directories, mount options, or policy; a nonzero exit with useful stderr means the child started and rejected its inputs or configuration.

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.

String commands, shells, and Windows

A string command is parsed by a shell, so quoting and metacharacters become part of the problem. Treat every interpolated value as untrusted; validate it and use the array form whenever possible. If shell syntax is genuinely required, apply the platform’s escaping rules and keep the composed command as small as possible.

On Windows, PHP documents that string commands go through cmd.exe unless bypass_shell is enabled. Executable paths, drive letters, quoting, and the service account’s profile can therefore differ substantially from a CLI test. Test with the same PHP build, account, and absolute paths used by the web process.

Check permissions and PHP restrictions

CLI often runs as a developer or deployment user, while Apache or PHP-FPM uses a dedicated service account. Compare access to:

  • The executable and every parent directory.
  • The explicit working directory.
  • Input files and configuration files.
  • Output directories and temporary locations.
  • Any interpreter, shared library, or helper executable the child loads.

Also compare PHP configuration between SAPIs. open_basedir can restrict filesystem access even when operating-system permissions appear correct. A function may be enabled in CLI but disabled for web PHP. Check the web runtime’s effective configuration rather than relying on the CLI’s php.ini.

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

Apache’s SetEnv and PassEnv are not interchangeable: Apache maintains an internal request environment, while operating-system environment variables are a separate concern. Confirm what PHP actually sees with getenv() and what the child receives through $env_vars.

When the process starts but stalls

A successful proc_open() call does not guarantee that the request can finish. Under load, Apache and PHP-FPM accounts can hit process-count or open-file limits. Review the limits applied to the relevant service account and process manager, including nproc and nofile. Also check for pipe deadlocks: a child that writes enough stderr while the parent reads only stdout can block.

For long-running work, consider a queue or background worker instead of holding an HTTP request open. If you must wait synchronously, define an application timeout, collect both streams without blocking, and terminate the child through a deliberate recovery path. Do not hide a stuck process by increasing the web-server timeout alone.

A repeatable diagnosis sequence

  1. Record PHP_VERSION, PHP_SAPI, PHP_BINARY, getcwd(), effective user, relevant environment values, and PHP restrictions in CLI and a protected web request.
  2. Run a harmless test using an absolute executable and absolute $cwd. Use absolute input and output paths too.
  3. Compare the service account’s permissions and the web SAPI’s open_basedir and disabled-function settings.
  4. Start the child with separate stdout and stderr pipes, close stdin when unused, and record proc_close()‘s exit code.
  5. If lookup is required, supply an explicit PATH; otherwise retain the absolute executable path.
  6. Only after the minimal test works, add application arguments, custom environment values, shell syntax, or production concurrency.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common symptoms and fixes

Symptom Likely difference Fix
“Command not found” Different or unset PATH Use an absolute executable path or provide the required child PATH.
“Permission denied” Apache/PHP-FPM service account lacks access Grant narrowly scoped access to the executable, parents, working directory, and files.
Relative file is missing Different getcwd() Set an absolute $cwd and absolute file paths.
Starts in CLI, fails before launch on web open_basedir, disabled function, or SAPI configuration Inspect effective web configuration and logs; change policy deliberately.
Starts but returns nonzero Child configuration, arguments, locale, or input error Read stderr and inspect the exit code; compare the supplied environment.
Hangs under traffic Pipe deadlock or process/file limits Read both streams correctly and review nproc/nofile and worker capacity.

Historical bug reports are not a diagnosis

PHP bug #50524 describes a historical Windows working-directory discrepancy and records a fix in September 2010. It is evidence about that old report, not proof that current Apache PHP generally mishandles cwd. Check your installed PHP version, operating system, SAPI, and reproducible paths before attributing a failure to a bug.

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

Or skip the browser setup

If the surrounding task is collecting screenshots while you diagnose a web runtime, ScreenshotNeo provides a direct website screenshot API instead of requiring you to maintain browser automation. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the page verdict and billing status. Its MCP server lets Claude, Cursor, or another MCP client use take_screenshot, get_page_info, and capture_pdf.

One call is enough:

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

See the ScreenshotNeo documentation for the other 63 capture options, including full-page and element capture, device and retina settings, PDF output, custom CSS and JavaScript, blocking rules, cookies and headers, geolocation, caching, signed links, asynchronous jobs, bulk capture, and usage reporting. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Frequently Asked Questions

Does changing Apache’s PATH permanently fix every proc_open problem?

No. PATH only addresses executable lookup. The working directory, service account, PHP restrictions, child arguments, filesystem access, and resource limits can still differ.

Should I pass null or an array for $env_vars?

Use null when the child should inherit PHP’s environment. Use an array when you need a controlled environment, and include every variable the child requires.

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.

Is proc_open() unavailable only because PHP runs under Apache?

Not inherently. Check the effective web configuration for disabled functions and compare it with CLI; the SAPI itself is not a separate proc_open implementation.

The Bottom Line

Measure both PHP contexts, then remove implicit assumptions: absolute executable, absolute cwd, explicit child environment, correct service-user permissions, separate diagnostics, and checked exit status.

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.