Production-настройка PHP
Development vs Production: полное сравнение
Главное различие -- баланс между удобством отладки (development) и безопасностью/производительностью (production).
Отображение ошибок
| Директива |
Development |
Production |
Почему |
display_errors |
On |
Off |
Не показывать ошибки пользователям |
display_startup_errors |
On |
Off |
Скрыть ошибки при старте |
error_reporting |
E_ALL |
E_ALL |
Логировать всё, но не показывать |
log_errors |
On |
On |
Всегда писать в лог |
log_errors_max_len |
1024 |
4096 |
Полные stack trace в production |
Производительность
| Директива |
Development |
Production |
Почему |
opcache.enable |
0 |
1 |
Кеширование байткода |
opcache.validate_timestamps |
1 |
0 |
Не проверять файлы |
opcache.revalidate_freq |
0 |
0 |
Не актуально при validate=0 |
opcache.jit |
disable |
tracing |
JIT-компиляция |
opcache.jit_buffer_size |
0 |
128M |
Память для JIT |
realpath_cache_size |
4096K |
4096K |
Кеш путей |
realpath_cache_ttl |
120 |
600 |
Дольше кешировать |
Безопасность
| Директива |
Development |
Production |
Почему |
expose_php |
On |
Off |
Скрыть версию PHP |
allow_url_include |
Off |
Off |
Всегда выключено |
open_basedir |
(пусто) |
/var/www/app:/tmp |
Ограничить файловый доступ |
disable_functions |
(пусто) |
exec,shell_exec,... |
Отключить опасные функции |
session.cookie_secure |
0 |
1 |
Только HTTPS |
session.cookie_httponly |
1 |
1 |
Защита от XSS |
zend.exception_ignore_args |
Off |
On |
Скрыть аргументы в trace |
Отладка
| Директива |
Development |
Production |
Почему |
xdebug.mode |
debug,coverage |
off |
Xdebug замедляет в 2-3 раза |
mysqlnd.collect_memory_statistics |
On |
Off |
Статистика замедляет |
mysqlnd.collect_statistics |
On |
Off |
Отключить сбор |
PHP-FPM Pool Configuration
PHP-FPM (FastCGI Process Manager) -- основной способ запуска PHP для веб-приложений. Конфигурация pool определяет, сколько процессов обрабатывают запросы.
Менеджер процессов (pm)
; /etc/php/8.4/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
; Listen on socket (faster) or TCP
listen = /run/php/php-fpm.sock
; listen = 127.0.0.1:9000 ; For Docker/reverse proxy
; Process manager modes:
; static -- fixed number of children (predictable memory)
; dynamic -- children scale between min and max
; ondemand -- children created only on demand (saves memory)
pm = dynamic
Режим static
; Fixed number of workers -- predictable, no overhead on scaling
pm = static
pm.max_children = 50
; Best for: dedicated servers with stable traffic
; Memory usage: always pm.max_children * memory_per_process
; Example: 50 * 256M = 12.8 GB (constant)
Режим dynamic (рекомендуется)
pm = dynamic
; Maximum workers at peak load
pm.max_children = 50
; Workers started initially
pm.start_servers = 10
; Minimum idle workers (ready to accept requests)
pm.min_spare_servers = 5
; Maximum idle workers (excess will be killed)
pm.max_spare_servers = 20
; Requests before worker restart (prevents memory leaks)
pm.max_requests = 1000
; Idle timeout for children above min_spare_servers
pm.process_idle_timeout = 10s
Режим ondemand
; Workers created only when needed -- saves memory
pm = ondemand
pm.max_children = 50
pm.process_idle_timeout = 10s
pm.max_requests = 1000
; Best for: low-traffic sites, shared hosting
; Drawback: first request after idle has latency (fork overhead)
Расчёт pm.max_children
<?php
// Formula:
// pm.max_children = Available_RAM / Average_memory_per_process
// Step 1: Find average memory per process
// Run under load, then check:
// ps aux | grep php-fpm | awk '{sum += $6; n++} END {print sum/n/1024 " MB"}'
// Step 2: Calculate available RAM
// Total RAM - OS - Database - Redis - Nginx - buffer
// Example calculation:
$totalRam = 16 * 1024; // 16 GB in MB
$osReserved = 2 * 1024; // 2 GB for OS
$dbReserved = 4 * 1024; // 4 GB for PostgreSQL
$redisReserved = 1 * 1024; // 1 GB for Redis
$buffer = 1 * 1024; // 1 GB safety buffer
$availableRam = $totalRam - $osReserved - $dbReserved - $redisReserved - $buffer;
// 16384 - 2048 - 4096 - 1024 - 1024 = 8192 MB
$avgProcessMemory = 128; // 128 MB per FPM worker (measured!)
$maxChildren = (int) floor($availableRam / $avgProcessMemory);
// 8192 / 128 = 64 workers
echo "Recommended pm.max_children = $maxChildren\n";
Мониторинг FPM
; Enable FPM status page
pm.status_path = /fpm-status
pm.status_listen = 127.0.0.1:9001
; Enable slow log
slowlog = /var/log/php/fpm-slow.log
request_slowlog_timeout = 5s
; Terminate requests exceeding timeout
request_terminate_timeout = 60s
# Check FPM status (via curl or nginx)
curl http://127.0.0.1:9001/fpm-status?full
# Key metrics:
# active processes -- currently handling requests
# idle processes -- waiting for requests
# listen queue -- requests waiting for free worker (should be 0!)
# max listen queue -- historical maximum (if >0, increase max_children)
Docker-специфичная конфигурация
Пользовательский php.ini в Docker
FROM php:8.4-fpm-alpine
# Copy production template as base
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
# Override with custom settings via conf.d
COPY docker/php/app.ini /usr/local/etc/php/conf.d/app.ini
# Copy FPM pool configuration
COPY docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf
# Set proper permissions
RUN chown -R www-data:www-data /var/www
USER www-data
Конфигурация через переменные окружения
; docker/php/app.ini
; Values can be overridden via environment in entrypoint
[PHP]
memory_limit = 256M
max_execution_time = 30
upload_max_filesize = 50M
post_max_size = 55M
display_errors = Off
log_errors = On
error_log = /dev/stderr
[opcache]
opcache.enable = 1
opcache.validate_timestamps = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 30000
opcache.jit = tracing
opcache.jit_buffer_size = 128M
[Session]
session.save_handler = redis
session.save_path = "tcp://redis:6379"
Docker entrypoint с переменными
#!/bin/sh
# docker/php/entrypoint.sh
# Override php.ini values from environment variables
if [ -n "$PHP_MEMORY_LIMIT" ]; then
echo "memory_limit = $PHP_MEMORY_LIMIT" > /usr/local/etc/php/conf.d/zz-env.ini
fi
if [ -n "$PHP_OPCACHE_VALIDATE" ]; then
echo "opcache.validate_timestamps = $PHP_OPCACHE_VALIDATE" >> /usr/local/etc/php/conf.d/zz-env.ini
fi
if [ -n "$PHP_SESSION_HANDLER" ]; then
echo "session.save_handler = $PHP_SESSION_HANDLER" >> /usr/local/etc/php/conf.d/zz-env.ini
echo "session.save_path = \"$PHP_SESSION_PATH\"" >> /usr/local/etc/php/conf.d/zz-env.ini
fi
exec "$@"
Docker Compose для PHP-FPM
# docker-compose.yml
services:
php:
build:
context: .
dockerfile: docker/php/Dockerfile
environment:
PHP_MEMORY_LIMIT: "256M"
PHP_OPCACHE_VALIDATE: "0"
PHP_SESSION_HANDLER: "redis"
PHP_SESSION_PATH: "tcp://redis:6379"
volumes:
- ./src:/var/www/app/src:cached
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "php-fpm-healthcheck || exit 1"]
interval: 10s
timeout: 5s
retries: 3
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
Docker: логирование
; In Docker, send logs to stdout/stderr (container logging)
; NOT to files inside container!
error_log = /dev/stderr
; Or via php-fpm.conf:
; access.log = /dev/stdout
; NEVER log to files inside container:
; - Logs are lost when container is replaced
; - Disk fills up without rotation
; - Cannot use docker logs / kubectl logs
Правило Docker: Все логи -- в stdout/stderr. Собираются Docker-демоном, ротируются автоматически, доступны через docker logs.
FPM pool для разных окружений
Development
[www]
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 5
pm.max_requests = 0
; Disable request timeout for debugging
request_terminate_timeout = 0
; Verbose logging
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /dev/stderr
Production
[www]
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
pm.process_idle_timeout = 10s
; Terminate slow requests
request_terminate_timeout = 60s
request_slowlog_timeout = 5s
slowlog = /dev/stderr
; Security
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /dev/stderr
php_admin_value[open_basedir] = /var/www/app:/tmp
Staging (имитация production)
[www]
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 10
pm.max_requests = 500
request_terminate_timeout = 60s
request_slowlog_timeout = 3s
slowlog = /dev/stderr
; Same security as production
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
Полный production php.ini
; ============================================
; PHP 8.4 Production Configuration
; ============================================
[PHP]
; Engine
engine = On
short_open_tag = Off
precision = 14
serialize_precision = -1
; Output
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
; Security
expose_php = Off
disable_functions = exec,shell_exec,system,passthru,proc_open,popen,dl,putenv,phpinfo,show_source
disable_classes =
allow_url_fopen = On
allow_url_include = Off
open_basedir = /var/www/app:/tmp
; Error handling
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On
log_errors_max_len = 4096
error_log = /dev/stderr
zend.exception_ignore_args = On
zend.exception_string_param_max_len = 0
; Resource limits
memory_limit = 256M
max_execution_time = 30
max_input_time = 60
max_input_vars = 1000
; File uploads
file_uploads = On
upload_max_filesize = 50M
post_max_size = 55M
max_file_uploads = 20
upload_tmp_dir = /tmp
; Date
date.timezone = UTC
; Realpath cache
realpath_cache_size = 4096K
realpath_cache_ttl = 600
[opcache]
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 30000
opcache.validate_timestamps = 0
opcache.save_comments = 1
opcache.huge_code_pages = 1
opcache.jit = tracing
opcache.jit_buffer_size = 128M
opcache.preload = /var/www/app/config/preload.php
opcache.preload_user = www-data
[Session]
session.save_handler = redis
session.save_path = "tcp://redis:6379"
session.name = __sess
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.use_strict_mode = 1
session.use_only_cookies = 1
session.use_trans_sid = 0
session.gc_maxlifetime = 1440
session.gc_probability = 0
session.sid_length = 48
session.sid_bits_per_character = 6
session.serialize_handler = php_serialize
[mysqlnd]
mysqlnd.collect_statistics = Off
mysqlnd.collect_memory_statistics = Off
Чеклист перед деплоем
<?php
function productionReadinessCheck(): array
{
$checks = [];
// Performance
$checks['opcache_enabled'] = (bool) ini_get('opcache.enable');
$checks['opcache_no_revalidate'] = !ini_get('opcache.validate_timestamps');
$checks['jit_enabled'] = ini_get('opcache.jit') !== 'disable'
&& ini_get('opcache.jit') !== '0';
// Security
$checks['errors_hidden'] = !ini_get('display_errors');
$checks['php_hidden'] = !ini_get('expose_php');
$checks['url_include_off'] = !ini_get('allow_url_include');
$checks['session_secure'] = (bool) ini_get('session.cookie_secure');
$checks['session_httponly'] = (bool) ini_get('session.cookie_httponly');
$checks['strict_mode'] = (bool) ini_get('session.use_strict_mode');
// Logging
$checks['logging_enabled'] = (bool) ini_get('log_errors');
$checks['error_log_set'] = ini_get('error_log') !== '';
// Functions
$disabled = ini_get('disable_functions');
$checks['dangerous_disabled'] = str_contains($disabled, 'exec')
&& str_contains($disabled, 'shell_exec');
$failed = array_filter($checks, fn(bool $v) => !$v);
return [
'passed' => count($checks) - count($failed),
'total' => count($checks),
'failed' => array_keys($failed),
];
}