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.sockand 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.compointed 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
sudo apt update
sudo apt install php-fpm php-mysql php-curl php-gd php-intl php-mbstring php-xml php-zip php-imagickFind the version that was installed and its socket:
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:
sudo nano /etc/php/8.3/fpm/pool.d/www.confSet these lines:
listen = /run/php/php8.3-fpm.sock
listen.owner = caddy
listen.group = caddy
listen.mode = 0660Keep user = www-data and group = www-data. PHP code still runs as www-data. Only the socket changes owner. Restart PHP-FPM:
sudo systemctl restart php8.3-fpmNote: 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
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
sudo nano /etc/caddy/Caddyfileexample.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:
rootsets the document root for every request.@blocked,@uploadsphpand@dotfilesare named matchers. Requests forwp-config.phpandxmlrpc.php, PHP files anywhere inside the uploads folder, and dotfiles such as.gitor.envget a 403 before PHP ever sees them./.well-known/stays open for certificate and other standard checks. Remove/xmlrpc.phpfrom the list if you use the Jetpack plugin or the WordPress mobile app.request_body max_sizecaps upload size at the Caddy level.php_fastcgisends.phprequests to the socket. Note theunix/prefix followed by the absolute path, which gives two slashes.file_serverserves 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:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyTip: 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:
sudo nano /etc/php/8.3/fpm/conf.d/99-uploads.iniupload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 120post_max_size must be at least as large as upload_max_filesize. Restart PHP-FPM to apply it:
sudo systemctl restart php8.3-fpmFinish 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:
sudo chmod 640 /var/www/example.com/wp-config.phpCheck 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:
curl -I https://example.com/wp-config.php
curl -I https://example.com/.envBoth 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 thatlisten.owneriscaddy.journalctl -u caddyshowspermission deniedorno such filein that case. - The browser downloads
index.phpinstead of running it:php_fastcgiis 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_sizein Caddy andupload_max_filesize/post_max_sizein PHP. - WordPress asks for FTP details when updating plugins: the files are not owned by
www-data. Re-run thechownabove. More in Fix WordPress asking for FTP credentials.
Next steps
- Tune the PHP-FPM pool for your RAM: Tune PHP-FPM pools (pm.max_children).
- Harden the site further: WordPress security hardening on a VPS.
