Websites & apps

Run PHP and WordPress with Caddy and PHP-FPM

Serve PHP sites and WordPress from Caddy using the php_fastcgi directive and PHP-FPM, with working permalinks, larger upload limits and wp-config.php kept private.

IntermediateLinux server5 min read

Caddy does not run PHP itself. It hands PHP requests to PHP-FPM over a socket, much like Nginx does, but with far less configuration. The php_fastcgi directive covers the index file, the front controller fallback that WordPress permalinks need, and the FastCGI parameters. This guide sets up a WordPress site on Caddy and PHP-FPM, then tightens uploads and file access.

Before you start

  • A Linux VPS running Ubuntu 22.04/24.04 or Debian 12, with a sudo user. The paths below use the Debian family. On AlmaLinux or Rocky Linux, the socket is /run/php-fpm/www.sock and the pool file is /etc/php-fpm.d/www.conf.
  • Caddy installed from the official repository: Install Caddy web server with automatic HTTPS.
  • A domain such as example.com pointed at your server's IP.
  • A MariaDB or MySQL database and user for WordPress: Create a MySQL database and user for an app.

Install PHP-FPM and the WordPress extensions

Bash
sudo apt update
sudo apt install php-fpm php-mysql php-curl php-gd php-intl php-mbstring php-xml php-zip php-imagick

Find the version that was installed and its socket:

Bash
php -v
ls /run/php/

You will see a socket such as php8.3-fpm.sock. The version depends on your OS release. Use the name you see in every step below.

Let Caddy use the PHP-FPM socket

Caddy's service runs as the caddy user. The default PHP-FPM pool runs as www-data and creates its socket with the owner www-data and mode 0660, so Caddy cannot connect to it yet.

The simplest fix is to make the pool socket owned by caddy. Edit the pool:

Bash
sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Set these lines:

INI
listen = /run/php/php8.3-fpm.sock
listen.owner = caddy
listen.group = caddy
listen.mode = 0660

Keep user = www-data and group = www-data. PHP code still runs as www-data. Only the socket changes owner. Restart PHP-FPM:

Bash
sudo systemctl restart php8.3-fpm

Note: If other web servers on the same VPS also use this pool, give WordPress its own pool instead. See Run each site in its own PHP-FPM pool and user.

Download WordPress

Bash
cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar xzf latest.tar.gz
sudo mkdir -p /var/www
sudo mv wordpress /var/www/example.com
sudo chown -R www-data:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;

PHP runs as www-data, so WordPress can write uploads and updates. Caddy only needs to read the files, and the 755/644 modes allow that.

Write the Caddyfile

Bash
sudo nano /etc/caddy/Caddyfile
example.com, www.example.com {
	root * /var/www/example.com
	encode zstd gzip

	@blocked path /wp-config.php /xmlrpc.php
	@uploadsphp path_regexp ^/wp-content/uploads/.*\.php$
	@dotfiles {
		path_regexp /\.
		not path /.well-known/*
	}
	respond @blocked 403
	respond @uploadsphp 403
	respond @dotfiles 403

	request_body {
		max_size 64MB
	}

	php_fastcgi unix//run/php/php8.3-fpm.sock
	file_server
}

What each part does:

  • root sets the document root for every request.
  • @blocked, @uploadsphp and @dotfiles are named matchers. Requests for wp-config.php and xmlrpc.php, PHP files anywhere inside the uploads folder, and dotfiles such as .git or .env get a 403 before PHP ever sees them. /.well-known/ stays open for certificate and other standard checks. Remove /xmlrpc.php from the list if you use the Jetpack plugin or the WordPress mobile app.
  • request_body max_size caps upload size at the Caddy level.
  • php_fastcgi sends .php requests to the socket. Note the unix/ prefix followed by the absolute path, which gives two slashes.
  • file_server serves images, CSS and JS directly.

php_fastcgi already tries the requested file, then the directory's index.php, then falls back to /index.php. That fallback is exactly what WordPress permalinks need, so you do not add any rewrite rules.

Validate and reload:

Bash
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Tip: To send www visitors to the bare domain instead of serving both, see Caddy redirects, headers, gzip/zstd and caching.

Raise PHP upload limits

Caddy now accepts 64 MB bodies, but PHP has its own limits. Create a small override file so package updates do not overwrite it:

Bash
sudo nano /etc/php/8.3/fpm/conf.d/99-uploads.ini
INI
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 120

post_max_size must be at least as large as upload_max_filesize. Restart PHP-FPM to apply it:

Bash
sudo systemctl restart php8.3-fpm

Finish the WordPress install

Browse to https://example.com. Caddy will have fetched a certificate on the first request. Enter the database name, user and password, then create your admin account.

After install, go to Settings → Permalinks, choose Post name and save. WordPress cannot write an .htaccess here and does not need one.

Lock down wp-config.php on disk as well:

Bash
sudo chmod 640 /var/www/example.com/wp-config.php

Check it works

  • Open a post with a pretty URL such as https://example.com/hello-world/. It should load, not 404.
  • Confirm the block rules:
Bash
  curl -I https://example.com/wp-config.php
  curl -I https://example.com/.env

Both should return HTTP/2 403.

  • Upload an image larger than 2 MB in Media → Add New. It should succeed.

Troubleshooting

  • 502 Bad Gateway: Caddy cannot open the socket. Check the path in the Caddyfile matches ls /run/php/, and that listen.owner is caddy. journalctl -u caddy shows permission denied or no such file in that case.
  • The browser downloads index.php instead of running it: php_fastcgi is missing or placed in a different site block. Add it and reload.
  • "413 Request Entity Too Large" or uploads fail silently: raise both request_body max_size in Caddy and upload_max_filesize/post_max_size in PHP.
  • WordPress asks for FTP details when updating plugins: the files are not owned by www-data. Re-run the chown above. More in Fix WordPress asking for FTP credentials.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.