I spent most of last Tuesday not writing code. I was trying to get a client’s 2019 Laravel app running on my laptop, and the app was completely fine. My machine was the problem. Four projects open, three PHP versions between them, two of them expecting MySQL 5.7 while everything else I own moved to 8 ages ago. The fan sounded like it was trying to leave the building.
Somewhere around the third docker compose up I realised I had been arguing about the wrong thing for about four years. The Laravel local dev conversation always gets framed the same way. Herd or Sail. Docker or native. “Just use Valet.” None of those are the real question.
The real question is boring: can you commit a file to this repo, or can’t you?
Everything else falls out of that one fact. Once I started asking it first, the tooling choice stopped feeling like a religious war and started feeling like arithmetic.
The four repos on my laptop, and why only one of them cooperates
Here is the actual state of my machine right now, because abstractions are useless here.
Repo one is mine. Greenfield, Laravel 12, I own the CI, I own the deploy. If I want a docker-compose.yml in the root I put one there and nobody files a ticket about it.
Repo two is a client’s internal tool. Six developers, four of them on Windows, one on a Mac so old it makes a noise when it compiles. Their CI already builds from a Dockerfile that I did not write and am not allowed to change without a change request that takes two weeks.
Repo three is a WordPress install with a Laravel app bolted onto the side of it, which is a sentence I am not proud of typing.
Repo four is a legacy PHP 7.4 thing I inherited. No composer autoload in half of it. Adding tooling files to that repo would be the least of its problems, but the client’s ops team treats the repo as production truth and gets nervous when files appear.
Only repo one is a good fit for Laravel Sail. Sail is per project Docker Compose. It is excellent when the repo is yours, because the environment lives in the repo and travels with it. New developer clones, runs ./vendor/bin/sail up, gets exactly your MySQL version. That is a genuinely good property and I don’t want to talk anyone out of it.
But Sail requires you to put things in the repo. Repos two, three and four all say no, for different reasons.
Shared stack versus per project stack
This is the axis that actually matters, and almost nobody names it.
Sail, ddev, Lando and Laradock are all per project stacks. Each project gets its own containers. Own nginx, own PHP-FPM, own MySQL, own Redis. Isolation is the feature. You pay for it in memory and in config files that have to live inside the project.
Valet, Herd and the newer Lerd are shared stacks. One nginx, one PHP-FPM pool per version, one MySQL, one Redis, and every project you link gets routed to it by a .test domain. Nothing goes in the repo. You point the tool at a directory and it works.
Here is what that difference looks like in files. Sail wants this committed:
# docker-compose.yml, in the repo, forever
services:
laravel.test:
build:
context: ./vendor/laravel/sail/runtimes/8.4
args:
WWWGROUP: '${WWWGROUP}'
ports:
- '${APP_PORT:-80}:80'
depends_on: [mysql, redis]
mysql:
image: 'mysql/mysql-server:8.0'
volumes: ['sail-mysql:/var/lib/mysql']
redis:
image: 'redis:alpine'
volumes:
sail-mysql: ~
A shared stack wants this, or nothing at all:
cd ~/Projects/that-client-thing
lerd link
lerd php 7.4
lerd secure
# https://that-client-thing.test is now live, repo untouched
Lerd will also read a .lerd.yaml if you want reproducibility, and Herd reads a herd.yml. The difference is that the config file is optional rather than load bearing. On repos two through four, “optional” is the whole ballgame.
The memory number, and how much of it I believe
Lerd’s comparison page claims roughly 200 MB with five projects running, against 1 to 2 GB for five Sail stacks. That is a vendor comparing itself to a competitor, so calibrate accordingly. I have not benchmarked it myself.
But I don’t need a benchmark to accept the direction. Five copies of MySQL is five copies of MySQL. Five PHP-FPM pools that are each idling at 60 MB is 300 MB of nothing happening. The per project model buys isolation by duplicating the parts that are identical across your projects, and on a laptop with four client repos open that duplication is most of what you are paying for.
The counterargument is real though. Shared MySQL means one MySQL version. If repo two needs 5.7 and repo one needs 8.0 at the same time, a shared stack cannot help you and Sail or Laradock can. That is the one case where I stop arguing and reach for Compose.
I wrote about the other half of this problem, the images we ship rather than the ones we develop against, in the multi stage build post. Local and production have different failure modes and I keep watching people optimise the wrong one.
Herd is very good, right up until it isn’t
Herd is the nicest developer experience in this category on macOS. Native PHP binaries, no container layer, sub second everything. If you are on a Mac and you are already in the Forge and Envoyer world, it’s an easy yes.
Two things push people off it. The first is Linux. There isn’t a Herd build for Linux, and going by the number of times that question shows up on Laracasts, that is not a niche complaint. The second is the free tier boundary. Herd is proprietary freemium, and Herd Pro is where the database inspector, the log viewer, the dump window and the per site Xdebug toggle live.
That’s a fair business model and I have no problem with people charging for software. It just means the calculation changes if you are one freelancer with fourteen client sites rather than a funded team.
Which is where Lerd came in. It is MIT licensed, written in Go as a single binary, and built on rootless Podman rather than native binaries or a Docker daemon. Laravel News covered it in August. It ships MySQL, Postgres, Redis, Meilisearch and Mailpit as shared services with no paywall, has FrankenPHP built in per site, runs queue and scheduler workers as systemd user services, and registers an MCP server so Claude Code and Cursor can talk to it.
What rootless Podman actually buys you
This is the part I found more interesting than the feature grid.
No daemon means no root level process holding your dev environment hostage. Containers run as your user. Services become systemd user units, which means systemctl --user status tells you the truth and your normal logging tools work. If you have ever had Docker Desktop wedge itself and taken down four projects at once, the appeal is obvious.
The trade is honest: it is still containers, so you don’t get Herd’s native binary speed on macOS. On Linux the gap mostly closes because the containers aren’t running inside a VM. On macOS they are. Anyone telling you container filesystem performance on macOS is a solved problem has not tried it with a large vendor/ directory recently.
The rule I use now
I stopped picking a tool and started picking based on two questions.
Can I put files in this repo? If no, shared stack. Herd on a Mac, Lerd on Linux. This is most client work, most legacy, most of what pays.
Do two projects need genuinely different service versions at the same time? If yes, per project stack, and I accept the RAM bill. Sail if it’s Laravel and mine, ddev if the team is mixed and cross platform.
That’s it. Both can live on the same machine, which is the thing I resisted for far too long because it felt like admitting I didn’t have a system. I do have a system now. The system is that two different problems get two different tools.
Most of my client work runs into the first branch, which is why I’ve been slowly moving that side of things over. You can see the kind of projects I mean on my work page if you want the specifics. Nearly all of it is code I didn’t start and don’t fully control, and tooling that assumes otherwise fights me constantly.
Something to try this week
Pick your least favourite client repo. The one where docker compose up takes ninety seconds and you have quietly stopped opening it.
Install a shared stack tool, link that directory, and time how long it takes to get an HTTPS .test URL responding. Don’t migrate anything. Don’t delete the Compose file. Just measure the two paths side by side on the same project.
If it’s a wash, you have learned something and you keep your setup. If the shared stack wins by a minute per context switch, multiply that by how many times a day you switch projects and then decide whether the isolation you’re paying for is isolation you actually use.
For me it was four repos and roughly six switches a day, and the answer stopped being close. If you’re weighing up frontend stacks on top of this, the Inertia versus Livewire post uses the same “what do you control” framing, because it turns out that question is doing a lot of work across my whole stack.