Production Configuration
The php.ini settings production needs: errors in the log and not on the page, sane limits, an explicit timezone, and a small dev gap.
ini_get(string $option): string|falsePHP reads its settings from php.ini when the process starts. The source distribution ships two templates, php.ini-development and php.ini-production, and they differ mainly in one decision: whether an error is shown to the visitor or written to a log.
The settings that decide your next incident
display_errors = Off keeps file paths, stack traces and SQL out of the response. log_errors = On with a writable error_log keeps them somewhere you can read. error_reporting = E_ALL records deprecations too, so the next version upgrade is not a surprise. PHP 8.5 adds fatal_error_backtraces, enabled by default, which writes a backtrace for fatal errors that are not exceptions. expose_php = Off drops the X-Powered-By header, and zend.exception_ignore_args = On keeps function arguments out of logged traces, so a password passed to a function is not stored in the log.
Limits
memory_limit caps one request. max_execution_time caps PHP's own time, not time spent waiting on a database call. upload_max_filesize must be at or below post_max_size, or the upload is discarded before your code sees it. Set date.timezone explicitly rather than relying on the UTC fallback.
Keep the gap small and visible
Development and production should differ in error display, opcache validation and credentials, and in nothing else. php --ini=diff, added in PHP 8.5, prints only the settings that are not at their default value, which makes two machines comparable in one glance. Note that the CLI and FPM can load different ini files.
Example
When to use it
- Harden a new server before it takes real traffic.
- Find out why an exception appeared in the browser of a live site.
- Explain why a 9MB upload silently vanished when upload_max_filesize allowed 10MB but post_max_size was 8MB.
More examples
A production ini drop-in
A numbered file in conf.d is loaded after the main php.ini, so your settings win without editing the package's file and losing the change on the next upgrade. error_reporting accepts the constant names you write in PHP, and E_ALL is exactly the same value as its integer; the names are easier to read. opcache.validate_timestamps = 0 means PHP stops checking whether a file changed, which is the fastest setting and the reason a deploy must reload FPM.
; /etc/php/8.5/fpm/conf.d/99-production.ini
display_errors = Off
display_startup_errors = Off
log_errors = On
error_reporting = E_ALL
error_log = /var/log/php/fpm-error.log
fatal_error_backtraces = On
zend.exception_ignore_args = On
expose_php = Off
memory_limit = 192M
max_execution_time = 30
post_max_size = 12M
upload_max_filesize = 10M
date.timezone = UTC
opcache.enable = 1
opcache.validate_timestamps = 0Refuse to boot in a dangerous configuration
A misconfigured server is quieter than a broken one, which is why it survives for months. This turns it into an immediate, loud failure on the first request after a bad deploy. Keep the message to the visitor empty of detail and put the detail in the log, which is the same rule the ini settings above enforce.
<?php
declare(strict_types=1);
// src/ConfigGuard.php, called from public/index.php before anything else.
final class ConfigGuard
{
public static function assertProductionSafe(): void
{
if (getenv('APP_ENV') !== 'production') {
return;
}
$faults = [];
if (ini_get('display_errors') !== '') {
$faults[] = 'display_errors is on';
}
if (ini_get('log_errors') !== '1') {
$faults[] = 'log_errors is off';
}
if (ini_get('error_log') === '') {
$faults[] = 'error_log is unset';
}
if ($faults === []) {
return;
}
error_log('refusing to serve: ' . implode('; ', $faults));
http_response_code(500);
exit('Configuration error');
}
}Comparing two machines
Run the same three commands on your laptop and on the server and compare the output line by line. This is how you find the setting that only production has. Remember that these report the CLI's configuration; FPM loads its own php.ini and pool overrides, so confirm web values through a temporary phpinfo() page restricted to localhost, then delete it.
# PHP 8.5: only the settings that differ from the built-in default
php --ini=diff
# which file is actually being read
php -i | grep -i 'Loaded Configuration File'
# '' means Off, '1' means On
php -r 'var_dump(ini_get("display_errors"), ini_get("log_errors"));'
Discussion