TL;DR
- Security first: disabling dangerous functions, restricting file access with
open_basedir, and hiding version fingerprints (expose_php) close off the most common PHP attack vectors. - Performance follows configuration: OPcache, realpath caching, and tuned memory/execution limits can cut response times dramatically without touching a single line of application code.
- PHP-FPM matters as much as php.ini: pool-level settings (process manager mode,
pm.max_children, slow log) directly affect both throughput and your server's resilience under load. - Defaults are not production-ready: the stock
php.ini shipped by most distros is tuned for compatibility, not for a hardened, high-traffic production environment — you need to change it deliberately.
Introduction
If you've ever deployed a PHP application and left php.ini untouched, you're running with the digital equivalent of factory-default Wi-Fi credentials. PHP's default configuration is designed to "just work" across the widest possible range of use cases — which means it leaks information to attackers, allows risky functions to run unchecked, and leaves performance gains sitting on the table.
The good news: hardening PHP doesn't require rewriting your application. Most of the wins come from a focused set of changes to php.ini and, if you're running PHP-FPM, your pool configuration. This guide walks through exactly which settings to change, why they matter, and how to verify you've done it right — covering both the security angle and the performance angle, since in PHP configuration the two are more connected than most people realize (a leaner, more restricted runtime is often a faster one too).
A diagram showing the PHP request lifecycle — from the web server, through PHP-FPM, to the PHP interpreter and OPcache — would help readers visualize exactly where each setting in this guide takes effect.
Finding and understanding your php.ini
Before changing anything, locate the configuration file actually being used:
php --ini
This prints the loaded php.ini path plus any additional .ini files scanned from a config directory (common on Debian/Ubuntu with separate FPM and CLI configs). Always confirm you're editing the file that matches your SAPI (php-fpm, apache2handler, cli), since it's common to edit the CLI config by mistake and wonder why nothing changed on the website.
You can also check any setting at runtime with phpinfo() or:
php -i | grep session.cookie_httponly
Section 1: hardening PHP for security
Hide PHP's fingerprint
By default, PHP announces itself in the X-Powered-By HTTP header, giving attackers an easy way to fingerprint your stack and target known vulnerabilities for your exact version.
expose_php = Off
Why it matters: Security through obscurity isn't a real defense on its own, but reducing reconnaissance surface is free and has zero downside. Pair this with hiding the web server's own version string (ServerTokens Prod in Apache, server_tokens off; in Nginx).
Disable dangerous functions
A huge share of PHP remote code execution (RCE) exploits rely on a small set of functions that most web applications never legitimately need.
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,pcntl_exec
Common pitfall: Blindly copying a "recommended" disable list can break legitimate functionality — frameworks like Symfony or Laravel may use proc_open for queue workers, and some PDF/image libraries shell out internally. Audit your actual dependencies (composer show plus a grep through vendor code) before disabling anything, and test in staging first.
Restrict Filesystem access with open_basedir
open_basedir confines PHP's file operations to a defined set of directories, so even if an attacker achieves file inclusion or path traversal, they can't read /etc/passwd or write outside your app.
open_basedir = /var/www/myapp:/tmp
Real-world example: A vulnerable file-upload endpoint that fails to validate paths becomes far less dangerous with open_basedir in place — the attacker's traversal attempt (../../../../etc/passwd) simply fails at the PHP engine level, before it ever reaches your application logic.
Lock Down Remote File Access
Two settings control whether PHP can treat URLs as files — a classic vector for Remote File Inclusion (RFI):
allow_url_fopen = Off
allow_url_include = Off
allow_url_include should essentially always be off; there's no legitimate reason to include() a remote URL in production code. allow_url_fopen is used more broadly (some HTTP client libraries fall back to it), so test carefully — but where possible, prefer cURL or Guzzle over stream wrappers.
Harden Session handling
Session fixation and cookie theft are still common in the wild. These settings meaningfully raise the bar:
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Strict"
session.use_strict_mode = 1
session.sid_length = 48
session.sid_bits_per_character = 6
cookie_httponly blocks JavaScript access to the session cookie, mitigating XSS-driven session theft.cookie_secure ensures the cookie is only sent over HTTPS.use_strict_mode rejects uninitialized session IDs, closing off session fixation attacks.
Control error reporting in production
Verbose errors are a debugging tool in development and an information disclosure vulnerability in production — stack traces reveal file paths, library versions, and sometimes query fragments.
display_errors = Off
display_startup_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
error_log = /var/log/php/error.log
Best practice: Keep error_reporting at E_ALL In staging, so you catch real issues, but never render errors to the browser in production. Ship them to a log aggregator instead.
Limit file upload risk
file_uploads = On
upload_max_filesize = 8M
max_file_uploads = 5
If your application doesn't accept uploads at all, set it file_uploads = Off outright. Where uploads are needed, pair this config with server-side validation of MIME type and file extension — php.ini Limits are a backstop, not a substitute for application-level checks.
Section 2: Tuning PHP for Performance
Enable and configure OPcache
OPcache caches compiled bytecode in shared memory, eliminating the need to re-parse and re-compile PHP scripts on every request. This is, by a wide margin, the single biggest performance lever available at the configuration level.
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.revalidate_freq = 0
Critical pitfall: opcache.validate_timestamps = 0 means PHP will not notice when you deploy new code — it keeps serving cached bytecode until OPcache is reset. This is exactly what you want in production for maximum performance, but you must trigger an OPcache reset (or a PHP-FPM reload) as part of your deployment pipeline, or you'll be debugging "my changes aren't showing up" for longer than you'd like.
In development, set opcache.validate_timestamps = 1 with a low revalidate_freq so file changes are picked up automatically.
Every time PHP resolves a file path (include, require, fopen), it performs filesystem stat calls. The realpath cache avoids repeating that work.
realpath_cache_size = 4096k
realpath_cache_ttl = 600
Frameworks with deep autoloading trees (Symfony, Laravel with many packages) benefit noticeably from a larger cache size, especially under high concurrency where filesystem contention adds up.
Right-size memory and execution limits
memory_limit = 256M
max_execution_time = 30
max_input_time = 60
Common pitfall: Setting memory_limit far higher "just in case" doesn't improve performance — it just delays the point at which a runaway script (e.g., an unbounded loop over a database result) gets killed, and it makes it easier for a single bad request to starve the server's available memory under concurrent load. Set it based on actual profiling of your heaviest legitimate endpoints, not guesswork.
Configure PHP-FPM Pools Correctly
If you're running PHP-FPM (the standard for Nginx and increasingly common with Apache), the pool configuration (www.conf or a custom pool file) matters as much as php.ini itself.
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500
pm.max_children should be sized against available RAM divided by average process memory footprint — not an arbitrary round number.pm.max_requests recycles worker processes periodically, which helps mitigate memory leaks in long-running workers (common with certain C extensions).
Enable the slow log to catch performance regressions before users complain:
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s
A before/after benchmark chart (requests per second, p95 latency) comparing default php.ini vs. a hardened+tuned configuration would make a compelling visual here — even a simple ab or wrk load test screenshot adds credibility.
Putting It Together: A Sample Hardened php.ini Snippet
; Security
expose_php = Off
allow_url_fopen = Off
allow_url_include = Off
open_basedir = /var/www/myapp:/tmp
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
display_errors = Off
log_errors = On
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Strict"
; Performance
opcache.enable = 1
opcache.memory_consumption = 256
opcache.validate_timestamps = 0
realpath_cache_size = 4096k
realpath_cache_ttl = 600
memory_limit = 256M
max_execution_time = 30
Best practices checklist
- Maintain separate configs for development and production — never share
opcache.validate_timestamps or display_errors settings between them. - Automate OPcache resets in your CI/CD deploy step (
cachetool or a PHP-FPM reload). - Audit
disable_functions whenever you add a new Composer dependency. - Load-test after tuning PHP-FPM pool settings — theoretical sizing and real-world behavior under load often diverge.
- Version-control your
php.ini and pool files alongside your application code, rather than hand-editing servers.
Conclusion
Hardening your PHP configuration is one of the highest-leverage, lowest-effort improvements you can make to a production application. A handful of php.ini and PHP-FPM changes can simultaneously close off common attack vectors and meaningfully improve response times — no application refactor required. Start with the security settings (they're non-negotiable for any production system), then move to OPcache and PHP-FPM pool tuning once you've established a performance baseline.
If you haven't reviewed your php.ini since your last framework upgrade, that's a good sign it's due. Audit it this week, test the changes in staging, and roll them out deliberately rather than all at once.
FAQ
Does disabling PHP functions break Composer packages? It can, if a package relies on a disabled function internally (commonly proc_open or exec for shelling out to external binaries). Always test your full dependency tree in staging before disabling functions in production.
Do I need to restart PHP-FPM after changing php.ini? Yes — PHP-FPM loads php.ini and pool configuration at startup. Use systemctl reload php-fpm (or restart for changes to pool process-manager settings) after any edit.
Will enabling OPcache affect my ability to deploy hotfixes quickly? Only if you don't automate cache invalidation. With opcache.validate_timestamps = 0, add an OPcache reset step to your deployment script so new code is picked up immediately after deploy.
Is open_basedir worth the compatibility risk? For most applications, yes — it's one of the strongest single mitigations against path traversal and local file inclusion. The main risk is forgetting to include a directory your app legitimately needs (like a system temp path), which is easy to catch in staging.
How do I know if my memory_limit is set correctly? Profile your heaviest real endpoints under realistic load and set the limit with headroom above observed peak usage — not an arbitrary high number "to be safe," which only delays detection of runaway scripts.