How to use Caddy Server with PHP

Published On30 Oct 2023

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, Composer vendor directory, tests, composer.json file, NPM node_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's match 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.

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.

Recent Articles on PHP.Watch

All ArticlesFeed 
PHP 8.4 Installation and Upgrade guide for Ubuntu and Debian

PHP 8.4 Installation and Upgrade guide for Ubuntu and Debian

A guide for Debian and Ubuntu on how to install PHP 8.4 on a new server or how to upgrade an existing PHP setup to PHP 8.4.
How to fix `mysql_native_password` not loaded errors on MySQL 8.4

How to fix mysql_native_password not loaded errors on MySQL 8.4

How to fix the SQLSTATE[HY000] [1524] Plugin 'mysql_native_password' is not loaded errors caused in MySQL 8.4 no longer enabling the mysql_native_password plugin by default.
How to fix PHP Curl HTTPS Certificate Authority issues on Windows

How to fix PHP Curl HTTPS Certificate Authority issues on Windows

On Windows, HTTPS requests made with the Curl extension can fail because Curl has no root certificate list to validate the server certificates. This article discusses the secure and effective solutions, and highlights bad advice that can leave PHP applications insecure.
Subscribe to PHP.Watch newsletter for monthly updates

You will receive an email on last Wednesday of every month and on major PHP releases with new articles related to PHP, upcoming changes, new features and what's changing in the language. No marketing emails, no selling of your contacts, no click-tracking, and one-click instant unsubscribe from any email you receive.

Support PHP.Watch — If you find the articles, version information, Codex, and other PHP.Watch contributions useful, consider supporting through GitHub Sponsors. Your sponsorship helps dedicate more time to creating valuable content and improving the PHP community. Together, we can keep the momentum going — thank you for your support!

Thanks to the highest tier sponsor: @TomasVotruba for your generous support to keep PHP.Watch moving 💜