- Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
You clone a client's Laravel app on a fresh machine. It needs PHP 8.1. Your main project is on 8.4. Homebrew has one active PHP at a time, so you start the usual dance: brew unlink php, brew link php@8.1 --force, restart the FPM pool, remember you also had Valet pinned somewhere, break the other project, spend the morning on it. Then the second client shows up on 8.3 and you do it again.
Laravel Herd was built to end that dance. It's a native macOS and Windows app from the team behind Laravel that bundles PHP, Nginx, and a DNS resolver into a single install. No Homebrew PHP. No editing nginx.conf. In 2026 it has grown past being "a nicer Valet" into a full local stack, and for most day-to-day Laravel work it replaces Valet outright.
Here's what actually earns its place, and where it stops making sense.
What Herd is doing under the hood
Herd runs the same architecture Valet always did, just packaged. A dnsmasq process resolves the .test TLD to 127.0.0.1. An Nginx process listens on 80 and 443 and routes each hostname to the matching site directory. PHP runs as FastCGI. The difference is you never touch any of those config files by hand. Herd ships its own PHP binaries inside the app bundle, so there's no brew link conflict with anything else on the machine.
You point Herd at a folder and every subdirectory becomes a .test site:
cd ~/Sites
herd park
# ~/Sites/invoicing -> http://invoicing.test
# ~/Sites/crm -> http://crm.test
Or link a single project from anywhere:
cd ~/work/client-app
herd link client-app
herd secure client-app # TLS cert, https://client-app.test
That part is Valet feature-for-feature. The reason to switch is everything stacked on top.
Per-site PHP versions without the Homebrew fight
This is the feature that sells it. Herd bundles multiple PHP versions and lets you pin one per site. No linking, no unlinking, no global state.
cd ~/work/legacy-app
herd isolate 8.1
cd ~/work/new-app
herd isolate 8.4
Each site now runs its pinned version. legacy-app.test gets PHP 8.1, new-app.test gets 8.4, and they run at the same time. The CLI follows the directory too, so php -v inside an isolated project reports that project's version:
cd ~/work/legacy-app && php -v # PHP 8.1.x
cd ~/work/new-app && php -v # PHP 8.4.x
The global default is a dropdown in the app. You set it once, override per project when a project needs it, and the Homebrew PHP juggling is gone. For anyone maintaining a spread of client apps on different versions, this alone justifies the install.
One-click databases and services (Herd Pro)
The free tier gives you PHP, Nginx, and Node. The paid tier (Herd Pro) adds the piece Valet never had: managed local services. Instead of running your own MySQL through Homebrew or spinning up a Docker container, you add a service from the UI or the CLI and Herd manages the process.
You get MySQL, PostgreSQL, Redis, and object storage, each as a named instance you can start and stop. Point your .env at the port Herd assigns:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=invoicing
DB_USERNAME=root
DB_PASSWORD=
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
Run more than one MySQL instance on different ports when two projects want incompatible versions or clean data. There's no docker compose up, no volume to reason about, no container networking. The services start with the app and sit in the menu bar.
Herd Pro also folds in the tools you'd otherwise wire up separately: Mailpit catches outbound mail so Mail::to(...)->send(...) lands in a local inbox instead of the internet, a log viewer tails storage/logs, and there's a dump/ray-style viewer for dump() output. These are the things you install piecemeal on every new machine. Herd has them on first launch.
Where Docker still wins
Herd is a developer-machine tool. It makes your Mac or Windows box pleasant to build on. It does not make your environment reproducible, and that's the line.
Docker's whole point is that the container you run locally is the container that runs in CI and in production. Same base image, same extension versions, same OS libraries. When a bug only shows up because production runs libicu at a version your Mac doesn't have, Docker catches it locally and Herd cannot. Herd's PHP is Herd's build on your host OS.
So the split most PHP devs land on:
- Herd for the projects you live in every day, where fast feedback matters more than parity: local Laravel apps, quick prototypes, client work across mixed PHP versions.
- Docker where the environment itself is part of the contract: anything with a custom PHP extension, native libraries, a specific Linux distro, or a team that needs "works on my machine" to mean "works on every machine."
Nothing stops you running both. Herd serves the app and the everyday services; a docker-compose.yml spins up the one weird dependency that has to match production, like a specific Kafka or a patched image processing library. They coexist fine.
The reproducibility trade-off, stated plainly
Herd's speed comes from convenience state that lives on your machine and nowhere else. The PHP version is pinned by a Herd setting, not by a file in the repo. The database exists in Herd's data directory, not in a versioned volume. A new teammate can't git clone and get your exact stack; they install Herd, then recreate the setup by hand or from your README.
Docker inverts that. The Dockerfile and docker-compose.yml are committed, so the environment is code. Slower to boot, heavier on the machine, reproducible by definition.
You can close part of the gap. Commit the PHP constraint in composer.json so a teammate who clones the repo knows which version to herd isolate on their machine:
{
"require": {
"php": "8.4.*"
},
"config": {
"platform": {
"php": "8.4.0"
}
}
}
The require.php constraint documents what the app runs on, and config.platform.php tells Composer to resolve dependencies against that version. Neither switches Herd's runtime on its own, though. The teammate still runs herd isolate 8.4 in the project after cloning; the committed constraint just tells them which number to use. Keep a short README block listing the services a project expects (MySQL 8, Redis, a mail catcher) so they can recreate them in Herd in a minute. That gets you most of the ergonomics without pretending Herd is a parity tool. It isn't, and using it as one is where teams get burned.
So, should you switch
If you're on Valet today and you spend any time fighting PHP versions, switch. Herd is a strict superset of what Valet does, with the version juggling and the local services solved. If you're a solo dev or a small team building Laravel apps and you don't have exotic native dependencies, Herd will carry most of your work and Docker becomes the exception you reach for, not the default you boot every morning.
If your environment is part of your deploy contract, keep Docker in the loop for the projects that need it. The two aren't rivals. Herd makes the machine nice; Docker makes the environment portable. Most PHP shops in 2026 want both, aimed at different problems.
The pattern underneath
Notice what made the Herd-versus-Docker call easy: the app didn't care. A Laravel app whose domain logic depends on which local tool serves it is an app that's tangled its business rules into its infrastructure. When the database driver, the mail transport, and the queue are behind interfaces your use cases talk to, swapping Herd's MySQL for a Dockerized one, or a real production broker, changes an adapter and nothing else. Keeping infrastructure at the edge is exactly what Decoupled PHP is about: the architecture layer your Laravel code reaches for once it needs to outlive whatever local stack you happen to run this year.
Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

Top comments (0)