Guide

Serve PHP with Caddy and php-fpm

Part 4 of 5By Amith Kumar8 min read
Part 4 of 5Caddy: the Auto-HTTPS Web Server, A Hands-On Guide
  1. 1Install Caddy and the Caddyfile Model
  2. 2Serve a Static Site with Caddy's Automatic HTTPS
  3. 3Reverse Proxy an App with Caddy
  4. 4Serve PHP with Caddy and php-fpm
  5. 5Production Caddy: Headers, Logging and Rate Limiting
Verified 24 Jul 2026 · Ubuntu 24.04 LTS

Serve PHP with Caddy and php-fpm

A static site is files; a PHP site is code that runs per request. This chapter serves PHP with Caddy by wiring the php_fastcgi directive to php-fpm on Ubuntu 24.04, then watches a real page execute and return over HTTPS. Where an nginx setup needs a location block, a FastCGI snippet and a socket path to join the two, Caddy folds all of that into one directive, and the automatic certificate from the earlier chapters means the dynamic page is served over TLS with nothing extra to set up.

Key takeaways
  • Caddy does not run PHP itself; it hands a request to php-fpm over FastCGI, and php-fpm executes the code.
  • The php_fastcgi directive points at the fpm socket and sets up the index routing, path splitting and file check for you.
  • A unix socket under /run keeps the runtime off any network port, so only Caddy reaches it.
  • A value the page computes on each request, like a timestamp, proves the code ran rather than being downloaded.
Have these ready
  • The running Caddy service from the previous chapters, still issuing certificates and serving over HTTPS.
  • A short PHP file to serve, here an index.php under /srv/tidepool-php that reports a tide status as JSON.
  • Sudo rights to install the php-fpm package and to reload the Caddy service after editing the site.
  • A moment to compare the result against the nginx FastCGI setup, if you have wired one before, to feel the difference.

Install php-fpm and find its socket

Caddy speaks FastCGI, but it needs a FastCGI server to speak to, and that is php-fpm. A fresh slice has no PHP, so install the fpm package; on Ubuntu it pulls the 8.3 runtime and the command-line binary with it.

sudo apt install -y php-fpm
php -v
Installing php-fpm on Ubuntu 24.04, php -v confirming the 8.3.6 runtime, and the fpm service exposing a pool of PHP workers on a unix socket under /run/php owned by the web user, the socket Caddy connects to over FastCGI

Reading php -v back confirms the build, here PHP 8.3.6. Installing php-fpm also starts a systemd service that manages a pool of PHP workers and exposes them on a unix socket. That socket, a file under /run/php owned by the web user, is what Caddy connects to. A socket on disk rather than a TCP port means nothing is published on an interface, and access is a plain filesystem permission.

Checkpoint

php -v should print an 8.3 line, and ls /run/php should list a php8.3-fpm.sock file. If the socket is missing, the fpm service did not start, so bring it up with sudo systemctl start php8.3-fpm and confirm the file exists before pointing Caddy at it.

The fpm service manages a pool of worker processes that stay warm between requests, so PHP is not started fresh each time a page loads. That pool is where the runtime's own tuning lives, its size, its user and its recycling, and it is the same pool whether Caddy or another web server sits in front. For this chapter the shipped default pool is enough, because the point here is the wiring rather than the tuning.

Serve a PHP page with Caddy and php-fpm

The page for this chapter is deliberately small: an index.php that reads the current tide phase from the clock and answers as JSON, so there is nothing but the runtime to look at. Serving it is one directive. Point root at the code and add php_fastcgi with the socket path.

app.tidepool.example.com {
    root * /srv/tidepool-php
    php_fastcgi unix//run/php/php8.3-fpm.sock
    file_server
}

That block is the entire integration. php_fastcgi sends PHP requests to the fpm socket and, on its own, routes a clean URL to index.php, splits the path so fpm gets the right script, and checks the file exists before passing it. file_server serves any static assets that sit alongside the code. Reload Caddy and call the page over HTTPS.

curl -s https://app.tidepool.example.com:8443/
The PHP page executing through Caddy php_fastcgi and returning computed JSON over HTTPS: the runtime reporting PHP 8.3.6, the server API reading fpm-fcgi which confirms php-fpm ran it, and a timestamp that changes on a second call, proving the code ran rather than being downloaded

The response is JSON the code built this request: the runtime reports PHP 8.3.6, the server API reads fpm-fcgi which confirms it ran under php-fpm and not the command line, and a timestamp shows the moment it executed. Call it again and the timestamp moves, which is the proof the page ran rather than being served as a file. Had the wiring been wrong, you would see the raw <?php source come back instead, the classic sign a request never reached fpm.

Checkpoint

The response should be computed JSON with the server API reading fpm-fcgi, and the timestamp should change between two calls. If the browser downloads the PHP file or shows its source, the request is not reaching fpm, so confirm the socket path in php_fastcgi matches the file under /run/php and reload.

One directive, the whole FastCGI pipeline

The single php_fastcgi line hides a set of handlers, and it is worth seeing what it stands for. Adapt the config and look at how the directive expands for the PHP site.

sudo caddy adapt --config /etc/caddy/Caddyfile
caddy adapt showing the single php_fastcgi directive expand into the full FastCGI pipeline: a rewrite routing a clean URL to index.php, a path split, a reverse proxy to the fpm socket, and the file server behind it, the whole nginx FastCGI block as one tested line

The one line becomes a subroute: a rewrite that routes a directory or clean URL to index.php, a path split so fpm receives the script name and the trailing path separately, a reverse proxy to the fpm socket carrying the FastCGI transport, and the file server behind it for static files.

In an nginx config each of those is a line you write and can get subtly wrong; the path-splitting mistake in particular has been the root of real security holes. Caddy ships that whole pipeline as one tested directive, which is fewer lines to author and fewer to get wrong.

Going further: front controllers and the runtime boundary

The demo is one file, but the same directive serves a full framework. A Laravel or WordPress site is a front controller plus a tree of code, and php_fastcgi already does the front-controller routing that those expect, sending unknown paths to index.php. Keep the code out of harm's way by pointing root at only the public directory and leaving the application and any database files one level up, so the web has no path to them at all.

The runtime tuning, the fpm pool sizing, the worker count, OPcache, the per-app user, lives on the php-fpm side and is the same work regardless of which web server sits in front. Caddy's job ends at the socket; what happens past it is the runtime's, and that clean split is why either side can be changed without disturbing the other.

Frequently asked questions

Do I need a PHP module for Caddy the way old Apache had mod_php?

No. Caddy has no in-process PHP module, and it does not need one. It talks to php-fpm over FastCGI, the same model nginx uses, so you install php-fpm as a separate service and join it with the php_fastcgi directive. That separation lets you reload or tune either side on its own, and it is why the integration is one config line rather than a compiled extension.

Should php-fpm listen on a socket or a TCP port?

On a single box the unix socket is the natural pick, and it is what the fpm package sets up by default. There is no port to expose by accident, and access is a file permission Caddy holds. A TCP listener like 127.0.0.1:9000 earns its place when the runtime runs on a different host from Caddy, or when a tool insists on a host and port. Both keep the runtime off the public interface, which is the part that matters.

Why does the server API read fpm-fcgi and does it matter?

It is how PHP reports which server interface ran the request. Seeing fpm-fcgi confirms the page executed inside a php-fpm worker over FastCGI, not from the command line and not as a downloaded file. It matters because settings differ between the fpm and cli interfaces, so a value you change for one does not affect the other. When you tune the runtime for the web, edit the fpm configuration, and this field is your confirmation the web path is the one you are on.

Can Caddy serve more than one PHP application on the same box?

Yes. Give each application its own site block with its own root and its own php_fastcgi socket, and run a separate fpm pool per application so each has its own user and worker budget. Caddy routes by hostname, so two sites on one server stay isolated. The runtime isolation between pools is a php-fpm concern; Caddy simply points each site at the right socket.

What you have, and what comes next

You installed php-fpm, found its socket, wired it to Caddy with a single php_fastcgi directive, and watched a dynamic PHP page execute and return over HTTPS with a timestamp that proves the code ran. You also saw that one directive stand in for the whole FastCGI pipeline an nginx config spells out by hand.

The site now serves static files, proxies an app, and runs PHP, all over automatic HTTPS. The final chapter takes this to production: security headers, structured logging, rate limiting, systemd hardening, and a graceful reload that swaps config with no downtime.


Run Caddy on your own slice

Run a site with auto-HTTPS on a slice

Automatic HTTPS needs a server with a public IP and a domain. Spin up a slice, install Caddy from this guide, and serve your site over a certificate that renews itself.

Get early access

This guide is provided for educational purposes and offered as is, without warranty of any kind. The commands change the configuration of a server you control, and some can lock you out if run out of order. Test on a non-production machine, keep a second session open, and take a snapshot or backup before you begin. You are responsible for changes made to your own systems; ServerCake accepts no liability for any loss, downtime, lockout, or damage arising from following this guide or running the accompanying script.

Was this guide helpful?
Share

Spotted something out of date or have a question?

Built by the ServerCake team

Cloud that speaks India.

ServerCake is India's KYC-verified cloud: VMs and managed databases priced in ₹, billed with GST, on India-resident infrastructure. Reserve your spot for early access.

Reserve your spot