How to use Caddy Server with PHP
Caddy Server is a modular and modern web server platform that supports automatic HTTPS certificates, QUIC and HTTP/2, Zstd and Brotli compression, and various modern features as well as classic web server features such as configurable virtual hosts, URL rewriting and redirects, reverse proxying, and more.
Caddy 2, the current version that was released in 2020 May introduced significant improvements to its configuration syntax, automation, plugins, and more.
This article explains how to integrate PHP with the Caddy web server version 2 series, and advanced configuration. It also compares similar configurations with Apache and Nginx configurations to ease the migration from Apache and Nginx to Caddy.
Initial Server Setup
Caddy is available on many operating systems and Linux-based distros. Caddy documentation explains how to install Caddy, and configure it as a service/daemon that runs automatically with the server start.
Once Caddy is installed, Caddy can be configured with minimal configuration that serves static files if they are present, and passes other requests to PHP-FPM.
example.com
is the domain name of the example the rest of this article uses, and its source files are placed in/var/www/example.com
, with a/var/www/example.com/public/index.php
acting as the entry point of the web application. All assets of the web application are stored in/var/www/example.com/public
directory as well, but the rest of the application (including source files, Composervendor
directory, tests,composer.json
file, NPMnode_modules
directory, etc) is located at/var/www/example.com
.
Caddy Server comes with secure and performant default configuration, which makes it easy to configure with minimal configuration.
When Caddy is installed and configured as a system service, a default /etc/caddy/Caddyfile
can be used as the global configuration file, and a sub-directory with a suggested name /etc/caddy/sites
to contain the configuration files for individual sites, akin to Apache and Nginx configuration.
/etc/caddy
├── Caddyfile
├── config/
│ └── php-fpm.conf
└── sites/
└── example.com.conf
The global Caddyfile
can specify the global configuration, and include config/*
and sites/*
directories to include the additional configuration.
Caddyfile
{
log default {
format console
output file /var/log/caddy/system.log
exclude http.log.access
}
}
import config/*
import sites/*
This configures Caddy to write system logs to /var/log/caddy/system.log
file (but not HTTP request logs), as well as loading additional configuration files from config
and sites
directories.
Run, Start, Stop, and Reload Caddy Server
If Caddy is installed as a systemd
service, systemctl
command can be used to start, stop, reload, and restart Caddy server.
For ad-hoc configuration, the server can controlled with the caddy
command:
systemctl start caddy
caddy start
caddy run # Starts server and blocks indefinitely
systemctl stop caddy
caddy stop
systemctl reload caddy
caddy reload
systemctl restart caddy
caddy stop && caddy start
Integrate Caddy with PHP-FPM
Similar to how Apache web server and Nginx integrate with PHP, Caddy is also integrated with PHP using Caddy's FastCGI reverse proxy.
The basic idea is that when Caddy receives a request that should be processed with PHP (e.g, a request to a file name with a .php
extension), the request is sent to PHP-FPM, where the PHP application is executed, and the response is sent back to Caddy to return to the user.
At its simplest, the following is a fully functional Caddy site definition:
/etc/caddy/sites/example.com.conf
example.com {
root * /var/www/example.com/public
log {
output file /var/log/caddy/example.access.log
format console
}
# Encode responses in zstd or gzip, depending on the
# availability indicated by the browser.
encode zstd gzip
# Configures multiple PHP-related settings
php_fastcgi unix//run/php/php-fpm.sock
# Prevent access to dot-files, except .well-known
@dotFiles {
path */.*
not path /.well-known/*
}
}
The configuration file above is a minimal yes complete configuration file that takes care of several security and performance aspects.
For a full explanation of the complete list of directives as well as the details information on directives used here, refer to the excellent Caddy documentation.
Caddy's Extra-mile for PHP: php_fastcgi
Caddy goes an extra-mile to ease integrating Caddy with PHP. The handy php_fastcgi
directive is a shorthand for multiple configuration options that passes requests to PHP-FPM, tries to load an index.php
from the immediate directory, and finally rewrites all requests to the root index.php
. This pattern is commonly called the "front controller pattern", and can be used for a vast majority of PHP frameworks and CMSs including Laravel, Drupal, WordPress, Slim PHP, etc.
At its simplest form, php_fastcgi
takes one argument for the PHP-FPM server. It can be a server address and a port (such as 127.0.0.1:9000
) or a Unix domain socket address. On Debian/Ubuntu/derivatives and RHEL/Fedora/derivatives, this is almost always available as a Unix socket, often taking the path pattern /run/php/php[VERSION]-fpm.sock
. For example, for PHP 8.2, the socket address would be /run/php/php8.2-fpm.sock
, and PHP 8.3's at /run/php/php8.3-fpm.sock
.
If using a Unix domain socket is not feasible, use the IP address and the port name.
The address of the Unix domain socket or the IP/port that PHP-FPM listens to is configurable from PHP-FPM configuration files.
URL Rewriting to index.php
By default, the php_fastcgi
implies three important URL rewrites:
1. Rewrite requests to the ./index.php
file if exists
If a request arrives at example.com/test
, and if a file exists at test/index.php
, Caddy rewrites this request to the test/index.php
file. This resolves the "trailing slash problem", where a PHP application exists inside a sub-directory of the document root.
2. Rewrite to index.php
if a file does not exist
The second thing configured with the php_fastcgi
is that Caddy tries to serve the request with a file if it exists. For example, if the user requests example.com/image.png
, and if a file named image.png
exists in the document root, Caddy serves it as a file without invoking PHP at all.
Then, if a file does not exist, it tries to look for an index.php
file in the path as a directory, followed by an attempt to rewrite it to the root index.php
.
This step is similar to the following Apache configuration:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
... and the following Nginx configuration:
try_files $uri $uri/ /index.php?$query_string;
3. Pass .php
files to PHP-FPM
Finally, the php_fastcgi
directive routes the .php
files to the specified FPM server address. Caddy knows to properly set the FPM parameters, split the path, and perform several other "handover tasks" with well thought-out defaults.
The
php_fastcgi
directive is a shorthand for several configuration options. It is possible to override certain parameters, or if it does not suit a particular use case, use the Expanded Form for granular configuration.
Serving Individual PHP Files Without Rewriting
If rewriting all requests to the root index.php
file is not necessary, or not desirable, Caddy can be configured to pass all .php
files to PHP-FPM without rewrites:
route {
# Add trailing slash for directory requests
@canonicalPath {
file {path}/index.php
not path */
}
redir @canonicalPath {http.request.orig_uri.path}/ 308
# If the requested file does not exist, try index files
@indexFiles file {
try_files {path} {path}/index.php
split_path .php
}
rewrite @indexFiles {file_match.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles path *.php
reverse_proxy @phpFiles <php-fpm_gateway> {
transport fastcgi {
split .php
}
}
}
This configuration is almost identical to the Expanded Form, but does not rewrite to the base index.php
file:
# If the requested file does not exist, try index files
@indexFiles file {
- try_files {path} {path}/index.php index.php
+ try_files {path} {path}/index.php
split_path .php
}
rewrite @indexFiles {file_match.relative}
Performance Tweaks
Caddy comes with several performance improvements built-in, and fine-tuned by default.
For example, the encode zstd gzip
directive makes Caddy encode the responses in zstd
or gzip
if the browser indicates in its request header that the browser can handle them. Caddy also sends the Vary: Accept-encoding
header by default, so CDNs and other caches know to not segment the cache by the Accept-encoding
value. It is small things like this that make Caddy a modern, and opinionated server with nice defaults.
Further, Caddy v2.7 has several great performance and security features enabled by default, including HTTP/3 and TLS 1.3 support, OCSP stapling support (so browsers do not have to query an OCSP server to check certificate validity), automatic Alt-Svc headers, dual RSA+ECC certificates, and more.
Make HTTP/3 Requests with PHP Curl Extension How to make HTTP/3 HTTP requests using PHP Curl extension, along with how to compile Curl with HTTP/3 support for PHP.
For PHP, some additional performance tweaks can be used if applicable:
Fast 404 Pages
Instead of rewriting all requests to the index.php
file, sometimes it makes sense to short-circuit and immediately end the response if the request URI is for a static file that the PHP application does not handle.
The following is an example snippet that matches incoming requests for certain extensions (such as .jpg
, .png
, .woff2
, etc), and immediately returns a page-not-found response if such file does not exist. This prevents unnecessarily invoking PHP (potentially more expensive, and often requires a database connection too) only to generate a page-not-found error from the application.
@static_404 {
path_regexp \.(jpg|jpeg|png|webp|gif|avif|ico|svg|css|js|gz|eot|ttf|otf|woff|woff2|pdf)$
not file
}
respond @static_404 "Not Found" 404 {
close
}
Cache Headers
Apache web server comes with a module named Expires (mod_expires
) that provides several directives to easily serve Cache-Control
and Expires
headers. Expires
header is outdated, which leaves Cache-Control
header deciding header browsers use to control its cache for the request.
Although Caddy does not provide a dedicated module or a set of directives for this, it is possible to use the existing header
directive to send Cache-Control
headers:
@static {
path_regexp \.(jpg|jpeg|png|webp|gif|avif|ico|svg|css|js|gz|eot|ttf|otf|woff|woff2|pdf)$
}
header @static Cache-Control "max-age=31536000,public,immutable"
This snippet sends Cache-Control: max-age=31536000,public,immutable
to all requests that end with jpg/jpeg/png/etc
, or otherwise static file types. Combined with Fast 404, these tweaks can make Caddy serve static files fast and efficiently while utilizing browser/CDN caches as well.
The
match
directive Caddy'smatch
directive can be used to set headers based on the response headers.
Security Tweaks
One of the major features of Caddy is that it supports automatic HTTPS. This includes obtaining a valid certificate (using ACME protocol) from Certificate Authorities like LetsEncrypt and ZeroSSL, as well as automatic HTTPS redirects.
It also sets a highly balanced and secure set of configuration values for TLS exchange curves and cipher-suits, while continuing to ensure that the default configuration will always be secure.
It is possible to turn off the automatic TLS certificate and redirects if it is desirable to obtain, validate, and renew certificates by other means. Because Caddy is always configured with sensible and secure defaults, PHP.Watch does not recommend changing the default TLS/HTTPS-related configuration options.
Security Headers
One of the easiest and most effective security tweaks a web site can make is sending the additional security headers along the application. This can range from powerful and fine0grained headers such as CSP and Permissions-Policy to headers such as HSTS and X-Content-Type-Options
.
The following shows an opinionated set of security headers set by a Caddy configuration file. It is highly likely that the example below does not apply to any real web site, but is here merely as a starting point:
header {
Strict-Transport-Security "max-age=31536000;includeSubDomains;preload"
X-Frame-Options "SAMEORIGIN"
X-Xss-Protection "1;mode=block"
Referrer-Policy "no-referrer-when-downgrade"
X-Content-Type-Options "nosniff"
Permissions-Policy "autoplay=(self),camera=(),geolocation=(),microphone=(),payment=(),usb=()"
?Content-Security-Policy "default-src 'self';script-src 'self';style-src 'self'"
}
Caddy supports header prefixes such as
?
, which tells Caddy to only send the header if it is not already set, or-
to delete the header if present.
Cookie SameSite Flags
Using Caddy's powerful header modification features, one can improve the security of the HTTP cookies set from the application by editing the Set-Cookie
header:
header >Set-Cookie (.*) "$1; SameSite=Lax;"
Note that for PHP applications using PHP sessions, it is advised to use the built-in PHP INI setting for SameSite cookies.
Limit Request Methods
If the PHP application is not designed to handle certain HTTP request methods, or if it is simply not required to handle certain HTTP methods, it is possible to allow-list HTTP request methods, making the Caddy reject all other request types:
@requestMethodsList {
not method GET HEAD POST OPTIONS
}
respond @requestMethodsList "Not Allowed" 405 {
close
}
This is similar to Apache's AllowMethods
directive:
<Location />
AllowMethods GET HEAD POST OPTIONS
</Location>
... and Nginx's limit_except
:
limit_except GET HEAD POST OPTIONS { deny all; }
Production Readiness
Some additional tweaks can assist PHP, and arrange the Caddy directives to align with the PHP application.
Trusted Proxies
When Caddy is behind a proxy, load balancer, or a CDN, the IP address of the client as observed from the PHP application and Caddy itself will be set to the IP address of the layer above, and not the real client.
This is solved by setting the static IP addresses of the proxy/balancer/CDN, making Caddy validate the source IP address to be in the trusted proxy list, and use the client IP address from the (configurable) X-Forwarded-For
header.
For example, if there is a load balancer with IP address 192.168.1.16:
trusted_proxies static 192.168.1.16
As Caddy takes care of the actual validation, the PHP application can rely on the client IP address without having to verify it again.
Request Body Size and PHP Upload Limits {#request-body}
Caddy supports optionally enforcing a maximum body size limit for HTTP requests. If this value is smaller than the PHP's post_max_size
(which in turn shadows the upload_max_filesize
), Caddy will terminate the request before it is handed to PHP.
request_body {
max_size 20MB
}
Setting a request_body
max_size
can help alleviate PHP's sometimes unpredictable behaviors on requests exceeding the post_max_size
.
Modular and reusable configuration
When a single Caddy instance serves multiple web sites, and everywhere else it makes sense, Caddy supports not only importing configuration files (as shown in the main Caddyfile
example above), but individual sections.
/etc/caddy/config/php.conf
(php83) {
php_fastcgi unix//run/php/php8.3-fpm.sock
}
(php82) {
php_fastcgi unix//run/php/php8.2-fpm.sock
}
The individual sections ((php83)
and (php82)
from this example) can now be used in any other configuration file:
/etc/caddy/sites/example.com.conf
example.com {
root * /var/www/example.com/public
import php83
# ...
}
Summary
Caddy is a modern web server with sensible, fast, and secure default configuration. It supports HTTP/3 and TLS 1.3 out of the box, automatic HTTPS and certificate lifetime management, and integrates well with PHP.
Caddy can be integrated with PHP using the most common "front controller" rewriting, or to serve individual PHP files via PHP-FPM.
When fine-tuned with additional performance tweaks and security tweaks, Caddy can serve dynamic and static content with additional security and caching headers, Fast 404 pages, and other best practices. Further, request limits and trusted proxies in Caddy can be configured to match the PHP application to alleviate some unpredictable behaviors with PHP, as well as to simplify certain tasks such as enforcing trusted proxy IP addresses.