Local PHP development environment with macOS, Homebrew and Caddy

I have been trying out a new development environment, because I was dissatisfied with both MAMP Pro's performance and Docker for Mac's performance when running PHP applications locally.

I have been using these setups for a couple of years and was wary of trying a manually managed stack of a web server, PHP and database. But with a bit of experience with Caddy and after some benchmarking, I was pleasantly surprised at both the performance and ease of use of this new combination.

My Requirements

What do I need this to do?

I have a couple of expectations of a local development environment, which should be possible for me to consider a setup:

  • Works on macOS
  • Low verbosity
  • Minimal interactions in daily usage
  • Multiple PHP versions in parallel
  • High performance

MAMP Pro and Docker for Mac had issues, especially with the performance part. This was due to different reasons.

MAMP's PHP has performance problems I don't really understand. I just saw much higher response times when comparing it to a basic PHP installation with Homebrew.

Docker for Mac has problems with I/O performance, especially with the high number of files usually present in a modern PHP application. Attempts with anonymous Docker volumes, mutagen, syncing via rsync or other ideas all felt clunky, brittle or didn't provide much of a performance improvement.

So that led me to a setup with Caddy and PHP via Homebrew.

Setup PHP

Install all the PHP versions you want to use for your projects:

brew install [email protected] [email protected] php

Setup PHP-FPM to use different socket files for each version to keep management simple.

$ vim /usr/local/etc/php/7.4/php-fpm.d/www.conf
---
[...]
listen = /usr/local/var/php/php7.4.sock

You can also tune the FPM configuration. Maybe something like this works for you:

pm.max_children = 32
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500

Then, you can set up the PHP-FPM services to start automatically:

brew services start [email protected]
brew services start [email protected]
brew services start php

You can change your CLI PHP version with brew unlink [email protected] && brew link [email protected] or use your required binary directly, e.g. /usr/local/Cellar/[email protected]/7.4.30/bin/php -h.

Setup Caddy

I'm using Caddy as a lightweight HTTP server with HTTPS handling included. It is shockingly easy to configure it, e.g. for a Symfony application.

First, install Caddy with brew install caddy. Open /usr/local/etc/Caddyfile and add a block for each of your projects, like this:

my.project.tld {
    tls internal
    root * /Users/me/code/my-project/public
    php_fastcgi unix//usr/local/var/php/php7.4.sock # or any other PHP version
    file_server
}

Start up Caddy once with caddy run --config /usr/local/etc/Caddyfile to make sure everything works as expected. Caddy will install a certificate in your local key store, so your browsers will accept Caddy's self-generated TLS certificates.

If all is well, you can set up the Caddy service like this:

brew services start caddy

Last, but not least, you have to add your domains to /etc/hosts to make them accessible by name:

$ sudo vim /etc/hosts
---
[...]
127.0.0.1 my.project.tld

Other Services

If you're using any other services (like MySQL, Elasticsearch, Redis...) you can start them up whichever way you want. I'm using MySQL via Homebrew, and start up a project-specific Docker container for Redis and Elasticsearch.

This gives me a bit of isolation between project data and makes configuration handling easier.

I have found some important MySQL parameters for Homebrew's MySQL installation. Those gave me a much better performance in Shopware 6, which is what I work with most days. I haven't tuned MySQL much, so take those parameters with a grain of salt.

Use these in your /usr/local/etc/my.cnf, but make sure you verify them for your use cases and adapt accordingly:

group_concat_max_len = 2048
key_buffer_size = 16777216
max_allowed_packet = 134217728
sync_binlog = 0
table_open_cache = 1024

I currently work with a 16" MacBook Pro 2019 with a 6-core i7 and 32 GB of RAM, to give those numbers some context.

Final Thoughts

So far, I'm very pleased with this setup. It's fast, easy to use, and removes most of the complexity of local setups in day-to-day usage. Let me know if you like it as well 🙂