nd upgrade existing packages
sudo apt update && sudo apt upgrade -y
Install PHP 8.5 FPM, CLI, and categorized extensions
sudo apt install -y
php8.5-fpm
php8.5-cli
php8.5-common
php8.5-opcache
php8.5-mysql
php8.5-pgsql
php8.5-redis
php8.5-curl
php8.5-gd
php8.5-xml
php8.5-mbstring
php8.5-zip
php8.5-bcmath
**Rationale:**
* **`php8.5-opcache`:** Included by default. Opcache is critical for performance, caching precompiled script bytecode in shared memory.
* **`php8.5-redis`:** Added for modern caching and session handling patterns.
* **Grouped Installation:** Installing all extensions in a single transaction ensures dependency resolution is atomic and reduces repository fetch overhead.
#### 2. PHP-FPM Pool Configuration
PHP-FPM manages worker processes. We configure the pool to use dynamic scaling, which adjusts the number of workers based on current load. This balances memory usage with responsiveness.
Edit the pool configuration file located at `/etc/php/8.5/fpm/pool.d/www.conf`.
```ini
; Process Manager Settings
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
; Resource Limits
request_terminate_timeout = 30s
memory_limit = 256M
; Security and Environment
security.limit_extensions = .php .php8
env[PATH] = /usr/local/bin:/usr/bin:/bin
Rationale:
pm = dynamic: Starts with a baseline and scales up/down. Ideal for variable traffic.
pm.max_children: Calculated based on available RAM. Formula: (Total RAM - OS/Other Services) / Avg PHP Process Size. For 8GB RAM, 50 children is a safe starting point.
pm.max_requests: Recycles workers after 500 requests to mitigate memory leaks common in long-running PHP processes.
security.limit_extensions: Restricts execution to specific file extensions, preventing arbitrary file execution vulnerabilities.
3. Nginx Integration and Security Hardening
Nginx acts as the reverse proxy, passing PHP requests to PHP-FPM via a Unix domain socket. We configure a virtual host with security headers and strict path handling.
Create the configuration file at /etc/nginx/sites-available/webapp.internal.conf.
server {
listen 80;
server_name webapp.internal;
root /srv/webapp/public;
index index.php;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Client Limits
client_max_body_size 10M;
# Default Location
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP Processing
location ~ \.php$ {
# Prevent execution in non-public directories
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
# Timeout settings
fastcgi_read_timeout 60s;
fastcgi_send_timeout 60s;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
Rationale:
- Unix Socket:
fastcgi_pass unix:/run/php/php8.5-fpm.sock eliminates TCP overhead.
try_files: Ensures the PHP file exists before passing to FPM, preventing 404s from being interpreted as valid scripts.
- Security Headers: Mitigates clickjacking, MIME sniffing, and XSS attacks.
realpath_root: Resolves symbolic links securely, preventing path traversal issues.
Enable the site and reload services:
sudo ln -s /etc/nginx/sites-available/webapp.internal.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl restart php8.5-fpm
4. Verification and Health Check
Avoid using phpinfo() in production due to information disclosure risks. Instead, create a lightweight health check endpoint that validates the runtime without exposing sensitive configuration.
Create /srv/webapp/public/health.php:
<?php
header('Content-Type: application/json');
$health = [
'status' => 'healthy',
'php_version' => PHP_VERSION,
'fpm_enabled' => (PHP_SAPI === 'fpm-fcgi'),
'extensions' => [
'mysql' => extension_loaded('mysqli'),
'redis' => extension_loaded('redis'),
'opcache' => function_exists('opcache_get_status'),
],
'timestamp' => time()
];
echo json_encode($health, JSON_PRETTY_PRINT);
Verify the deployment:
curl -s http://webapp.internal/health.php | jq
Expected output confirms PHP 8.5 is running via FPM with required extensions loaded.
Pitfall Guide
-
TCP Loopback vs. Unix Socket
- Mistake: Configuring
fastcgi_pass 127.0.0.1:9000.
- Impact: Increased latency due to network stack processing and potential port exhaustion under high concurrency.
- Fix: Always use
unix:/run/php/php8.5-fpm.sock for local deployments.
-
Leaving phpinfo() Accessible
- Mistake: Deploying
info.php with <?php phpinfo(); ?> and forgetting to remove it.
- Impact: Exposes server paths, environment variables, and configuration details to attackers.
- Fix: Use restricted health checks or remove diagnostic scripts immediately after verification.
-
Static Process Manager on Variable Load
- Mistake: Using
pm = static with a high pm.max_children.
- Impact: Wasted memory during idle periods or OOM kills during spikes if the limit is too low.
- Fix: Use
pm = dynamic and tune pm.start_servers and pm.max_children based on traffic patterns.
-
Missing try_files in PHP Location
- Mistake: Omitting
try_files $uri =404; inside the location ~ \.php$ block.
- Impact: Nginx may pass non-existent files to PHP-FPM, which can lead to arbitrary code execution or 404s being served as 200 OK.
- Fix: Always include
try_files to validate file existence before FastCGI handoff.
-
Insufficient pm.max_requests
- Mistake: Leaving
pm.max_requests at default or setting it too high.
- Impact: Memory leaks in PHP extensions or user code accumulate over time, eventually crashing the worker.
- Fix: Set
pm.max_requests to a moderate value (e.g., 500) to force periodic worker recycling.
-
Opcache Misconfiguration
- Mistake: Installing Opcache but leaving it disabled or misconfigured.
- Impact: PHP re-parses scripts on every request, causing significant CPU overhead and latency.
- Fix: Ensure
opcache.enable=1 and configure opcache.validate_timestamps=0 in production for maximum performance.
-
Permission Mismatches
- Mistake: Web root owned by
root or a user other than www-data.
- Impact: PHP-FPM cannot read application files, resulting in 403 Forbidden errors.
- Fix: Set ownership to
www-data:www-data and ensure directory permissions are 755 and file permissions are 644.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High Traffic Web App | Dynamic PM + Unix Socket | Scales workers efficiently; minimizes latency. | Higher RAM usage during peaks. |
| Low Traffic / Dev | OnDemand PM | Workers spawn only when needed; saves resources. | Slight latency spike on first request. |
| Memory Constrained | Static PM with low limit | Predictable memory footprint; prevents OOM. | May reject requests under load. |
| Shared Hosting | OnDemand + Strict Limits | Isolates resource usage per pool. | Requires careful tuning per user. |
Configuration Template
PHP-FPM Pool Snippet (/etc/php/8.5/fpm/pool.d/www.conf):
[webapp]
user = www-data
group = www-data
listen = /run/php/php8.5-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
request_terminate_timeout = 30s
slowlog = /var/log/php8.5-fpm.log.slow
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 10M
php_admin_value[post_max_size] = 10M
Nginx Server Block Snippet:
server {
listen 80;
server_name webapp.internal;
root /srv/webapp/public;
index index.php;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\. {
deny all;
}
}
Quick Start Guide
- Install Stack: Run
sudo apt install -y php8.5-fpm php8.5-cli php8.5-mysql php8.5-curl php8.5-xml php8.5-mbstring php8.5-zip nginx.
- Enable Services: Execute
sudo systemctl enable --now php8.5-fpm nginx.
- Configure Nginx: Create
/etc/nginx/sites-available/webapp.internal.conf with the provided template, link to sites-enabled, and run sudo nginx -t.
- Verify: Run
curl -s http://localhost/health.php to confirm PHP 8.5 is operational via FPM.
- Deploy: Place your application code in
/srv/webapp/public and ensure ownership is set to www-data.