FreeBSD.software
Home/Guides/FreeBSD Web Server Stack: NGINX + PHP + PostgreSQL Setup Guide (2026)
guide·2026-04-09·15 min read

FreeBSD Web Server Stack: NGINX + PHP + PostgreSQL Setup Guide (2026)

Complete guide to building a production web server stack on FreeBSD: NGINX with TLS, PHP-FPM, PostgreSQL, Redis caching, Let's Encrypt SSL, and performance tuning. Step-by-step with real configs.

FreeBSD Web Server Stack: NGINX + PHP + PostgreSQL Setup Guide (2026)

This guide walks through building a complete, production-grade web server stack on FreeBSD. By the end, you will have NGINX serving TLS-terminated traffic, PHP-FPM processing dynamic requests, PostgreSQL storing your data, and Redis accelerating everything with caching. Every path, command, and config file is FreeBSD-specific.

Tested on FreeBSD 14.2-RELEASE. All software installed from the official pkg repository.

Table of Contents

  1. Architecture Overview
  2. Installing the Stack
  3. NGINX Configuration
  4. PHP-FPM Setup
  5. PostgreSQL Setup
  6. Redis Caching
  7. Let's Encrypt SSL
  8. Performance Tuning
  9. Security
  10. WordPress Deployment Example
  11. Monitoring the Stack
  12. FAQ

Architecture Overview

The stack follows a standard reverse-proxy pattern:

shell
Client (HTTPS:443) | v NGINX (TLS termination, static files, gzip) | v fastcgi_pass 127.0.0.1:9000 PHP-FPM (dynamic content, opcache) | +---> PostgreSQL (persistent data, unix socket) +---> Redis (session store, object cache, tcp 6379)

Request flow: NGINX accepts the incoming HTTPS connection, terminates TLS, and serves static assets directly from disk. For .php requests, NGINX forwards the request to PHP-FPM via FastCGI. PHP-FPM processes the script, querying PostgreSQL for data and Redis for cached objects or sessions, then returns the response to NGINX which delivers it to the client.

Why this stack on FreeBSD:

  • ZFS integration for PostgreSQL datasets with tunable recordsize and snapshots.
  • jails for optional service isolation without VM overhead.
  • sysrc and rc.conf provide clean, declarative service management.
  • Rock-solid network stack with low-latency kqueue event handling in NGINX.

For detailed NGINX setup beyond this guide, see our NGINX FreeBSD production setup. For PostgreSQL deep-dives, see PostgreSQL on FreeBSD.


Installing the Stack

Update the package repository and install everything in one pass:

sh
pkg update && pkg upgrade -y pkg install -y nginx php83 php83-extensions php83-curl php83-gd \ php83-mbstring php83-zip php83-xml php83-pgsql php83-pdo_pgsql \ php83-pecl-redis php83-opcache php83-intl php83-bcmath \ postgresql17-server postgresql17-client redis

Package references: nginx, php83, postgresql17-server, redis.

Enable all services at boot:

sh
sysrc nginx_enable=YES sysrc php_fpm_enable=YES sysrc postgresql_enable=YES sysrc redis_enable=YES

Verify the installations:

sh
nginx -v php -v psql --version redis-server --version

NGINX Configuration

All NGINX configuration on FreeBSD lives under /usr/local/etc/nginx/. The main config file is /usr/local/etc/nginx/nginx.conf.

Main Configuration

sh
vi /usr/local/etc/nginx/nginx.conf
nginx
worker_processes auto; worker_rlimit_nofile 65535; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 4096; use kqueue; multi_accept on; } http { include mime.types; default_type application/octet-stream; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; client_max_body_size 64m; # Gzip compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 5; gzip_min_length 256; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; # Logging 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; # Include server blocks include /usr/local/etc/nginx/conf.d/*.conf; }

Server Block with TLS and FastCGI

Create the conf.d directory and your site config:

sh
mkdir -p /usr/local/etc/nginx/conf.d vi /usr/local/etc/nginx/conf.d/example.com.conf
nginx
server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name example.com www.example.com; root /usr/local/www/example.com; index index.php index.html; # TLS (Let's Encrypt paths, configured later) ssl_certificate /usr/local/etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /usr/local/etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Static files location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ { expires 30d; add_header Cache-Control "public, immutable"; access_log off; } # PHP via FastCGI location ~ \.php$ { try_files $uri =404; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_read_timeout 300; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; } location / { try_files $uri $uri/ /index.php?$args; } # Deny access to hidden files location ~ /\. { deny all; access_log off; log_not_found off; } }

Create the document root and test the config:

sh
mkdir -p /usr/local/www/example.com chown -R www:www /usr/local/www/example.com nginx -t service nginx start

For the full production NGINX walkthrough including rate limiting and caching, see NGINX FreeBSD production setup.


PHP-FPM Setup

PHP-FPM configuration on FreeBSD lives in /usr/local/etc/php-fpm.d/. The default pool file is www.conf.

Pool Configuration

sh
cp /usr/local/etc/php.ini-production /usr/local/etc/php.ini vi /usr/local/etc/php-fpm.d/www.conf

Key settings to adjust:

ini
[www] user = www group = www listen = 127.0.0.1:9000 listen.allowed_clients = 127.0.0.1 ; Process manager - 'dynamic' for most workloads pm = dynamic pm.max_children = 50 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.max_requests = 500 ; Status page (restrict access in NGINX) pm.status_path = /fpm-status ping.path = /fpm-ping ; Logging php_admin_value[error_log] = /var/log/php-fpm/www-error.log php_admin_flag[log_errors] = on slowlog = /var/log/php-fpm/www-slow.log request_slowlog_timeout = 5s ; Security php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen php_admin_value[open_basedir] = /usr/local/www/:/tmp/:/var/tmp/

Tuning pm.max_children

Calculate based on available RAM:

sh
# Check average PHP-FPM process memory usage ps -auxw | grep php-fpm | awk '{sum += $6; n++} END {print sum/n/1024 " MB average"}'

Formula: pm.max_children = (Available RAM - OS/DB/Redis overhead) / Average PHP process size

For a 4 GB server: roughly (4096 - 1500) / 50 = ~50 children.

OPcache Configuration

Edit /usr/local/etc/php.ini and add or modify:

ini
[opcache] opcache.enable=1 opcache.memory_consumption=256 opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 opcache.revalidate_freq=60 opcache.validate_timestamps=1 opcache.save_comments=1 opcache.fast_shutdown=1 opcache.jit=1255 opcache.jit_buffer_size=128M

Create the log directory and start PHP-FPM:

sh
mkdir -p /var/log/php-fpm chown www:www /var/log/php-fpm service php-fpm start

Verify PHP-FPM is listening:

sh
sockstat -l | grep 9000

PostgreSQL Setup

Initialize the Database on ZFS

If you are running ZFS (recommended for PostgreSQL on FreeBSD), create a dedicated dataset:

sh
zfs create -o mountpoint=/var/db/postgres zpool/postgres zfs set recordsize=16k zpool/postgres zfs set primarycache=metadata zpool/postgres zfs set atime=off zpool/postgres zfs set compression=lz4 zpool/postgres zfs set logbias=throughput zpool/postgres chown postgres:postgres /var/db/postgres

The recordsize=16k matches PostgreSQL's default 8 KB page size with some buffer, and primarycache=metadata avoids double-caching since PostgreSQL manages its own buffer pool via shared_buffers.

Initialize the database cluster:

sh
sysrc postgresql_data="/var/db/postgres/data17" service postgresql initdb

Authentication (pg_hba.conf)

Edit /var/db/postgres/data17/pg_hba.conf:

shell
# TYPE DATABASE USER ADDRESS METHOD local all postgres peer local all all scram-sha-256 host all all 127.0.0.1/32 scram-sha-256 host all all ::1/128 scram-sha-256

Performance Tuning (postgresql.conf)

Edit /var/db/postgres/data17/postgresql.conf. Settings below are for a server with 8 GB RAM:

shell
# Connection settings listen_addresses = 'localhost' port = 5432 max_connections = 100 # Memory shared_buffers = 2GB effective_cache_size = 6GB work_mem = 32MB maintenance_work_mem = 512MB wal_buffers = 64MB # Write-ahead log wal_level = replica max_wal_size = 2GB min_wal_size = 512MB checkpoint_completion_target = 0.9 # Query planner random_page_cost = 1.1 # SSD storage effective_io_concurrency = 200 # SSD storage default_statistics_target = 200 # Logging log_destination = 'stderr' logging_collector = on log_directory = 'log' log_min_duration_statement = 250 log_checkpoints = on log_connections = on log_disconnections = on log_lock_waits = on

Start PostgreSQL and create a database user:

sh
service postgresql start su - postgres -c "psql -c \"CREATE USER webapp WITH PASSWORD 'strong_password_here';\"" su - postgres -c "psql -c \"CREATE DATABASE appdb OWNER webapp;\"" su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE appdb TO webapp;\""

Test the connection:

sh
psql -h 127.0.0.1 -U webapp -d appdb -c "SELECT version();"

For advanced PostgreSQL configuration and backup strategies, see PostgreSQL on FreeBSD.


Redis Caching

Installation and Configuration

Redis configuration on FreeBSD is at /usr/local/etc/redis.conf.

sh
vi /usr/local/etc/redis.conf

Key settings:

shell
bind 127.0.0.1 port 6379 protected-mode yes requirepass your_redis_password_here # Memory management maxmemory 512mb maxmemory-policy allkeys-lru # Persistence (optional, disable for pure cache) save "" # Or keep RDB snapshots: # save 900 1 # save 300 10 # Performance tcp-backlog 511 timeout 300 tcp-keepalive 300 # Logging loglevel notice logfile /var/log/redis/redis.log
sh
mkdir -p /var/log/redis chown redis:redis /var/log/redis service redis start

Test the connection:

sh
redis-cli -a your_redis_password_here ping # Should return: PONG

PHP Session Handler via Redis

Edit /usr/local/etc/php.ini:

ini
session.save_handler = redis session.save_path = "tcp://127.0.0.1:6379?auth=your_redis_password_here&database=0"

Object Cache Configuration

For application-level caching, configure your PHP application to use Redis. Example connection in PHP:

php
$redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->auth('your_redis_password_here'); $redis->select(1); // Use database 1 for object cache // Set a cached value with 300 second TTL $redis->setex('user:42:profile', 300, json_encode($userData)); // Retrieve $cached = $redis->get('user:42:profile'); if ($cached !== false) { $userData = json_decode($cached, true); }

Restart PHP-FPM after changing session settings:

sh
service php-fpm restart

For the full Redis setup including Sentinel and replication, see Redis on FreeBSD.


Let's Encrypt SSL

Install Certbot

sh
pkg install -y py311-certbot py311-certbot-nginx

Obtain a Certificate

Stop NGINX temporarily for the initial certificate (or use the NGINX plugin for zero-downtime):

sh
# Option 1: Standalone (requires stopping NGINX) service nginx stop certbot certonly --standalone -d example.com -d www.example.com service nginx start # Option 2: NGINX plugin (no downtime, preferred) certbot --nginx -d example.com -d www.example.com

Certificates are stored at /usr/local/etc/letsencrypt/live/example.com/.

Auto-Renewal

Add a cron job for automatic renewal:

sh
crontab -e
shell
# Renew Let's Encrypt certificates twice daily 0 3,15 * * * /usr/local/bin/certbot renew --quiet --deploy-hook "service nginx reload"

Alternatively, use a periodic script:

sh
vi /usr/local/etc/periodic/daily/500.certbot-renew
sh
#!/bin/sh /usr/local/bin/certbot renew --quiet --deploy-hook "service nginx reload"
sh
chmod +x /usr/local/etc/periodic/daily/500.certbot-renew

Test the renewal process:

sh
certbot renew --dry-run

For the complete Let's Encrypt walkthrough, see Let's Encrypt on FreeBSD.


Performance Tuning

System-level (sysctl.conf)

Add to /etc/sysctl.conf:

sh
# Network stack kern.ipc.somaxconn=4096 kern.ipc.shmmax=2147483648 kern.ipc.shmall=524288 net.inet.tcp.msl=5000 net.inet.tcp.recvspace=65536 net.inet.tcp.sendspace=65536 net.inet.tcp.nolocaltimewait=1 net.inet.tcp.fast_finwait2_recycle=1 net.inet.tcp.cc.algorithm=cubic # File descriptors kern.maxfiles=131072 kern.maxfilesperproc=104856 # Shared memory for PostgreSQL kern.ipc.shmmax=2147483648

Apply immediately:

sh
sysctl -f /etc/sysctl.conf

Also raise file descriptor limits in /etc/login.conf:

shell
daemon:\ :openfiles-cur=65536:\ :openfiles-max=65536:\ :tc=default:

Rebuild the login database:

sh
cap_mkdb /etc/login.conf

NGINX Tuning Summary

| Setting | Default | Recommended | Notes |

|---|---|---|---|

| worker_processes | 1 | auto | Matches CPU cores |

| worker_connections | 1024 | 4096 | Per worker |

| keepalive_timeout | 75 | 65 | Reduce for high-traffic |

| client_max_body_size | 1m | 64m | For file uploads |

| gzip_comp_level | 1 | 5 | Balance CPU vs compression |

PHP OPcache Tuning Summary

| Setting | Value | Why |

|---|---|---|

| opcache.memory_consumption | 256 | Enough for large apps |

| opcache.max_accelerated_files | 10000 | Covers most CMS installs |

| opcache.revalidate_freq | 60 | Check for changes every 60s |

| opcache.jit | 1255 | Full JIT for PHP 8.3 |

PostgreSQL Tuning Quick Reference

For an 8 GB RAM server:

| Parameter | Value | Rule of Thumb |

|---|---|---|

| shared_buffers | 2GB | 25% of total RAM |

| effective_cache_size | 6GB | 75% of total RAM |

| work_mem | 32MB | RAM / max_connections / 4 |

| maintenance_work_mem | 512MB | 5-10% of RAM |

| random_page_cost | 1.1 | Use 1.1 for SSD |

| effective_io_concurrency | 200 | Use 200 for SSD |


Security

File Permissions

sh
# Web root owned by www, not writable by PHP chown -R www:www /usr/local/www/example.com find /usr/local/www/example.com -type d -exec chmod 755 {} \; find /usr/local/www/example.com -type f -exec chmod 644 {} \; # Writable upload directory (only where needed) mkdir -p /usr/local/www/example.com/uploads chown www:www /usr/local/www/example.com/uploads chmod 750 /usr/local/www/example.com/uploads

NGINX Rate Limiting

Add to the http block in nginx.conf:

nginx
# Rate limiting zones limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; limit_conn_zone $binary_remote_addr zone=connlimit:10m;

Apply in your server block:

nginx
location / { limit_req zone=general burst=20 nodelay; limit_conn connlimit 50; try_files $uri $uri/ /index.php?$args; } location ~ ^/(wp-login\.php|login|admin) { limit_req zone=login burst=3 nodelay; # ... fastcgi_pass config ... }

PHP Security Settings

In /usr/local/etc/php.ini:

ini
expose_php = Off display_errors = Off display_startup_errors = Off log_errors = On error_log = /var/log/php-fpm/php_errors.log allow_url_fopen = Off allow_url_include = Off max_input_time = 60 max_execution_time = 30 memory_limit = 256M post_max_size = 64M upload_max_filesize = 64M session.cookie_httponly = 1 session.cookie_secure = 1 session.use_strict_mode = 1

PostgreSQL Security

Ensure PostgreSQL only listens on localhost (already set in postgresql.conf):

shell
listen_addresses = 'localhost'

Use scram-sha-256 authentication exclusively (already set in pg_hba.conf above). Avoid trust or md5.

Firewall (IPFW or PF)

Basic PF configuration in /etc/pf.conf:

shell
ext_if = "vtnet0" set block-policy drop set skip on lo0 # Scrub incoming scrub in all # Default deny block all # Allow SSH, HTTP, HTTPS pass in on $ext_if proto tcp to port { 22, 80, 443 } keep state pass out all keep state

Enable and start PF:

sh
sysrc pf_enable=YES service pf start

For comprehensive server hardening, see Hardening a FreeBSD server.


WordPress Deployment Example

This section deploys WordPress on the stack we just built. The same general approach works for Nextcloud (see Nextcloud on FreeBSD) or any PHP application.

Create the Database

sh
su - postgres -c "psql -c \"CREATE USER wordpress WITH PASSWORD 'wp_db_password_here';\"" su - postgres -c "psql -c \"CREATE DATABASE wordpress OWNER wordpress;\""

Download and Install WordPress

sh
cd /usr/local/www fetch https://wordpress.org/latest.tar.gz tar xzf latest.tar.gz mv wordpress example.com chown -R www:www example.com rm latest.tar.gz

Configure WordPress for PostgreSQL

WordPress requires a PostgreSQL database driver plugin since it natively uses MySQL. Install the PG4WP plugin:

sh
cd /usr/local/www/example.com/wp-content mkdir -p mu-plugins fetch -o pg4wp.zip https://github.com/PostgreSQL-For-Wordpress/postgresql-for-wordpress/archive/refs/heads/master.zip unzip pg4wp.zip mv postgresql-for-wordpress-master/pg4wp pg4wp cp pg4wp/db.php ../ rm -rf postgresql-for-wordpress-master pg4wp.zip chown -R www:www /usr/local/www/example.com

Alternatively, use MySQL/MariaDB if you prefer a standard WordPress setup. Install with pkg install -y mariadb1011-server and adjust the configuration accordingly.

WordPress wp-config.php

sh
cd /usr/local/www/example.com cp wp-config-sample.php wp-config.php vi wp-config.php

Set the database credentials:

php
define('DB_NAME', 'wordpress'); define('DB_USER', 'wordpress'); define('DB_PASSWORD', 'wp_db_password_here'); define('DB_HOST', '127.0.0.1'); define('DB_CHARSET', 'utf8mb4'); // Redis object cache define('WP_REDIS_HOST', '127.0.0.1'); define('WP_REDIS_PORT', 6379); define('WP_REDIS_PASSWORD', 'your_redis_password_here'); define('WP_REDIS_DATABASE', 2); // Security define('DISALLOW_FILE_EDIT', true); define('WP_AUTO_UPDATE_CORE', true);

Generate unique salts:

sh
curl -s https://api.wordpress.org/secret-key/1.1/salt/

Paste the output into wp-config.php, replacing the placeholder salt values.

NGINX Server Block for WordPress

Update your /usr/local/etc/nginx/conf.d/example.com.conf location block:

nginx
location / { try_files $uri $uri/ /index.php?$args; } # Deny access to sensitive WordPress files location ~ /\.(htaccess|htpasswd) { deny all; } location = /wp-config.php { deny all; } location ~* /wp-content/uploads/.*\.php$ { deny all; } location ~* /(?:xmlrpc\.php)$ { deny all; }

Reload NGINX:

sh
nginx -t && service nginx reload

Visit https://example.com to complete the WordPress installation wizard.

Install Redis Object Cache Plugin

After WordPress is running, install the Redis Object Cache plugin via WP-CLI or the admin panel. This plugin uses the WP_REDIS_* constants from wp-config.php and drastically reduces database queries on subsequent page loads.

sh
pkg install -y php83-pecl-redis service php-fpm restart

Monitoring the Stack

Checking Service Status

sh
# All services at once service nginx status service php-fpm status service postgresql status service redis status

Quick Health Check Script

Save as /usr/local/sbin/stack-check.sh:

sh
#!/bin/sh echo "=== Stack Health Check ===" # NGINX if service nginx status > /dev/null 2>&1; then echo "[OK] NGINX is running" else echo "[FAIL] NGINX is down" fi # PHP-FPM if service php-fpm status > /dev/null 2>&1; then echo "[OK] PHP-FPM is running" echo " Active processes: $(sockstat -l | grep -c ':9000')" else echo "[FAIL] PHP-FPM is down" fi # PostgreSQL if service postgresql status > /dev/null 2>&1; then echo "[OK] PostgreSQL is running" echo " Active connections: $(su - postgres -c 'psql -t -c "SELECT count(*) FROM pg_stat_activity;"' 2>/dev/null)" else echo "[FAIL] PostgreSQL is down" fi # Redis if redis-cli ping > /dev/null 2>&1; then echo "[OK] Redis is running" echo " Memory used: $(redis-cli info memory | grep used_memory_human | cut -d: -f2)" else echo "[FAIL] Redis is down" fi echo "=== Disk Usage ===" df -h / /var/db/postgres 2>/dev/null echo "=== Load Average ===" uptime
sh
chmod +x /usr/local/sbin/stack-check.sh

Log Locations

| Service | Log Path |

|---|---|

| NGINX access | /var/log/nginx/access.log |

| NGINX error | /var/log/nginx/error.log |

| PHP-FPM error | /var/log/php-fpm/www-error.log |

| PHP-FPM slow | /var/log/php-fpm/www-slow.log |

| PostgreSQL | /var/db/postgres/data17/log/ |

| Redis | /var/log/redis/redis.log |

| System | /var/log/messages |

Log Rotation

FreeBSD uses newsyslog for log rotation. Add entries to /etc/newsyslog.conf:

shell
/var/log/nginx/access.log www:www 644 7 * @T00 JB /var/run/nginx.pid 30 /var/log/nginx/error.log www:www 644 7 * @T00 JB /var/run/nginx.pid 30 /var/log/php-fpm/www-error.log www:www 644 7 * @T00 JB /var/log/redis/redis.log redis:redis 644 7 * @T00 JB

FAQ

How much RAM does this stack need?

A minimum of 2 GB for a light-traffic site. For production with PostgreSQL tuning, 4-8 GB is the practical baseline. The biggest consumers are shared_buffers (PostgreSQL) and pm.max_children (PHP-FPM). On a 2 GB server, set shared_buffers=512MB and pm.max_children=15.

Should I use Unix sockets or TCP for PHP-FPM?

Unix sockets are slightly faster since they avoid TCP overhead. To switch, change NGINX's fastcgi_pass to unix:/var/run/php-fpm.sock and set listen = /var/run/php-fpm.sock in the PHP-FPM pool config along with listen.owner = www and listen.group = www. TCP (127.0.0.1:9000) is used in this guide for clarity and because it simplifies debugging with sockstat.

How do I update packages without breaking the stack?

Use pkg upgrade which respects ABI compatibility. Before upgrading, take a ZFS snapshot:

sh
zfs snapshot zpool/ROOT/default@before-upgrade zfs snapshot zpool/postgres@before-upgrade pkg upgrade -y service nginx restart service php-fpm restart service postgresql restart service redis restart

If something breaks, roll back: zfs rollback zpool/ROOT/default@before-upgrade.

Can I run this stack in a FreeBSD jail?

Yes, and it is recommended for multi-tenant setups. Create a jail per application or per client. Each jail gets its own NGINX, PHP-FPM, and either its own PostgreSQL instance or connects to a shared PostgreSQL on the host. Redis can be shared across jails by binding to the host's loopback interface and using VNET jail networking. See Hardening a FreeBSD server for jail setup patterns.


Summary

You now have a complete, production-ready web stack on FreeBSD:

  • NGINX handles TLS, serves static files, and proxies PHP requests.
  • PHP-FPM processes dynamic content with OPcache and JIT enabled.
  • PostgreSQL stores persistent data on a tuned ZFS dataset.
  • Redis accelerates sessions and object caching.
  • Let's Encrypt provides free, auto-renewing TLS certificates.

All managed through sysrc and rc.conf, with logs in predictable locations and a health check script to verify the stack.

For the individual deep-dives referenced throughout this guide:

Get more FreeBSD guides

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