PHP-FPM #
PHP remains the programming language dominating the global web ecosystem, powering the most popular content management systems like WordPress, to modern high-performance frameworks like Laravel and Symfony. Unlike Apache web servers that can run PHP as an internal module (mod_php), modern web servers like Caddy communicate with PHP using the external FastCGI protocol through PHP-FPM (FastCGI Process Manager). Caddy radically simplifies this integration through the php_fastcgi directive, which automatically handles URL routing, FastCGI parameter compilation, and static file detection in the background — tasks that on Nginx servers usually require a dozen complicated, error-prone configuration lines. We’ll discuss in depth the FastCGI communication mechanism, compare UNIX Socket vs TCP Socket connection efficiency, compose robust Caddyfile configurations for WordPress and Laravel, strengthen security against sensitive file leakage, do PHP-FPM pool parameter optimization (tuning), and configure distributed session storage using Redis.
How PHP Works in Caddy #
In Caddy’s PHP request handling model, the process is strictly separated based on the file type requested by the user’s browser.
Caddy acts as the fastest web server for serving static assets (like CSS files, JavaScript, PNG/JPEG images, and fonts). When a static asset request arrives, Caddy directly reads the file from local disk storage and returns it to the user without involving the PHP interpreter.
However, when a request for a .php extension file arrives (or dynamic routes without a physical file on disk), Caddy converts that HTTP request into FastCGI data packets, then sends them to the PHP-FPM socket. There, the PHP-FPM pool worker processes execute the PHP script, interact with the MySQL/PostgreSQL database, compile the HTML response, then return the result to Caddy to be sent back to the user’s browser.
flowchart TD
Browser["HTTP Request Browser"] --> Caddy["Caddy Server"]
Caddy -->|"Is it a physical static file?"| Disk["Read directly from Disk"]
Caddy -->|".php file type / Dynamic route?"| FastCGI["Send FastCGI packets (via unix//run/php-fpm.sock)"]
subgraph PHP["PHP Backend"]
FastCGI --> PHPFPM["PHP-FPM (PHP code execution)"]
end
Disk --> Response["HTML Response"]
PHPFPM --> Response
style Caddy stroke:#0288d1,stroke-width:2px
style PHP stroke:#37474f,stroke-width:1px,stroke-dasharray:5,5This task separation guarantees efficient server CPU resource usage because the PHP runtime is only triggered to handle the dynamic logic that truly needs it.
UNIX Socket vs TCP Connection #
Caddy and PHP-FPM can communicate through one of two socket types: UNIX Domain Socket or TCP Socket. Understanding the performance differences and how to configure both is crucial when composing your web infrastructure.
1. UNIX Domain Socket (Recommended for Standalone Servers) #
UNIX sockets represent the connection as a special socket file on the Linux filesystem (e.g., /run/php/php8.3-fpm.sock).
- Advantages: Very fast because communication happens entirely inside the Linux kernel without going through the network protocol stack (network stack bypass). Data writing is directly copied in RAM memory.
- Disadvantages: Can only be used if Caddy and PHP-FPM run on the same physical machine or VM. Often faces socket file permission problems.
2. TCP Socket (Mandatory for Multi-Server / Separate) #
TCP sockets represent the connection as a network IP address and port (e.g., 127.0.0.1:9000).
- Advantages: Allows Caddy and PHP-FPM to run on separate machines (e.g., Caddy in front acting as a gateway, and PHP-FPM on a dedicated application backend server).
- Disadvantages: Has additional network latency overhead because every piece of data must be packed into local TCP/IP packets, even when running on the same machine.
Solving the “Permission Denied” Problem on UNIX Sockets #
The classic problem often encountered when first integrating Caddy with the PHP-FPM UNIX socket is the 502 Bad Gateway error in the browser, and the dial unix /run/php/php8.3-fpm.sock: connect: permission denied error log in Caddy.
This happens because the socket file is created by PHP-FPM with limited access rights only readable by the default Apache/Nginx user (www-data), while the Caddy process runs under its own user (caddy).
To solve it, you must change the socket owner configuration in the PHP-FPM pool configuration file (usually at /etc/php/8.3/fpm/pool.d/www.conf):
; /etc/php/8.3/fpm/pool.d/www.conf
; ANTI-PATTERN: Leaving the default www-data owner when using Caddy
; listen.owner = www-data
; listen.group = www-data
; CORRECT: Set the socket owner to match the user running the Caddy process
listen.owner = caddy
listen.group = caddy
listen.mode = 0660
After changing that configuration, restart the PHP-FPM service:
sudo systemctl restart php8.3-fpm
Basic PHP Configuration #
After ensuring the socket access rights are safe, you can write a basic Caddyfile to run PHP. The php_fastcgi directive in Caddy automatically does smart tricks: automatically detecting the index.php index file, blocking access to hidden files, and forwarding dynamic requests:
# Basic PHP-FPM configuration in Caddy
example.com {
# Set the root directory where your PHP code is stored
root * /var/www/html
# Forward PHP requests to the PHP-FPM UNIX socket
php_fastcgi unix//run/php/php8.3-fpm.sock
# Enable direct static file serving
file_server
}
WordPress Hardening & Permalinks #
WordPress is the world’s most popular CMS, but it’s often a hacker attack target because of the many security holes in third-party themes or plugins. You must strengthen (harden) the Caddyfile configuration to block access to sensitive WordPress files, and configure search-engine-friendly URLs (pretty permalinks):
# Secure WordPress configuration in Caddy
wordpress.example.com {
root * /var/www/wordpress
encode zstd gzip
# 1. Security Hardening: Block direct access to sensitive files
@blocked {
path /wp-admin/includes/*
path /wp-includes/theme-compat/*
path /wp-includes/js/tinymce/langs/*
path *.sql
path */.git/*
path */.env*
path /xmlrpc.php # Block XML-RPC brute force attacks
path /wp-config.php # Protect database credentials
}
respond @blocked "Access Denied!" 403
# 2. WordPress Pretty Permalinks & Core Routing.
# php_fastcgi automatically redirects non-physical URL requests to index.php
php_fastcgi unix//run/php/php8.3-fpm.sock
# 3. Serve WordPress static assets
file_server
# Fallback rewrite for handling WordPress virtual routes
try_files {path} {path}/ /index.php?{query}
}
Laravel Optimization #
Laravel is a modern PHP framework using the Front Controller Pattern where all requests flow through a single /public/index.php file. You must ensure the root directory points to the public/ folder, not your Laravel project’s root folder, to prevent sensitive .env files from being exposed to the public:
# Optimal production configuration for Laravel
laravel.example.com {
# IMPORTANT: Point the root to Laravel's public folder
root * /var/www/laravel/public
encode zstd gzip
# Insert standard security headers
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
-Server
-X-Powered-By
}
# Stream requests to PHP-FPM
php_fastcgi unix//run/php/php8.3-fpm.sock
# Enable static file reading
file_server
# Laravel routing fallback: Forward all virtual routes to index.php
try_files {path} {path}/ /index.php?{query}
}
Sensitive PHP File Security #
Besides relying on folder separation in Laravel, sometimes you deploy legacy PHP applications (legacy PHP) that store configuration files in the same folder as public files. You must apply layered blocking in the Caddyfile to prevent users from directly downloading those sensitive files:
# Sensitive data protection tactics in the Caddyfile
example.com {
root * /var/www/html
# Block access to dangerous file extensions
@sensitive {
path /.env*
path /config.php
path /composer.json
path /composer.lock
path /package.json
path *.sql
path *.log
path *.bak
}
respond @sensitive 403
# ANTI-PATTERN: Script Execution in Upload Folders:
# Hackers often successfully upload PHP backdoor files (e.g., shell.php)
# into the image upload folder, then access them from the browser to take over the server.
# SOLUTION: Block PHP execution in the uploads and media subdirectories.
@phpInUpload path /uploads/*.php /media/*.php
respond @phpInUpload "Access Denied: Forbidden to Execute Scripts in the Upload Folder!" 403
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
}
Large File Uploads #
By default, both Caddy and PHP-FPM limit request body sizes to prevent DDoS attacks based on giant file uploads that can exhaust RAM memory. If your application needs large file upload features (e.g., a file manager application or uploading videos up to 100MB), you must change the configuration on both sides:
1. Caddyfile Configuration (Web Server Side) #
# Caddy request size limit configuration
uploads.example.com {
root * /var/www/uploads-app
php_fastcgi unix//run/php/php8.3-fpm.sock {
# Increase the response reading timeout so the connection isn't dropped during slow uploads
read_timeout 300s
}
# Limit the maximum request body size to 100 Megabytes
request_body {
max_size 100mb
}
file_server
}
2. php.ini Configuration (PHP-FPM Runtime Side) #
You must also raise the memory and upload size limits in the /etc/php/8.3/fpm/php.ini file:
; /etc/php/8.3/fpm/php.ini
upload_max_filesize = 100M
post_max_size = 105M ; Must be slightly larger than upload_max_filesize
max_execution_time = 300 ; Script execution time limit (5 minutes)
max_input_time = 300 ; Time limit for reading upload data input
memory_limit = 256M ; RAM usage limit per worker
PHP-FPM Pool Tuning Based on RAM Capacity #
Determining the right number of worker processes in PHP-FPM is very important for maintaining server performance. If the worker count is too low, user requests queue for a long time and trigger timeout errors. However, if the worker count is too high, the server runs out of RAM and completely crashes.
You can set dynamic process management parameters in /etc/php/8.3/fpm/pool.d/www.conf:
; /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 50 ; The maximum number of active worker processes allowed
pm.start_servers = 5 ; The number of workers created immediately when FPM starts
pm.min_spare_servers = 5 ; The minimum number of idle workers that must always be ready
pm.max_spare_servers = 35 ; The maximum number of idle workers allowed to stand by
pm.max_requests = 500 ; Kill & recreate workers after processing 500 requests (anti-leak)
The Formula for Calculating pm.max_children Precisely #
Don’t guess the pm.max_children value. You can calculate it scientifically based on your server’s remaining RAM capacity.
Use the following shell command to monitor the average RAM memory used by one PHP-FPM worker process on your server:
ps -eo pid,pmem,rss,comm | grep php-fpm | awk '{print $3}' | \ awk 'BEGIN{s=0}{s+=$1}END{print "Average RAM usage per worker: " s/NR/1024 " MB"}'For example, the average consumption per worker is 40 MB.
Use the following formula to calculate the maximum child process limit: [\text{pm.max_children} = \frac{\text{Total Server RAM} - \text{RAM for System & Caddy}}{\text{Average RAM per PHP-FPM Worker}}] If your server has 4 GB (4096 MB) of RAM, and you set aside 1 GB (1024 MB) for the OS, database, and Caddy: [\text{pm.max_children} = \frac{4096 - 1024}{40} = 76 \text{ workers}]
PHP with Redis Sessions (Distributed Sessions) #
If you do horizontal scaling by deploying a PHP application on several virtual servers behind one Caddy load balancer, a tricky problem arises if user session data is stored as local files on each server. Visitors routed to Server A at login suddenly get logged out when the next request goes to Server B because Server B doesn’t have that visitor’s session file.
The solution is storing all PHP sessions in a centralized Redis cache server accessed together by all PHP-FPM nodes:
; Centralized session storage configuration at /etc/php/8.3/fpm/conf.d/redis-session.ini
session.save_handler = redis
session.save_path = "tcp://redis-server.local:6379?auth=our-secret-redis-password"
session.gc_maxlifetime = 7200 # Session validity: 2 hours
With the configuration above, all your PHP-FPM nodes are stateless (without local session data load), so you can easily add or remove PHP server counts dynamically anytime according to web traffic fluctuations.
PHP-FPM Request Processing Pipeline Diagram by Caddy #
For a clear visualization of how Caddy evaluates routes and communicates with the PHP-FPM subsystem, look at the following flowchart:
flowchart TD
A["Request Arrives from Browser\n(e.g., GET /profile/settings)"] --> B["1. Caddy evaluates named matchers & rules"]
B --> C{"2. Does the route match a\nstatic file existing on disk?"}
C -- "Yes" --> D["3. Caddy directly serves the file\n(Without triggering PHP-FPM)"]
D --> E["Done"]
C -- "No" --> F["4. Trigger the php_fastcgi directive"]
F --> G["5. Do an internal rewrite to index.php\n(Laravel/WordPress front controller pattern)"]
G --> H["6. Package the request into FastCGI data\n(Define SCRIPT_FILENAME & parameters)"]
H --> I["7. Send the packets via the UNIX socket\n(unix//run/php/php8.3-fpm.sock)"]
I --> J["8. PHP-FPM Pool Manager\n(Receives the data & assigns a worker process)"]
J --> K["9. Execute the PHP runtime code\n(Database access, template rendering, etc.)"]
K --> L["10. Return the response output to the Caddy socket"]
L --> M["11. Caddy compresses the response (Gzip/Zstd)\nand sends it to the Client browser"]
M --> ESummary #
- Configuration Ease: The
php_fastcgidirective in the Caddyfile simplifies PHP configuration by natively automating route rewrites, index handling, and FastCGI mapping.- UNIX Socket Performance: Use UNIX sockets (
unix//run/php/php8.3-fpm.sock) instead of the127.0.0.1:9000TCP port for faster local memory I/O performance.- Access Permission Solution: Set the socket owner properties
listen.owner = caddyandlisten.group = caddyin the PHP-FPM pool configuration to avoid permission denied errors.- File Protection: Always block direct access to sensitive files (
.env,.git,wp-config.php,composer.json) using named matchers and therespond 403action in the Caddyfile.- Upload Directory Hardening: Prevent web shell backdoor execution by blocking
.phpfile execution in the uploads and media subdirectories.- Worker RAM Calculation: Determine the maximum
pm.max_childrenlimit based on the average memory consumption of one PHP-FPM worker process to prevent RAM exhaustion.- Horizontal Scaling: Use Redis as the centralized PHP session storage repository so your PHP-FPM server cluster can be stateless.