Hard📖Теория8 min

Production настройка

Рекомендуемый production php.ini, сравнение dev vs prod, PHP-FPM pool configuration, Docker-специфичные настройки

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),
    ];
}

Проверь себя

🧪

Что делает директива `pm.max_requests = 1000` в PHP-FPM?

🧪

Как правильно рассчитать `pm.max_children`?

🧪

Какое значение `listen queue` в статусе PHP-FPM сигнализирует о проблеме?

🧪

Куда должны писаться логи PHP в Docker-контейнере?

🧪

Какой режим `pm` в PHP-FPM лучше всего подходит для сервера со стабильно высокой нагрузкой?