How to Set Up PHP-FPM with NGINX on FreeBSD
PHP-FPM (FastCGI Process Manager) paired with NGINX is the standard production stack for PHP applications on FreeBSD. NGINX handles static files and proxies PHP requests to FPM worker processes. This architecture is faster and more resource-efficient than Apache with mod_php, and it gives you fine-grained control over process management, resource limits, and security isolation.
This guide covers the full setup: installing PHP and NGINX, configuring FPM pools, connecting NGINX via FastCGI, running multiple PHP versions side by side, tuning OPcache, and hardening the stack for production.
Installing PHP
Choose Your PHP Version
FreeBSD ports provide multiple PHP versions simultaneously. Check what is available:
shpkg search '^php[0-9]+-[0-9]' | head -10
For a new project, use the latest stable version:
shpkg install php84
For legacy applications that need PHP 8.1:
shpkg install php81
Install Common PHP Extensions
shpkg install php84-extensions php84-curl php84-gd php84-mbstring \ php84-xml php84-zip php84-pdo php84-pdo_mysql php84-pdo_pgsql \ php84-opcache php84-intl php84-bcmath php84-json php84-session \ php84-fileinfo php84-tokenizer php84-ctype php84-dom
Verify the Installation
shphp -v php -m
Installing NGINX
shpkg install nginx sysrc nginx_enable="YES"
Configuring PHP-FPM
PHP-FPM configuration on FreeBSD lives in /usr/local/etc/php-fpm.d/. The main configuration file is /usr/local/etc/php-fpm.conf.
Global FPM Settings
Edit /usr/local/etc/php-fpm.conf:
sh; /usr/local/etc/php-fpm.conf [global] pid = /var/run/php-fpm.pid error_log = /var/log/php-fpm.log log_level = warning ; Include pool configurations include=/usr/local/etc/php-fpm.d/*.conf
Default Pool Configuration
Edit /usr/local/etc/php-fpm.d/www.conf:
sh; /usr/local/etc/php-fpm.d/www.conf [www] user = www group = www ; Use a Unix socket (faster than TCP for local connections) listen = /var/run/php-fpm.sock listen.owner = www listen.group = www listen.mode = 0660 ; Process management pm = dynamic pm.max_children = 50 pm.start_servers = 5 pm.min_spare_servers = 2 pm.max_spare_servers = 10 pm.max_requests = 500 ; Status page (for monitoring) pm.status_path = /fpm-status ping.path = /fpm-ping ; Logging access.log = /var/log/php-fpm-access.log access.format = "%R - %u %t \"%m %r\" %s %f %{mili}d %{kilo}M" slowlog = /var/log/php-fpm-slow.log request_slowlog_timeout = 5s ; Security security.limit_extensions = .php
Process Manager Modes
PHP-FPM offers three process management modes:
| Mode | Description | Best For |
|---|---|---|
| static | Fixed number of workers, always running | High-traffic sites with predictable load |
| dynamic | Workers scale between min and max | Most production sites |
| ondemand | Workers spawn only when requests arrive | Low-traffic sites, saves memory |
For most FreeBSD servers, dynamic is the right choice. Adjust pm.max_children based on available RAM:
sh# Rough formula: max_children = Available RAM / Average PHP process size # A typical PHP process uses 30-50MB # Server with 4GB RAM, 2GB for PHP: max_children = 2048 / 40 = ~50
Enable and Start PHP-FPM
shsysrc php_fpm_enable="YES" service php-fpm start
Verify it is running:
shsockstat -l | grep php-fpm
You should see the Unix socket /var/run/php-fpm.sock.
Configuring NGINX
Basic NGINX Configuration
Edit /usr/local/etc/nginx/nginx.conf:
sh# /usr/local/etc/nginx/nginx.conf worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; use kqueue; } http { include mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # Gzip compression gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; gzip_min_length 1000; include /usr/local/etc/nginx/conf.d/*.conf; }
PHP Site Configuration
Create /usr/local/etc/nginx/conf.d/example.conf:
shmkdir -p /usr/local/etc/nginx/conf.d
sh# /usr/local/etc/nginx/conf.d/example.conf server { listen 80; server_name example.com www.example.com; root /usr/local/www/example; index index.php index.html; # Logging access_log /var/log/nginx/example-access.log main; error_log /var/log/nginx/example-error.log warn; # Static files location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2?|ttf|svg)$ { expires 30d; add_header Cache-Control "public, immutable"; } # PHP handling location ~ \.php$ { try_files $uri =404; fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_buffer_size 128k; fastcgi_buffers 4 256k; fastcgi_busy_buffers_size 256k; fastcgi_read_timeout 300; } # Deny access to hidden files location ~ /\. { deny all; } # Deny access to sensitive files location ~* (\.env|composer\.json|composer\.lock|\.git) { deny all; } }
FastCGI Parameters
Ensure /usr/local/etc/nginx/fastcgi_params exists and contains the standard parameters. FreeBSD's NGINX package includes this file. Add any custom parameters needed:
sh# Append to /usr/local/etc/nginx/fastcgi_params if not present fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
Test and Start NGINX
shnginx -t service nginx start
Verify the Stack
Create a test PHP file:
shecho '<?php phpinfo(); ?>' > /usr/local/www/example/info.php
Access http://your-server/info.php in a browser. You should see the PHP info page showing FPM/FastCGI as the Server API.
Remove the test file when done:
shrm /usr/local/www/example/info.php
Multiple PHP Versions
FreeBSD makes it easy to run multiple PHP versions simultaneously, each with its own FPM pool.
Install Both Versions
shpkg install php84 php81
Create Separate FPM Pools
For PHP 8.4 (default), use the existing /usr/local/etc/php-fpm.d/www.conf with socket /var/run/php-fpm.sock.
For PHP 8.1, create /usr/local/etc/php81-fpm.d/legacy.conf:
sh; /usr/local/etc/php81-fpm.d/legacy.conf [legacy] user = www group = www listen = /var/run/php81-fpm.sock listen.owner = www listen.group = www listen.mode = 0660 pm = dynamic pm.max_children = 20 pm.start_servers = 3 pm.min_spare_servers = 2 pm.max_spare_servers = 5 pm.max_requests = 500 security.limit_extensions = .php
Enable both FPM services:
shsysrc php_fpm_enable="YES" sysrc php81_fpm_enable="YES" service php-fpm start service php81-fpm start
Route Different Sites to Different PHP Versions
sh# Modern site using PHP 8.4 server { server_name modern.example.com; root /usr/local/www/modern; location ~ \.php$ { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } # Legacy site using PHP 8.1 server { server_name legacy.example.com; root /usr/local/www/legacy; location ~ \.php$ { fastcgi_pass unix:/var/run/php81-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }
OPcache Configuration
OPcache stores precompiled PHP bytecode in shared memory, eliminating the need to parse and compile PHP scripts on every request. This is critical for production performance.
Enable and Configure OPcache
Edit /usr/local/etc/php/ext-30-opcache.ini or create /usr/local/etc/php/opcache.ini:
sh; /usr/local/etc/php/opcache.ini [opcache] opcache.enable=1 opcache.enable_cli=0 opcache.memory_consumption=256 opcache.interned_strings_buffer=16 opcache.max_accelerated_files=20000 opcache.revalidate_freq=0 opcache.validate_timestamps=0 opcache.save_comments=1 opcache.fast_shutdown=1 ; JIT (PHP 8.0+) opcache.jit=1255 opcache.jit_buffer_size=128M
Key settings:
| Setting | Production Value | Explanation |
|---|---|---|
| validate_timestamps | 0 | Never check if files changed (deploy clears cache) |
| revalidate_freq | 0 | Irrelevant when validate_timestamps=0 |
| memory_consumption | 256 | MB of shared memory for cached scripts |
| max_accelerated_files | 20000 | Max number of cached scripts |
| jit | 1255 | Enable JIT with tracing mode |
Clearing OPcache After Deployment
Since validate_timestamps=0, OPcache will not detect file changes. Clear it after deploying new code:
sh# Option 1: Restart PHP-FPM service php-fpm restart # Option 2: Use cachetool pkg install cachetool cachetool opcache:reset --fcgi=/var/run/php-fpm.sock
Monitoring OPcache
Add an OPcache status script at /usr/local/www/example/opcache-status.php (remove after checking):
sh<?php $status = opcache_get_status(false); echo "Memory used: " . round($status['memory_usage']['used_memory'] / 1048576, 2) . " MB\n"; echo "Memory free: " . round($status['memory_usage']['free_memory'] / 1048576, 2) . " MB\n"; echo "Cached scripts: " . $status['opcache_statistics']['num_cached_scripts'] . "\n"; echo "Hit rate: " . round($status['opcache_statistics']['opcache_hit_rate'], 2) . "%\n";
Security Hardening
PHP Configuration
Edit /usr/local/etc/php.ini:
sh; /usr/local/etc/php.ini -- security settings expose_php = Off display_errors = Off log_errors = On error_log = /var/log/php-errors.log ; Disable dangerous functions disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source ; File upload limits upload_max_filesize = 10M post_max_size = 12M max_file_uploads = 5 ; Session security session.cookie_httponly = 1 session.cookie_secure = 1 session.use_strict_mode = 1 session.cookie_samesite = "Strict" ; Open basedir restriction open_basedir = /usr/local/www:/tmp:/var/tmp
FPM Pool Isolation
For multi-tenant setups, create separate pools per site with different Unix users:
sh; /usr/local/etc/php-fpm.d/site-a.conf [site-a] user = site_a group = site_a listen = /var/run/php-fpm-site-a.sock listen.owner = www listen.group = www php_admin_value[open_basedir] = /usr/local/www/site-a:/tmp php_admin_value[upload_tmp_dir] = /usr/local/www/site-a/tmp php_admin_flag[log_errors] = on php_admin_value[error_log] = /var/log/php-fpm-site-a-errors.log
Create the user:
shpw useradd -n site_a -d /usr/local/www/site-a -s /usr/sbin/nologin chown -R site_a:site_a /usr/local/www/site-a
NGINX Security Headers
Add to your server block:
shadd_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; add_header Content-Security-Policy "default-src 'self'" always;
Rate Limiting
In the http block of nginx.conf:
shlimit_req_zone $binary_remote_addr zone=php:10m rate=10r/s;
In the PHP location block:
shlocation ~ \.php$ { limit_req zone=php burst=20 nodelay; # ... fastcgi config ... }
Framework-Specific NGINX Configurations
Laravel
shserver { listen 80; server_name laravel.example.com; root /usr/local/www/laravel/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.(?!well-known) { deny all; } }
WordPress
shserver { listen 80; server_name wp.example.com; root /usr/local/www/wordpress; index index.php; location / { try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { try_files $uri =404; fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires max; log_not_found off; } # Block xmlrpc.php (common attack vector) location = /xmlrpc.php { deny all; } }
Monitoring and Troubleshooting
Check FPM Status
Access the status page (if configured):
shcurl http://localhost/fpm-status curl http://localhost/fpm-ping
Check Logs
shtail -f /var/log/php-fpm.log tail -f /var/log/php-fpm-slow.log tail -f /var/log/nginx/error.log
Common Issues
502 Bad Gateway: PHP-FPM is not running or the socket path is wrong. Check:
shservice php-fpm status ls -la /var/run/php-fpm.sock
504 Gateway Timeout: PHP script is taking too long. Increase fastcgi_read_timeout in NGINX and request_terminate_timeout in the FPM pool.
Permission denied on socket: Ensure NGINX and PHP-FPM agree on socket ownership:
sh# FPM pool listen.owner = www listen.group = www # NGINX runs as user www
FAQ
Should I use Unix sockets or TCP for FPM?
Use Unix sockets for local connections. They are faster (no TCP overhead) and more secure (filesystem permissions). Use TCP only when NGINX and PHP-FPM run on different machines.
How many FPM workers should I run?
Start with pm.max_children equal to your available RAM divided by 40MB. Monitor actual memory usage with ps aux | grep php-fpm and adjust. Too many workers cause swapping; too few cause request queuing.
Can I use PHP-FPM without NGINX?
PHP-FPM is a FastCGI server -- it needs a web server in front of it. You could use Apache, Caddy, or lighttpd instead of NGINX, but NGINX is the standard choice on FreeBSD.
How do I update PHP on FreeBSD?
Run pkg upgrade to update to the latest patch version. For major version upgrades (e.g., 8.3 to 8.4), install the new version, migrate your configuration, test, then remove the old version.
Does PHP-FPM support HTTP/2?
PHP-FPM speaks FastCGI, not HTTP. HTTP/2 is handled by NGINX. Configure listen 443 ssl http2; in your NGINX server block, and NGINX communicates with FPM via FastCGI regardless of the client protocol.