Using Caddy for WordPress Local Development on Linux

There are no shortage of options for managing WordPress sites locally. I really like the simplicity of both Local and DevKinsta and highly recommend them for the average user. For advanced users that prefer the command line, Laraval Valet is a good option. All of these tools are great when they work however if you run into technical issues, then welcome to an endless rabbit hole of learning about the inner workings of the tools themselves.

After seeing the complexities that come with each approach I decided to see how complicated is it to just do everything from scratch. If you’d like to attempt a more do it yourself approach then read on.

Start with Caddy, PHP FPM, MariaDB and WP-CLI then mix and match for extra features.

Caddy is modern web server with automatic HTTPS. It includes a local HTTPS feature which is perfect for configuring WordPress sites for local development. Configuring Caddy is a joy which is why I picked it over Apache or Nginx.

PHP FPM and MariaDB are configured very similar to a typical LEMP stack setup except we’ll be replacing Nginx for Caddy. Before digging in a few warnings. I’m not an expert at Caddy, this was only my second time actually using it. Also I run Linux (Pop!_OS) on my Laptop. In theory this approach could be adapted for MacOS and WSL on Windows however that’s outside the scope of what I’ll be covering.

Installing the tech stack.

Follow the these guides to install Caddy, MariaDB and WP-CLI. Each version of PHP that you want to use will need to be installed separately. I’m using both PHP 7.4 and 8.0 which is installed as shown below. Your need for various PHP extensions may vary. You can always add additional ones later.

sudo apt install php7.4-common php7.4-fpm php7.4-curl php7.4-mysql php7.4-xml php7.4-xmlrpcphp7.4-gd php7.4-imagick php7.4-cli php7.4-dev php7.4-imap php7.4-mbstring php7.4-opcache php7.4-soap php7.4-zip php7.4-intl php7.4-bcmath -y
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php8.0 php8.0-fpm php8.0-curl php8.0-mysql -y

Configuring Caddy and folder structure for local sites.

For each site let’s create a logs and public directory.

mkdir -p ~/Caddy/sitename1.localhost/logs/
mkdir -p ~/Caddy/sitename1.localhost/public/

Caddy can be configured using a single file name Caddyfile. Let’s create that here ~/Caddy/Caddyfile. Here is a copy of my Caddyfile which you can use as a starter template. It’s already fairly readable. You’ll need to swap out the website names with your sites and change the folder locations accordingly.

(boilerplate) {
    root * "/home/austin/Caddy/{host}/public"
    encode gzip
    tls internal
    php_fastcgi unix//run/php/php{args.1}-fpm-{host}.sock
    log {
        output file /home/austin/Caddy/{args.0}/logs/server.log

anchor.localhost {
    import boilerplate anchor.localhost 7.4

captaincore.localhost {
    import boilerplate captaincore.localhost 8.0

Typically you need to add each host name to your local HOSTS file /etc/hosts/. That’s not required when using .localhost. That top level domain has special rules which require it to always loopback and is ideal for development only site.

Configuring PHP pool per site.

We’ll be creating a separate PHP pool per site which is explained more in-depth in this article. Create a php.conf file and save it at the top level of each site, example: ~/Caddy/sitename1/. This will determine which version of PHP it will use and allow for other custom PHP configurations. You’ll need to change the site name, user, listen and php_admin_value[error_log] variables with appropriate values for your setup.

user = austin
group = www-data
listen = /run/php/php7.4-fpm-anchor.sock
listen.owner = www-data = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chdir = /
php_admin_value[error_log] = /home/austin/Caddy/anchor.localhost/logs/php.log
php_admin_value[memory_limit] = 512M

Next link this file to PHP’s pool.d directory which will setup a custom PHP pool for just this site.

sudo ln -s ~/Caddy/anchor.localhost/php.conf /etc/php/7.4/fpm/pool.d/anchor.localhost.conf

Last we’ll need to restart PHP FPM in order to have PHP pick up the new PHP pool.

sudo service php7.4-fpm restart

Repeat these steps for each site. For different versions of PHP simply swap out references in the conf file and be sure to link it to correct location. Also don’t forget to update the Caddyfile php_fastcgi to the new socket location. Got all of that?

Additional configurations for file permissions.

Out of the box WordPress won’t be able to write to the file system. That means if you try installing themes or plugins you’ll be prompted with a security box.

There are a couple ways to resolve this permissions issue. The first and safest way would be to change file/folder ownership of your website directories to www-data. However that also makes managing your website files locally a mess. Instead we can grant www-data with extra access to the files directly. Considering this is a local environment, I’m alright with lessening my security for a bit of convenience.

sudo usermod -a -G www-data $( whoami ) 

Next edit PHP’s FPM pool /etc/php/7.4/fpm/pool.d/www.conf and change user = www-data to user = username to your actual username. If your not sure what that is then run whoami in the command line. Then restart PHP.

systemctl restart php7.4-fpm

This will need repeated for each version of PHP.

Spinning up Caddy.

Let’s check to make sure we can spin up Caddy and verify PHP is working. Create a phpinfo.php file within one of the sites with <?php phpinfo(); then run Caddy.

cd ~/Caddy
sudo caddy start

If you run into errors then you might need to first stop caddy.

sudo caddy stop

In the browser visit https://sitename1.localhost/phpinfo.php.

If you happen to have issues with the SSL complaining, here are a few tips. Try running.

sudo caddy trust

Next check your browser’s SSL certificates to make sure Caddy’s local CA and Immediate certificates are trusted. For Chrome that located at chrome://settings/certificates then select Authorities. Import them from Caddy’s data directory.

Verify that both are fully trusted.

If Chrome continues to complain about the SSL cert then here are a few other ideas. Running Caddy with sudo can cause the SSL certs to be placed in a diifferent location. On my Linux setup that’s located here /root/.local/share/caddy/pki/authorities/local/. Always having to import these manually can be time consuming. Luckily that to can be automated with the following command.

sudo certutil -d sql:$HOME/.pki/nssdb -A -t TC -n "org-Caddy Local Authority - ECC Intermediate" -i /root/.local/share/caddy/pki/authorities/local/intermediate.crt

Importing or creating our first WordPress site.

Assuming MariaDB was properly installed and we know the root password, we can import an existing WordPress site by copying over the WordPress files over to our public/ directory. Update wp-config.php with the new database info. We can name DB_NAME anything unique. Then with WP-CLI run the following.

cd ~/Caddy/sitename1.localhost/public
wp db create
wp db import database-backup.sql   # or where ever we have a SQL backup

Create a new site can be accomplished with just a few WP-CLI commands. Assuming we have an empty /public directory, then run the following.

wp core download
wp config create --dbname=database_name --dbuser=root --dbpass=database_password
wp db create
wp core install --url=https://sitename1.localhost --title="Site 1" --admin_user=username --admin_email=email@domain.tld 

Final thoughts.

Doing everything from scratch is not for everyone. However it’s great way to learn about how web servers work and have complete control over the entire stack. There are no limitations. If I want to try out an experimental version of PHP, I don’t have to wait for a new release to my local development application. I can just install it and start using it right away. Similarly if I run into a bug, I don’t have to create a support ticket and wait for someone else to fix it, I can fix the issue myself.

I think there is plenty of room in the WordPress locally development for even more options. I would love to see a polished solution built around Caddy with additional feature like public link sharing, local email intercept and proper GUI or CLI to manage sites. If anyone out there builds that, let me know and feel free to use this guide as rough starting point.