Handling PHP-FPM using Caddy

I've been using Caddy for years as a reverse proxy in front of other services, but recently wanted to use it to directly route traffic to PHP-FPM. Caddy has a specialized reverse proxy directive for PHP-FPM, php_fastcgi, which seemed like it would do the trick, but I found that no traffic was ever getting routed to my FPM pool.

What was happening

There's a fantastic StackOverflow answer describing what happens, and it's great reading.

The long and the short of it is that the php_fastcgi directive looks for matching files on the disk, and in the case of paths that look like directories, an index.php under that directory. In other words, it cannot match arbitrary paths (aka pretty URLs), just files. Which doesn't work with basically any fraį¹ework-based application; as the author of that SO answer posits, the directive was likely written to target Wordpress. Additionally, this causes issues if the PHP-FPM web pool is using a different root directory than you're using in your Caddy server, as Caddy will look for the files in the root defined for the server, and return a 404 if it can't find it, without ever passing it on to PHP-FPM.

The solution

The solution is to use the standard reverse_proxy directive, and have it use the fastcgi transport. You wrap this in a handle directive so that you can also specify a different filesystem root to pass to PHP-FPM.

As an example, let's assume:

  • I'm running PHP-FPM on port 9000 of the host "app".
  • The filesystem root for my PHP application is in /var/www/app.example.org/public
  • I want to route all requests to index.php in that root so that they are handled by the application.

I can write the Caddyfile as follows:

app.example.org {
    # Note that this is the filesystem root for the _server_
    root * /static/app.example.org

    file_server

    handle {
        # This is the filesystem root for the FPM pool
        root * /var/www/app.example.org/public
        rewrite index.php
        reverse_proxy app:9000 {
            transport fastcgi {
                split .php
                capture_stderr
            }
        }
    }
}

If the file is not found in the filesystem, it will try to pass it on to PHP-FPM. When it does, it rewrites to index.php in the PHP-FPM root (which is a different root than Caddy uses for its own file server!); PHP-FPM will use the matched path as the PATH_INFO, and your framework remains happy.