Switching From Laravel Valet to Caddy and Mise (1 of 2)
Why Switch?
I’ve been using Laravel Valet for local development for a long time. It does a great job at what it was designed for - running a secure site proxy for local development and managing PHP versions on a per-site basis. I found it less ideal for projects that needed more complex proxy configuration, which often required editing nginx configuration files manually. I also still needed various tools for managing non-PHP dependencies (like nvm, uv, etc).
Having started using Caddy in production both at home and at work, I had really grown to appreciate its intuitive and flexible configuration language. I wondered: how painful would it be to replace Valet with Caddy for local development?
Certainly, Caddy could easily fill the role of a secure proxy, but how would I handle managing PHP versions? I didn’t want to install yet another tool for that, but explored the various options anyway. I found a few solutions, like phpenv and phpmon, which certainly would have worked but seemed like they would add more complexity than Valet. I parked the idea for some time.
Then I happened upon this interview with the creator of mise-en-place. It seemed like it might solve the missing piece of the puzzle - an all-in-one version manager for any dev tool or language dependency I could possibly need. I later learned it was even more powerful, providing tools for things like defining tasks and managing environmental variables. After playing around with it a bit, I decided it was time to try giving this a shot.
My New Workflow
My workflow for creating a new site is now:
-
Create a new configuration file for Caddy.
This is usually a simple, single directive file that proxies
my-cool-site.testto a service on localhost. -
Create a
mise.toml(ormise.local.toml) file in the root of the project.This describes the project’s dependencies and any tasks I want to define.
-
Profit?
When I
cdinto that project directory, mise automatically installs any dependencies I need and sets up the environment for me (including switching myphpcommand to the specified version in a PHP project).
What’s nice about this is:
- No magic, there’s a single proxy configuration file for each site.
- All the build dependencies, runtime dependencies, and tasks for the project managed in a single .toml file.
- I don’t need to remember whether I set up task runners for a project in composer.json, package.json, or somewhere else. I can run
mise runto see what tasks are configured for any project.
This doesn’t take that much more time than setting up a new site with Valet, and I feel much more in control of my development environment.
If you’re interested in giving it a try, below are the steps I followed…
Step 1 - Uninstall Valet
Valet provides an uninstall command that will stop nginx and PHP (removing the .sock files so that you can remove the ~/.config/valet directory) and print out further instructions. This is the route I took, since I wanted to keep PHP and dnsmasq installed.
valet uninstall
brew uninstall --force nginx
mv ~/.config/valet ~/.config/valet.bak
# or
# rm -rf ~/.config/valet if you don't need to keep it as a reference
Step 2 - Configure PHP-FPM
Next you’ll need to give php-fpm a new location to store its .sock files.
I put this in $XDG_STATE_HOME, which (I think) needs to be hard-coded. Create the storage directory and check the location of this directory by running:
mkdir -p $XDG_STATE_HOME && echo $XDG_STATE_HOME
# /Users/jesse/.local/state
Now, in /opt/homebrew/etc/php look in each PHP version’s directory.
If you have a www.conf file, make the change there.
Otherwise, rename www.conf.default to www.conf and make the change.
Finally, delete valet-fpm.conf if it exists.
This approach will help keep things from breaking after PHP updates or re-installs. If you start experiencing 502 errors, check this value in www.conf to ensure it wasn’t
overwritten.
-listen = /Users/jesse/.config/valet/php.sock
+listen = /Users/jesse/.local/state/php84.sock
+;; from https://github.com/laravel/valet/blob/e3902e712136b5b4746cb2e2cfb9010e6ca2e63b/cli/stubs/etc-phpfpm-valet.conf (you may not need this if not using Postgres)
+;; these are an attempt to mitigate 502 errors caused by segfaults in upstream processes caused by krb5 v1.21 added in June 2023 to php's core build. Ref Issue #1433
+; for gettext
+env['LC_ALL'] = C
+; for postgres
+env['PGGSSENCMODE'] = disable
Now you should be able to restart PHP without any errors and see one socket file in ~/.local/state/php for each version of PHP installed.
brew services restart php
# Repeat for any other versions of PHP you are using
# brew services restart php@8.3
# or just
# brew services restart --all
brew services # should show all versions of PHP-FPM running
Step 3 - Fix Service Permissions
Laravel Valet executes a number of commands using sudo, which frequently caused permissions problems for me when doing things like brew upgrade.
I learned that sudo brew services start serviceName will auto-start the services at system startup, while brew services start serviceName will start it at user login. There’s really no reason for PHP-FPM to run at system startup for my use case.
I left dnsmasq running at system startup, although I did not experiment to see if this was actually necessary.
> sudo brew services stop php
> sudo brew services stop php@8.3
## Repeat for as many versions of PHP as you have installed
> brew services start php
> brew services start php@8.3
## Etc
> sudo brew services list
Name Status User File
dnsmasq started root /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
php none root
php@8.3 none root
> brew services list
Name Status User File
caddy started jesse ~/Library/LaunchAgents/homebrew.mxcl.caddy.plist
dnsmasq none root
php started jesse ~/Library/LaunchAgents/homebrew.mxcl.php.plist
php@8.3 started jesse ~/Library/LaunchAgents/homebrew.mxcl.php@8.3.plist
Your list of services may look slightly different, but you get the idea.
If you get permission errors when starting the services, this is related to the original valet install. You can either manually fix the necessary permissions or just uninstall and reinstall PHP from Homebrew.
Step 4 - Set up Caddy
Next we’ll set up Caddy and get the configuration in place.
brew install caddy
mkdir -p $XDG_CONFIG_HOME/caddy
mkdir -p $XDG_CONFIG_HOME/caddy/sites
touch $XDG_CONFIG_HOME/caddy/Caddyfile
echo "import $XDG_CONFIG_HOME/caddy/Caddyfile" >> /opt/homebrew/etc/Caddyfile
Next add the following to your Caddyfile (customizing the php_fastcgi lines to match your username and installed PHP versions):
{
local_certs
}
(php84) {
file_server
encode zstd gzip
php_fastcgi unix//Users/jesse/.local/state/php84.sock
}
(php83) {
file_server
encode zstd gzip
php_fastcgi unix//Users/jesse/.local/state/php83.sock
}
import ./sites/*.caddy
Finally run brew services restart caddy to pick up the new configuration.
Trusting SSL Certificates
The local_certs directive above tells Caddy to issue a self-signed root CA certificate and use it to sign certificates for any local development domains you create. You need to trust this root CA in your Keychain so browsers will accept the certificates Caddy generates without warnings.
To do this, run:
sudo security add-trusted-cert -d -r trustAsRoot -k "/Library/Keychains/System.keychain" /opt/homebrew/var/lib/caddy/pki/authorities/local/root.crt
# (Running `caddy trust` should also work, but it didn't for me).
Add your first site
Add a site by creating a .caddy file in $XDG_CONFIG_HOME/caddy/sites/. See the Caddyfile documentation for more details on configuration options. Here’s a simple example for a Laravel site:
beebic.test {
root /Users/jesse/Projects/beebic/public
import php84
}
Run brew services restart caddy and you should be able to visit your site at the .test domain.
In the next post I’ll cover adding mise-en-place to the mix.
If you followed this guide and ran into any issues or have any tips, please email me so that I can correct or improve it.