Chapter 19: Performance and Deployment
Performance comes from efficient code, well tuned configuration, and good caching at every level. Deployment builds on that foundation by providing repeatable, isolated environments and clear monitoring. This chapter shows how to configure OPcache, profile bottlenecks, introduce caching layers, prepare production settings, containerize PHP, and observe your system in real time.
OPcache and Realpath Cache
PHP normally parses and compiles every script on each request. OPcache stores compiled bytecode in memory to remove this overhead. Realpath cache remembers file path resolutions so repeated includes avoid filesystem lookups.
Enabling and tuning OPcache
Enable OPcache in php.ini and adjust its limits for your project size. Increase the memory size if you have many scripts and raise interned strings buffer for large frameworks.
; opcache.ini
zend_extension=opcache
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.save_comments=1
Disabling validate_timestamps avoids checking file modification times on each request. Reload the service after deployment instead.
Clearing OPcache safely during deployments
When you deploy new code, invalidate OPcache so the next request reloads scripts. You can call opcache_reset() in a maintenance script or reload the PHP-FPM pool.
// clear_opcache.php
if (function_exists('opcache_reset')) {
opcache_reset();
}
Tuning the realpath cache
The realpath cache remembers directory lookups and symbolic links. Set larger limits for big frameworks with deep include trees.
; php.ini
realpath_cache_size=4096K
realpath_cache_ttl=600
Profiling and Bottlenecks
Profiling shows where time and memory are consumed. Collect data under realistic loads and use tools that visualize call graphs and memory allocation. Optimize high impact paths, not random code.
Using Xdebug profiler for local analysis
Xdebug can record function call times into cachegrind files viewable with tools like QCacheGrind or Webgrind. Enable only when profiling, since it adds overhead.
; php.ini
xdebug.mode=profile
xdebug.output_dir=/tmp/profiles
Using xhprof and tideways_xhprof
For lower overhead profiling, use xhprof or tideways_xhprof. They sample call times with minimal impact, making them suitable for staging or short bursts in production.
xhprof_enable();
... // your app logic
$data = xhprof_disable();
file_put_contents('/tmp/profile.json', json_encode($data));
Reading call graphs and focusing optimizations
Call graphs reveal which functions dominate runtime. Optimize those first: reduce I/O, cache repeated computations, and precompute constants. Micro-optimizations like string concatenation order rarely help compared to reducing query counts or external API calls.
Caching Layers
Caching removes redundant work. PHP projects typically layer caches: opcode, data, application, and HTTP. Each layer shortens a different path between request and result.
Using APCu for in-memory data caching
APCu stores simple key-value pairs in memory for one process pool. It suits shared configuration, computed templates, or small results reused often.
if (apcu_exists('config')) {
$config = apcu_fetch('config');
} else {
$config = load_config_from_db();
apcu_store('config', $config, 300);
}
Adding shared caches with Redis or Memcached
Distributed caches work across processes or servers. They reduce load on databases and improve response times for frequent reads.
$redis = new Redis();
$redis->connect('redis', 6379);
$key = 'user:' . $id;
if ($cached = $redis->get($key)) {
return unserialize($cached);
}
$user = $repo->find($id);
$redis->setex($key, 600, serialize($user));
Leveraging HTTP and reverse proxies
Static and dynamic pages can benefit from edge caching. Set response headers like Cache-Control, ETag, and Last-Modified. Reverse proxies such as Varnish or Cloudflare cache responses and serve them directly when valid.
header('Cache-Control: public, max-age=300');
header('ETag: "abc123"');
Configuration for Production
Production configuration prioritizes stability, predictable performance, and controlled logging. Keep detailed error output off, isolate logs, and disable modules you do not use.
Setting secure and consistent defaults
Disable features not needed in production and restrict file uploads. Enable OPcache, set timeouts, and avoid high memory use per process.
; production.ini
display_errors=0
log_errors=1
error_log=/var/log/php/error.log
expose_php=0
file_uploads=1
upload_max_filesize=10M
post_max_size=12M
max_execution_time=30
max_input_time=60
memory_limit=256M
opcache.enable=1
Separating configuration by environment
Store different .ini sets or use environment variables. The phpinfo() page can confirm which files are active but never leave it public on production servers.
Docker and Containerized PHP
Containers isolate dependencies and environment settings, ensuring consistent behavior from laptop to production. Docker allows reproducible builds and simplified scaling when combined with orchestration tools.
Creating a minimal PHP-FPM image
Start with an official base image and install only the extensions your app needs. Use multi-stage builds to keep the final image small.
# Dockerfile
FROM php:8.3-fpm-alpine AS base
RUN docker-php-ext-install pdo_mysql opcache
WORKDIR /var/www/html
COPY . .
FROM base AS prod
ENV APP_ENV=production
CMD ["php-fpm"]
Running with Nginx using Docker Compose
Compose files define multiple services and their networks. Connect php-fpm with Nginx through a shared volume and network.
# docker-compose.yml
version: '3.9'
services:
app:
build: .
volumes:
- .:/var/www/html
networks:
- backend
web:
image: nginx:1.27-alpine
volumes:
- .:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- "8080:80"
networks:
- backend
networks:
backend:
Configuring PHP-FPM and health checks
Expose health endpoints and tune process settings. For small sites pm = dynamic works well; for APIs under load consider pm = static with fixed workers.
; www.conf
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.status_path = /status
Observability and Error Monitoring
Observability turns unknowns into data. Instrument logs, metrics, and traces so you can detect issues before users report them. Choose lightweight libraries that integrate with your framework and centralize outputs.
Structured logging with Monolog
Structured logs include context, timestamp, and level. Use handlers to send logs to files, syslog, or external services. Format logs as JSON for ingestion by tools such as ELK or Loki.
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('app');
$log->pushHandler(new StreamHandler(__DIR__ . '/../var/app.log', Logger::INFO));
$log->info('User login', ['user' => $id]);
Adding metrics and health endpoints
Expose application metrics in a simple format like Prometheus text. Track request duration, error counts, and cache hits. Add a lightweight /health route that returns 200 OK when dependencies respond.
header('Content-Type: text/plain');
echo "requests_total 1234\n";
echo "errors_total 12\n";
Using error monitoring services
Tools like Sentry or Bugsnag capture uncaught exceptions and contextual data. Install their SDKs and initialize early in your bootstrap.
Sentry\init(['dsn' => getenv('SENTRY_DSN')]);
try {
// app code
} catch (Throwable $e) {
Sentry\captureException($e);
throw $e;
}
Efficient caching, tuned configuration, reliable containers, and thorough observability yield fast and predictable deployments. Combined with testing and security practices from earlier chapters, these complete the core of a robust PHP production stack.