FreeBSD.software
Home/Guides/How to Set Up PHP-FPM with NGINX on FreeBSD
tutorial·2026-04-09·9 min read

How to Set Up PHP-FPM with NGINX on FreeBSD

Complete guide to setting up PHP-FPM with NGINX on FreeBSD: PHP installation, FPM pool configuration, NGINX FastCGI setup, multiple PHP versions, OPcache tuning, and security hardening.

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:

sh
pkg search '^php[0-9]+-[0-9]' | head -10

For a new project, use the latest stable version:

sh
pkg install php84

For legacy applications that need PHP 8.1:

sh
pkg install php81

Install Common PHP Extensions

sh
pkg 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

sh
php -v php -m

Installing NGINX

sh
pkg 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

sh
sysrc php_fpm_enable="YES" service php-fpm start

Verify it is running:

sh
sockstat -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:

sh
mkdir -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

sh
nginx -t service nginx start

Verify the Stack

Create a test PHP file:

sh
echo '<?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:

sh
rm /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

sh
pkg 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:

sh
sysrc 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:

sh
pw 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:

sh
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; add_header Content-Security-Policy "default-src 'self'" always;

Rate Limiting

In the http block of nginx.conf:

sh
limit_req_zone $binary_remote_addr zone=php:10m rate=10r/s;

In the PHP location block:

sh
location ~ \.php$ { limit_req zone=php burst=20 nodelay; # ... fastcgi config ... }

Framework-Specific NGINX Configurations

Laravel

sh
server { 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

sh
server { 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):

sh
curl http://localhost/fpm-status curl http://localhost/fpm-ping

Check Logs

sh
tail -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:

sh
service 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.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.