📋 Table of Contents
1. Introduction to PHP-FPM Pools
PHP-FPM (FastCGI Process Manager) pools are a powerful feature that allows you to manage multiple PHP processes independently. Each pool operates as a separate entity with its own configuration, resources, and security boundaries. This architecture is essential for hosting multiple websites on a single server while maintaining isolation and security.
💡 What is PHP-FPM?
PHP-FPM is a process manager that works in conjunction with web servers like Nginx or Apache to handle PHP requests efficiently. It provides advanced features like adaptive process spawning, slow request logging, and graceful stop/start capabilities.
PHP-FPM Architecture Overview
2. Understanding PHP Pools
What Are PHP Pools?
PHP pools allow you to create multiple sets of PHP processes, each dedicated to serving requests for specific sites, applications, or domains. Each pool operates independently with its own configuration settings and resource allocations.
Pool: site1.com
User: site1user
Socket: php8.3-fpm-site1.sock
Memory: 256M
Processes: 5-10
Pool: site2.com
User: site2user
Socket: php8.3-fpm-site2.sock
Memory: 128M
Processes: 3-8
Pool: site3.com
User: site3user
Socket: php8.3-fpm-site3.sock
Memory: 512M
Processes: 10-20
Advantages of PHP Pools
| Feature | Benefit | Example |
|---|---|---|
| Isolation | Each site runs in its own environment | If site1.com is compromised, site2.com remains secure |
| Resource Control | Allocate specific resources per pool | High-traffic site gets more memory and processes |
| Custom Configuration | Different PHP settings per site | Enable specific extensions or memory limits |
| Security | Contain security breaches | Vulnerable plugin affects only one site |
| Performance | Optimize each site independently | Tune process management per workload |
Disadvantages to Consider
⚠️ Potential Challenges
- Resource Consumption: Multiple pools consume more memory than a single shared process
- Complexity: Additional configuration and maintenance required
- Cost: May require more powerful server hardware
- Monitoring: Need to monitor each pool separately
3. Complete Setup Process
This comprehensive guide walks you through setting up PHP-FPM pools for multiple WordPress sites. Follow each step carefully to ensure proper configuration.
Step 1: Create User Account
Create a dedicated user for the site
Each site should have its own user account for isolation and security.
# Create new user (replace 'example' with your domain name)
sudo useradd example
angelscript
Step 2: Configure User Groups
Add users to appropriate groups
Ensure proper permissions between the web server, PHP-FPM, and file owner.
# Add www-data to the site user's group
sudo usermod -a -G example www-data
Add site user to www-data group (required for mail functionality)
sudo usermod -a -G www-data example
Add your non-root user to the site user's group
sudo usermod -a -G example $USER
angelscript
🔍 Understanding the Commands
-a: Append - adds user to group without removing from other groups-G: Specifies the supplementary group to add the user towww-data: The default Nginx web server user
Step 3: Create PHP-FPM Pool
Create and configure the pool file
Copy the default pool configuration and customize it for your site.
# Navigate to pool directory
cd /etc/php/8.3/fpm/pool.d/
Copy default pool configuration
sudo cp www.conf example.com.conf
Edit the new configuration
sudo nano example.com.conf
angelscript
Step 4: Configure Pool Settings
Customize pool configuration
Set the pool name, user, group, and socket path.
; Pool name
[example]
; Unix user/group of processes
user = example
group = example
; Socket path - IMPORTANT: Make this unique for each pool
listen = /run/php/php8.3-fpm-example.com.sock
; Resource limits
rlimit_files = 15000
rlimit_core = 100
; PHP configuration
php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/fpm-php.example.com.log
php_admin_flag[log_errors] = on
; Optional: Set custom memory limit
; php_admin_value[memory_limit] = 256M
; Custom temp directories
php_admin_value[upload_tmp_dir] = /var/www/example.com/tmp/
php_admin_value[sys_temp_dir] = /var/www/example.com/tmp/
; Security: Restrict file access
php_admin_value[open_basedir] = /var/www/example.com/public_html/:/var/www/example.com/tmp/
; Security: Disable dangerous functions
php_admin_value[disable_functions] = shell_exec,exec,passthru,system,proc_open,popen
angelscript
✅ Key Configuration Points
- Pool Name: Should match your domain for easy identification
- Socket Path: Must be unique for each pool
- Resource Limits: Set based on site requirements
- open_basedir: Restricts PHP file access to specific directories
Step 5: Create Log File
Set up PHP error logging
Create a dedicated log file for the pool with proper permissions.
# Create log file
sudo touch /var/log/fpm-php.example.com.log
Set ownership (user:www-data)
sudo chown example:www-data /var/log/fpm-php.example.com.log
Set permissions (read/write for owner and group)
sudo chmod 660 /var/log/fpm-php.example.com.log
angelscript
Step 6: Create Temp Directory
Create temporary upload directory
Essential for file uploads and temporary storage.
# Navigate to site root
cd /var/www/example.com/
Create temp directory
sudo mkdir tmp/
Set ownership
sudo chown example:example tmp/
Set permissions
sudo chmod 770 tmp/
angelscript
Step 7: Reload PHP-FPM
Apply the new configuration
Reload PHP-FPM to enable the new pool.
# Reload PHP-FPM service
sudo systemctl reload php8.3-fpm
Verify pool is running
sudo grep "listen = /" /etc/php/8.3/fpm/pool.d/example.com.conf
angelscript
Step 8: Configure Nginx
Update Nginx server block
Configure Nginx to use the new PHP-FPM pool socket.
# Edit Nginx site configuration
sudo nano /etc/nginx/sites-available/example.com.conf
Find the PHP processing location block and update:
location ~ .php$ {
include snippets/fastcgi-php.conf;
fastcgi_param HTTP_HOST $host;
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
include /etc/nginx/includes/fastcgi_optimize.conf;
}
Test Nginx configuration
sudo nginx -t
Reload Nginx
sudo systemctl reload nginx
angelscript
Step 9: Set File Permissions
Configure proper ownership and permissions
Security is crucial - set appropriate file and directory permissions.
Standard Permissions (Development)
# Set ownership
sudo chown -R example:example /var/www/example.com/public_html/
Directories: 770 (rwxrwx---)
sudo find /var/www/example.com/public_html/ -type d -exec chmod 770 {} ;
Files: 660 (rw-rw----)
sudo find /var/www/example.com/public_html/ -type f -exec chmod 660 {} ;
Secure wp-config.php
sudo chmod 400 /var/www/example.com/public_html/wp-config.php
Hardened Permissions (Production)
# Base directories: 550 (r-xr-x---)
sudo find /var/www/example.com/public_html/ -type d -exec chmod 550 {} ;
Base files: 440 (r--r-----)
sudo find /var/www/example.com/public_html/ -type f -exec chmod 440 {} ;
wp-content writable: 770/660
sudo find /var/www/example.com/public_html/wp-content/ -type d -exec chmod 770 {} ;
sudo find /var/www/example.com/public_html/wp-content/ -type f -exec chmod 660 {} ;
angelscript
⚠️ Permission Guidelines
- 770/660: Development - allows user and group full access
- 550/440: Production - read-only for most files
- wp-content: Always needs write permissions for uploads and updates
- wp-config.php: Should be 400 (read-only for owner)
4. SSL/TLS Configuration
Secure your sites with Let's Encrypt SSL certificates and modern TLS configuration for A+ rating.
Install Certbot
# Update packages
sudo apt update
Install Certbot with Cloudflare DNS plugin
sudo apt install certbot python3-certbot-dns-cloudflare
oxygene
Obtain SSL Certificate
# Using webroot method
sudo certbot certonly --webroot
-w /var/www/example.com/public_html/
-d example.com
-d www.example.com
Generate Diffie-Hellman Parameters
# Create SSL directory
cd /etc/nginx/
sudo mkdir ssl/
cd ssl/
Generate DH parameters (takes several minutes)
sudo openssl dhparam -out dhparam.pem 2048
Create SSL Configuration Files
Site-Specific Certificate Configuration
# Create certificate configuration file
sudo nano /etc/nginx/ssl/ssl_certs_example.com.conf
Add these lines:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
Global SSL Configuration
# Create global SSL settings file
sudo nano /etc/nginx/ssl/ssl_all_sites.conf
Configuration for A+ SSL Labs rating
Updated: May 2025
SSL Session Management
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 180m;
TLS Protocols and Ciphers
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
Strong cipher suite (single line in actual config)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
DH parameters
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
DNS resolver (Cloudflare)
resolver 1.1.1.1 1.0.0.1;
resolver_timeout 15s;
Disable session tickets
ssl_session_tickets off;
HSTS Header
add_header Strict-Transport-Security "max-age=31536000;" always;
HTTP/3 and QUIC
ssl_early_data on;
add_header Alt-Svc 'h3=":$server_port"; ma=86400';
add_header x-quic 'H3';
quic_retry on;
angelscript
📝 Note on SSL Stapling
Let's Encrypt removed support for SSL stapling on May 7, 2025. The ssl_stapling and
ssl_stapling_verify directives should be commented out or removed from your
configuration.
Configure Nginx Server Block for HTTPS
# HTTP to HTTPS redirect
server {
listen 80;
server_name example.com www.example.com;
# 301 permanent redirect to HTTPS
return 301 https://example.com$request_uri;
}
HTTPS server block (first server with reuseport)
server {
listen 443 ssl;
http2 on;
awk
listen 443 quic reuseport;
http3 on;
server_name example.com www.example.com;
root /var/www/example.com/public_html;
index index.php index.html;
# Include SSL configuration
include /etc/nginx/ssl/ssl_certs_example.com.conf;
include /etc/nginx/ssl/ssl_all_sites.conf;
# Rest of your configuration...
}
Additional HTTPS server (without reuseport)
server {
listen 443 ssl;
http2 on;
listen 443 quic;
http3 on;
# Configuration for www subdomain...
}
angelscript
✅ HTTP/3 and QUIC Support
- First server: Use
reuseporton the QUIC listen directive - Additional servers: Omit
reuseportto avoid conflicts - Testing: Use browser DevTools Network tab to verify h3 protocol
- Verification: Check https://http3check.net/
Testing HTTP/3
# Temporarily disable caching to test HTTP/3
Add to location / block:
add_header Cache-Control 'no-cache,no-store';
Test with curl
curl -I https://example.com
Remove the cache-control header after confirming HTTP/3 works
SSL Certificate Renewal
# Manual renewal
sudo certbot renew
Force renewal
sudo certbot renew --force-renewal
List all certificates
sudo certbot certificates
Delete certificate
sudo certbot delete
Automatic Renewal with Cron
# Edit root crontab
sudo crontab -e
Add these lines (renew on 14th and 28th at 1 AM)
00 1 14,28 * * certbot renew --force-renewal
00 2 14,28 * * systemctl reload nginx
angelscript
SSL Testing
🏆 Verify Your Configuration
- SSL Labs: Test at https://www.ssllabs.com/ssltest/ (Target: A+ rating)
- HTTP/3: Verify at https://http3check.net/
- Browser DevTools: Check Network tab for "h3" protocol
5. Security Hardening
HTTP Security Headers
# Create security headers file
sudo nano /etc/nginx/includes/http_headers.conf
Add these headers:
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "sameorigin";
add_header X-XSS-Protection "1; mode=block";
add_header Permissions-Policy 'accelerometer=(), camera=(), clipboard-read=(), clipboard-write=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), fullscreen=(self "https://www.youtube.com")';
Include in your server block
include /etc/nginx/includes/http_headers.conf;
WordPress Security Directives
# Create WordPress security file
sudo nano /etc/nginx/includes/nginx_security_directives.conf
Disable favicon logging
location = /favicon.ico {
access_log off;
log_not_found off;
}
Deny access to sensitive files
location = /wp-config.php { deny all; }
location = /wp-admin/install.php { deny all; }
location ~* ^/(readme|license|licence).(txt|html)$ { deny all; }
location ~* .ini$ { deny all; }
Harden WP core
location ~* ^/wp-includes/[^/]+.php$ { deny all; }
location ~* ^/wp-includes/js/tinymce/langs/.+.php$ { deny all; }
location ~* ^/wp-includes/theme-compat/ { deny all; }
Prevent PHP execution in uploads
location ~* ^/wp-content/uploads/..(php[1-8]?|pht|phtml?|phps)$ { deny all; }
location ~ ^/wp-content/plugins/..(php[1-8]?|pht|phtml?|phps)$ { deny all; }
location ~ ^/wp-content/themes/.*.(php[1-8]?|pht|phtml?|phps)$ { deny all; }
Block development files
location ~* (composer.(json|lock)|package.json|yarn.lock|/vendor/|/node_modules/) { deny all; }
Block dangerous HTTP methods
if ($request_method ~* ^(TRACE|DELETE|TRACK)$) { return 403; }
Block vulnerability scanners
if ($http_user_agent ~* (nikto|sqlmap|masscan|nmap|dirbuster|acunetix|openvas)) { return 444; }
angelscript
🔒 Critical Security Measures
- Always deny direct access to wp-config.php
- Prevent PHP execution in uploads directory
- Block access to development and dependency files
- Disable dangerous HTTP methods
- Consider blocking known vulnerability scanners
Allow Specific Plugin PHP Execution
Some plugins require PHP execution in the plugins directory. Here's how to allow it selectively:
# Allow PHP execution for a specific plugin file
location = /wp-content/plugins/specific-plugin/file.php {
allow all;
include snippets/fastcgi-php.conf;
fastcgi_param HTTP_HOST $host;
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
include /etc/nginx/includes/fastcgi_optimize.conf;
}
PHP Function Restrictions
# In your pool configuration file
php_admin_value[disable_functions] = shell_exec,opcache_get_configuration,opcache_get_status,disk_total_space,diskfreespace,dl,exec,passthru,pclose,pcntl_alarm,pcntl_exec,pcntl_fork,proc_close,proc_get_status,proc_nice,proc_open,proc_terminate,show_source,system,popen,posix_kill
Database Security
# Revoke all privileges
REVOKE ALL PRIVILEGES ON site_db.* FROM 'site_user'@'hostname';
Grant only necessary privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON site_db.* TO 'site_user'@'hostname';
For migrations or major updates, temporarily grant:
GRANT CREATE, ALTER, INDEX ON site_db.* TO 'site_user'@'hostname';
Apply changes
FLUSH PRIVILEGES;
WordPress Hardening
# Add to wp-config.php to disable file modifications
define('DISALLOW_FILE_MODS', true);
angelscript
⚠️ Important Notes
- Setting
DISALLOW_FILE_MODSprevents plugin/theme installation via admin panel - You'll need to install plugins/themes via SFTP or command line
- This significantly improves security by preventing unauthorized modifications
6. Rate Limiting
Rate limiting helps protect your sites from brute force attacks and excessive requests.
Configure Rate Limiting Zone
# Edit main Nginx configuration
sudo nano /etc/nginx/nginx.conf
Add in http block:
limit_req_zone $binary_remote_addr zone=wp:10m rate=30r/m;
angelscript
🔍 Understanding the Configuration
$binary_remote_addr: Uses client IP addresszone=wp:10m: Creates 10MB zone named "wp"rate=30r/m: Allows 30 requests per minute
Apply Rate Limiting to Specific Locations
# Create rate limiting configuration
sudo nano /etc/nginx/includes/rate_limiting_example.com.conf
Protect wp-login.php
location = /wp-login.php {
limit_req zone=wp burst=20 nodelay;
limit_req_status 444;
include snippets/fastcgi-php.conf;
fastcgi_param HTTP_HOST $host;
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
include /etc/nginx/includes/fastcgi_optimize.conf;
}
Protect xmlrpc.php
location = /xmlrpc.php {
limit_req zone=wp burst=20 nodelay;
limit_req_status 444;
include snippets/fastcgi-php.conf;
fastcgi_param HTTP_HOST $host;
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
include /etc/nginx/includes/fastcgi_optimize.conf;
}
Include in your server block
include /etc/nginx/includes/rate_limiting_example.com.conf;
angelscript
✅ Rate Limiting Parameters
burst=20: Allows burst of 20 requests before enforcing limitnodelay: Immediately processes requests within burst limitlimit_req_status 444: Returns 444 status (connection closed) when limit exceeded
Browser Caching Headers
# Create browser caching configuration
sudo nano /etc/nginx/includes/browser_caching_security_headers.conf
expires 30d;
etag on;
if_modified_since exact;
add_header Pragma "public";
add_header Cache-Control "public, no-transform";
try_files $uri $uri/ /index.php?$args;
include /etc/nginx/includes/http_headers.conf;
access_log off;
7. Troubleshooting & Maintenance
Checking Logs
# View Nginx error logs
cd /var/log/nginx
sudo tail -f error.log
View Nginx access logs
sudo tail -f access.log
View specific site access log
sudo tail -f example.com.access.log
View PHP-FPM pool error log
sudo tail -f /var/log/fpm-php.example.com.log
Less command for browsing logs
sudo less /var/log/nginx/error.log
Testing Configuration
# Test Nginx configuration syntax
sudo nginx -t
Reload Nginx after changes
sudo systemctl reload nginx
Restart Nginx (if reload doesn't work)
sudo systemctl restart nginx
Reload PHP-FPM
sudo systemctl reload php8.3-fpm
Restart PHP-FPM
sudo systemctl restart php8.3-fpm
Check Nginx status
sudo systemctl status nginx
Check PHP-FPM status
sudo systemctl status php8.3-fpm
xml
Common Issues and Solutions
| Issue | Possible Cause | Solution |
|---|---|---|
| 502 Bad Gateway | PHP-FPM not running or wrong socket | Check PHP-FPM status, verify socket path |
| 403 Forbidden | Permission issues or security rules | Check file permissions and security directives |
| File upload fails | Missing tmp directory or permissions | Create tmp directory with correct ownership |
| Unable to send email | User not in www-data group | Add user to www-data group |
| Changes not visible | Configuration not reloaded | Reload or restart Nginx and PHP-FPM |
Testing Connectivity
# Test HTTP connection
curl -I http://example.com
Test HTTPS connection
curl -I https://example.com
Test with verbose output
curl -v https://example.com
Test specific host header
curl -H "Host: example.com" http://SERVER_IP/
Verifying Pool Configuration
# Check pool is loaded
sudo grep "listen = /" /etc/php/8.3/fpm/pool.d/example.com.conf
List all running PHP-FPM processes
ps aux | grep php-fpm
Check PHP-FPM pools
sudo php-fpm8.3 -tt
View pool status (if enabled in pool config)
Add to pool config: pm.status_path = /status
Then access: https://example.com/status
Performance Monitoring
# Monitor server resources
htop
Check disk usage
df -h
Check memory usage
free -h
Monitor network connections
netstat -tulpn
Check open files
lsof | grep nginx
lsof | grep php-fpm
angelscript
💡 Pro Tips
- Always test configuration with
nginx -tbefore reloading - Keep backups of working configurations
- Monitor logs regularly for errors and suspicious activity
- Use version control (Git) for configuration files
- Document any custom configurations or modifications
Maintenance Checklist
✅ Regular Maintenance Tasks
- Daily: Monitor error logs for issues
- Weekly: Check disk space and memory usage
- Monthly: Review and rotate logs
- Quarterly: Update security headers and SSL configuration
- Bi-annually: Review and update PHP disabled functions list
- Annually: Audit all user accounts and permissions
Emergency Recovery
# If Nginx fails to start, check configuration
sudo nginx -t
View detailed error messages
sudo journalctl -xe -u nginx
Restore from backup if needed
sudo cp /etc/nginx/sites-available/example.com.conf.backup
/etc/nginx/sites-available/example.com.conf
Restart services
sudo systemctl restart nginx
sudo systemctl restart php8.3-fpm
8. Summary and Best Practices
Key Takeaways
Isolation
Each site operates independently with its own user, group, and resources
Security
Multiple layers of protection including permissions, headers, and rate limiting
Performance
Optimized caching, HTTP/3, and resource allocation per site
Best Practices Summary
🏆 Production Recommendations
- Always use separate PHP pools for each site or client
- Set hardened permissions (550/440) in production environments
- Enable HTTP/3 and QUIC for optimal performance
- Implement rate limiting on login and API endpoints
- Use strong SSL configuration targeting A+ SSL Labs rating
- Disable unnecessary PHP functions to reduce attack surface
- Monitor logs regularly and set up automated alerts
- Keep software updated (Nginx, PHP, WordPress, plugins)
- Automate SSL renewal with cron jobs
- Document all customizations and maintain backups
Configuration File Structure
/etc/nginx/
├── nginx.conf (main config)
├── sites-available/
│ └── example.com.conf (server block)
├── sites-enabled/
│ └── example.com.conf -> ../sites-available/example.com.conf
├── ssl/
│ ├── dhparam.pem
│ ├── ssl_all_sites.conf (global SSL config)
│ └── ssl_certs_example.com.conf (per-site certificates)
└── includes/
├── http_headers.conf
├── nginx_security_directives.conf
├── rate_limiting_example.com.conf
├── browser_caching_security_headers.conf
└── fastcgi_optimize.conf
/etc/php/8.3/fpm/
├── php-fpm.conf (main PHP-FPM config)
└── pool.d/
├── www.conf (default pool)
└── example.com.conf (site-specific pool)
/var/www/example.com/
├── public_html/ (web root)
├── tmp/ (temporary files)
└── logs/ (optional site-specific logs)
dts
Quick Reference Commands
| Task | Command |
|---|---|
| Test Nginx | sudo nginx -t |
| Reload Nginx | sudo systemctl reload nginx |
| Reload PHP-FPM | sudo systemctl reload php8.3-fpm |
| View Nginx logs | sudo tail -f /var/log/nginx/error.log |
| View PHP logs | sudo tail -f /var/log/fpm-php.example.com.log |
| Renew SSL | sudo certbot renew |
| Test SSL | curl -I https://example.com |
| Set permissions | sudo find . -type d -exec chmod 770 {} \; |