PHPphp.ini & Configuration

php.ini & Configuration

php.ini is the master configuration file that PHP reads the moment it starts up, before a single line of your code runs. It controls things your scripts cannot change on their own: how much memory a request may consume, whether errors are printed to the screen or only written to a log, how large an uploaded file is allowed to be, and how long a script may run before PHP kills it. Most "works on my machine but not on the server" bugs that have nothing to do with your code trace back to a php.ini difference — so knowing where it lives, how to inspect it, and how to override it safely is a core PHP skill, not an ops-only concern.

Finding the active php.ini

A single machine can easily have several php.ini files sitting on disk — one shipped with an installer, one from a package manager, one left over from an old version. The only way to know which one PHP is actually using is to ask PHP itself.

Inspecting the active configuration

Bash
php --ini
Configuration File (php.ini) Path: /etc/php/8.3/cli
Loaded Configuration File:         /etc/php/8.3/cli/php.ini
Scan for additional .ini files in: /etc/php/8.3/cli/conf.d
Additional .ini files parsed:      /etc/php/8.3/cli/conf.d/10-opcache.ini,
                                    /etc/php/8.3/cli/conf.d/20-mysqli.ini

Loaded Configuration File is the one file that wins overall. The Scan for additional .ini files in directory is a folder of smaller, per-extension .ini snippets (things like mysqli.ini or opcache.ini) that get merged in afterward, usually in alphabetical order — later files can override earlier ones. From inside a running script you can get the same answer with php_ini_loaded_file() and php_ini_scanned_files().

CLI and the web server often load different files
Run `php --ini` and you get the file used by the **command-line** binary. That is frequently a *different* file from the one your web server uses to run the same PHP version, because most Linux distributions ship separate configuration trees for each SAPI (Server API) — commonly something like `/etc/php/8.3/cli/php.ini` for the CLI and `/etc/php/8.3/apache2/php.ini` or `/etc/php/8.3/fpm/php.ini` for the web-facing SAPI. This is the classic reason a setting you changed and confirmed with `php --ini` on the terminal has zero effect in the browser: you edited the wrong copy. To check what the web server sees, put `<?php phpinfo();` in a script and load it in the browser, or check the `Loaded Configuration File` line in that output instead of trusting the CLI.
Directives you will touch the most
  • error_reporting — a bitmask of which error levels PHP acts on at all (e.g. E_ALL for everything, or E_ALL & ~E_DEPRECATED to silence deprecation notices). This decides what gets reported; it does not decide where it goes.

  • display_errors — whether reported errors are printed directly into the HTTP response / terminal output. Independent from error_reporting: you can report everything but display nothing.

  • log_errors and error_log — whether reported errors are written to a log file instead, and which file to write them to.

  • memory_limit — the maximum RAM a single PHP request may allocate before it is killed with a fatal Allowed memory size exhausted error. Set to -1 for unlimited, which you should basically never do on a public server.

  • upload_max_filesize — the largest single file PHP will accept via a multipart/form-data upload.

  • post_max_size — the largest total size of a POST request body, including all uploaded files combined. Must be greater than or equal to upload_max_filesize, or uploads silently fail.

  • max_execution_time — seconds a script may run before PHP aborts it with a fatal timeout error. Does not count time spent waiting on external I/O like a slow database query in every configuration, which is a common source of confusion.

A representative slice of php.ini

Text
error_reporting = E_ALL
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
memory_limit = 256M
upload_max_filesize = 20M
post_max_size = 25M
max_execution_time = 30
Overriding settings per directory: .htaccess

On Apache with the older mod_php module, you can override certain directives for a specific folder without touching the global php.ini at all, using php_value (for string/numeric directives) and php_flag (for boolean on/off directives) inside a .htaccess file.

.htaccess — folder-level overrides (mod_php only)

Text
php_value upload_max_filesize 50M
php_value post_max_size 55M
php_flag   display_errors Off
php_value / php_flag do not work with PHP-FPM
This mechanism depends entirely on `mod_php` running PHP as an Apache module. It has no effect whatsoever with **PHP-FPM**, which is the standard setup for Nginx and is increasingly common with Apache too (via `mod_proxy_fcgi`). If a `.htaccess` `php_value` line is silently ignored and Apache does not even throw a "500 Internal Server Error" for an unrecognized directive, the most likely cause is that the site is actually running on PHP-FPM. Under FPM, use pool configuration (below) or `ini_set()` instead.
Overriding settings per pool: PHP-FPM

PHP-FPM manages one or more "pools" of worker processes, each configured by its own file (commonly www.conf or a file under a pool.d/ directory). Directives are set there with the php_admin_value / php_admin_flag or php_value / php_flag prefixes, and take effect only for requests handled by that pool.

/etc/php/8.3/fpm/pool.d/www.conf

Text
[www]
; ...pool definition above...

php_admin_value[memory_limit] = 512M
php_admin_value[upload_max_filesize] = 50M
php_admin_value[post_max_size] = 55M
php_admin_flag[display_errors] = off

The _admin_ variants are locked — a script cannot override them later with ini_set(). The plain php_value/php_flag variants set a new default that user-level code is still allowed to change at runtime, if the directive permits it. FPM must be restarted (not just reloaded) for pool-level changes to take effect: systemctl restart php8.3-fpm.

Overriding settings at runtime: ini_set()

Some directives can be changed from inside a running script with ini_set(), but only for the remainder of that request — the change never touches the file on disk and never affects other requests. Whether a given directive is eligible depends on its declared "changeable mode": directives marked PHP_INI_USER or PHP_INI_ALL in the PHP manual can be set this way; ones marked PHP_INI_SYSTEM or PHP_INI_PERDIR cannot — those require php.ini, a pool file, or .htaccess.

debug-this-script.php

PHP
&lt;?php

// Only affects this one request, only for directives PHP allows at runtime
ini_set('display_errors', '1');
ini_set('error_reporting', E_ALL);
ini_set('memory_limit', '512M');

// memory_limit is PHP_INI_ALL, so this works even though it looks like
// a "system" setting — but max_execution_time cannot be raised this way
// on every SAPI, and some hosts lock it down further regardless.
ini_get() to check, not guess
Before relying on `ini_set()` succeeding, read the current value with `ini_get('memory_limit')` and compare it against what you asked for. `ini_set()` returns the *previous* value on success and `false` on failure, so checking the return value is the reliable way to confirm an override actually took effect rather than being silently ignored by a locked-down host.
Recommended settings: development vs. production

Directive

Development

Production

display_errors

On

Off

error_reporting

E_ALL

E_ALL (report everything; just don’t display it)

expose_php

On or Off (developer preference)

Off

log_errors

On

On

memory_limit

-1 or a generous value (e.g. 512M)

A tight, measured value (e.g. 256M)

expose_php controls whether PHP adds an X-Powered-By: PHP/8.3.x header to every response. It has no effect on how your application behaves — it only affects how much version information you hand to anyone scanning your server for known vulnerabilities in a specific PHP release.

display_errors = On in production leaks real information
With `display_errors` on, an uncaught exception or a raw SQL error prints straight into the HTTP response: full file paths, function arguments, sometimes a database connection string or query text, and a complete stack trace of your application's internal structure. To an attacker this is a free map of your server layout and code. The correct production posture is `display_errors = Off` combined with `log_errors = On`, so the detail still exists — in a log file only you can read — while visitors see a generic error page.
A note on upload size directives

upload_max_filesize and post_max_size are frequently misconfigured as a pair. Raising only upload_max_filesize while leaving post_max_size smaller (or equal) means the upload is rejected before your code even sees it — PHP populates $_FILES as empty and $_POST as empty too, with no exception thrown, which looks indistinguishable from the user simply not selecting a file unless you specifically check $_SERVER['CONTENT_LENGTH'] against post_max_size.

A correctly paired upload configuration

Text
upload_max_filesize = 20M
post_max_size       = 25M   ; leaves headroom for other form fields
Tip
Keep one canonical `php.ini` snippet (or FPM pool file) checked into your project's infrastructure repository alongside the application code, and apply it identically to every environment through your deployment process. Hand-editing `php.ini` directly on a production box is how "it was fine yesterday" configuration drift happens — the file should be reproducible from version control, not from memory of what someone typed into `vim` six months ago.