Lerd
Laravel Herd for Linux — a Podman-native local development environment for Laravel projects.
Lerd bundles Nginx, PHP-FPM, and optional services (MySQL, Redis, PostgreSQL, Meilisearch, MinIO) as rootless Podman containers, giving you automatic .test domain routing, per-project PHP/Node version isolation, and one-command TLS — all without touching your system's PHP or web server.
Requirements
- Linux (Arch, Debian/Ubuntu, or Fedora-based)
- Podman (rootless, with systemd user session active)
- NetworkManager (for
.test DNS)
systemctl --user functional (loginctl enable-linger $USER if needed)
unzip (used during install to extract fnm)
Go is only needed to build from source. The released binary has no runtime dependencies.
Installation
One-line installer (recommended)
With curl:
curl -fsSL https://raw.githubusercontent.com/geodro/lerd/main/install.sh | bash
With wget:
wget -qO- https://raw.githubusercontent.com/geodro/lerd/main/install.sh | bash
This will:
- Check and offer to install missing prerequisites (Podman, NetworkManager, unzip)
- Download the latest
lerd binary for your architecture (amd64 / arm64)
- Install it to
~/.local/bin/lerd
- Add
~/.local/bin to your shell's PATH (bash, zsh, or fish)
- Automatically run
lerd install to complete environment setup
DNS setup: lerd install writes to /etc/NetworkManager/dnsmasq.d/ and /etc/NetworkManager/conf.d/ and restarts NetworkManager. This is the only step that requires sudo.
After install, reload your shell or open a new terminal so PATH takes effect.
Install from a local build
If you built from source and want to skip the GitHub download:
make build
bash install.sh --local ./build/lerd
Update
lerd update
Fetches the latest release from GitHub, downloads the binary for your architecture, and atomically replaces the running binary. No restart needed.
You can also re-run the installer:
curl -fsSL https://raw.githubusercontent.com/geodro/lerd/main/install.sh | bash -s -- --update
wget -qO- https://raw.githubusercontent.com/geodro/lerd/main/install.sh | bash -s -- --update
Uninstall
lerd uninstall
Stops all containers, disables and removes Quadlet units, removes the watcher service, removes the binary, and cleans up the PATH entry from your shell config. Prompts before deleting config and data directories.
To skip all prompts:
lerd uninstall --force
Check prerequisites only
bash install.sh --check
From source
git clone https://github.com/geodro/lerd
cd lerd
make build
make install # installs to ~/.local/bin/lerd
make install-installer # installs lerd-installer to ~/.local/bin/
lerd install will:
- Create XDG config and data directories
- Create the
lerd Podman network
- Download static binaries: Composer, fnm, mkcert
- Install the mkcert CA into your system trust store
- Write and start the
lerd-dns and lerd-nginx Podman Quadlet containers
- Enable the
lerd-watcher background service (auto-discovers new projects)
- Add
~/.local/share/lerd/bin to your shell's PATH
Quick start
# 1. Park your projects directory — any Laravel project inside is auto-registered
lerd park ~/Lerd
# 2. Visit your project in a browser
# ~/Lerd/my-app → http://my-app.test
# 3. Check everything is running
lerd status
That's it. Nginx is serving your project through PHP-FPM, all inside Podman containers on the lerd network.
Commands
Setup & lifecycle
| Command |
Description |
lerd install |
One-time setup: directories, network, binaries, DNS, nginx, watcher |
lerd update |
Update to the latest release |
lerd uninstall |
Stop all containers and remove Lerd |
lerd uninstall --force |
Same, skipping all confirmation prompts |
lerd dns:check |
Verify that *.test resolves to 127.0.0.1 |
lerd status |
Health summary: DNS, nginx, PHP-FPM containers, services, cert expiry |
Site management
| Command |
Description |
lerd park [dir] |
Register all Laravel projects inside dir (defaults to cwd) |
lerd link [name] |
Register the current directory as a site |
lerd link [name] --domain foo.test |
Register with a custom domain |
lerd unlink |
Remove the current directory from Lerd |
lerd sites |
Table view of all registered sites |
lerd secure [name] |
Issue a mkcert TLS cert and enable HTTPS for a site |
PHP
| Command |
Description |
lerd use <version> |
Set the global PHP version (e.g. lerd use 8.3) |
lerd isolate <version> |
Pin PHP version for cwd — writes .php-version |
lerd php:list |
List all downloaded static PHP binaries |
Node
| Command |
Description |
lerd isolate:node <version> |
Pin Node version for cwd — writes .node-version, runs fnm install |
Services
| Command |
Description |
lerd service start <name> |
Start a service container |
lerd service stop <name> |
Stop a service container |
lerd service restart <name> |
Restart a service container |
lerd service status <name> |
Show systemd unit status |
lerd service list |
Show all services and their current state |
Available services: mysql, redis, postgres, meilisearch, minio.
Shell completion
lerd completion bash # add to ~/.bashrc
lerd completion zsh # add to ~/.zshrc
lerd completion fish # add to ~/.config/fish/completions/lerd.fish
PHP version resolution
When serving a request, Lerd picks the PHP version for a project in this order:
.php-version file in the project root (plain text, e.g. 8.2)
.lerd.yaml in the project root — php_version field
composer.json — require.php constraint (e.g. ^8.2 → 8.2)
- Global default in
~/.config/lerd/config.yaml
To pin a project permanently:
cd ~/Lerd/my-app
lerd isolate 8.2
# writes .php-version: 8.2 — commit this if you like
To change the global default:
lerd use 8.4
Node version resolution
.nvmrc in the project root
.node-version in the project root
package.json — engines.node field
- Global default in
~/.config/lerd/config.yaml
To pin a project:
cd ~/Lerd/my-app
lerd isolate:node 20
# writes .node-version and runs: fnm install 20
HTTPS / TLS
Lerd uses mkcert — a locally-trusted CA that your browser will accept without warnings.
cd ~/Lerd/my-app
lerd secure
# Issues a cert for my-app.test, regenerates the SSL vhost, reloads nginx
# Visit https://my-app.test — no certificate warning
Certificates are stored in ~/.local/share/lerd/certs/sites/.
Configuration
Global config — ~/.config/lerd/config.yaml
Created automatically on first run with sensible defaults:
php:
default_version: "8.4"
node:
default_version: "22"
nginx:
http_port: 80
https_port: 443
dns:
tld: "test"
parked_directories:
- ~/Lerd
services:
mysql: { enabled: true, image: "mysql:8.0", port: 3306 }
redis: { enabled: true, image: "redis:7-alpine", port: 6379 }
postgres: { enabled: false, image: "postgres:16-alpine", port: 5432 }
meilisearch: { enabled: false, image: "getmeili/meilisearch:v1.7", port: 7700 }
minio: { enabled: false, image: "minio/minio:latest", port: 9000 }
Per-project config — .lerd.yaml
Optional file in a project root to override site settings:
php_version: "8.2"
node_version: "18"
domain: "my-app.test" # override the auto-generated domain
secure: true
Directory layout
~/.config/lerd/
└── config.yaml
~/.config/containers/systemd/ # Podman Quadlet units (auto-loaded)
~/.config/systemd/user/
└── lerd-watcher.service
~/.local/share/lerd/
├── bin/ # mkcert, fnm, static PHP binaries
├── nginx/
│ ├── nginx.conf
│ ├── conf.d/ # one .conf per site (auto-generated)
│ └── logs/
├── certs/
│ ├── ca/
│ └── sites/ # per-domain .crt + .key
├── data/ # Podman volume bind-mounts
│ ├── mysql/
│ ├── redis/
│ ├── postgres/
│ ├── meilisearch/
│ └── minio/
├── dnsmasq/
│ └── lerd.conf
└── sites.yaml
Architecture
All containers join the rootless Podman network lerd. Communication between Nginx and PHP-FPM uses container names as hostnames:
Browser → 127.0.0.1:80 → lerd-nginx
└─ fastcgi_pass lerd-php84-fpm:9000
└─ lerd-php84-fpm (mounts ~/Lerd read-only)
*.test → DNS → 127.0.0.1
└─ lerd-dns (dnsmasq, host network, port 5300)
← NetworkManager forwards .test queries here
| Component |
Technology |
| CLI |
Go + Cobra, single static binary |
| Web server |
Podman Quadlet — nginx:alpine |
| PHP-FPM |
Podman Quadlet per version — php:X.Y-fpm-alpine |
| PHP CLI |
Static binary from static-php.dev |
| Composer |
composer.phar via bundled PHP CLI |
| Node |
fnm binary, version per project |
| Services |
Podman Quadlet containers |
| DNS |
dnsmasq container + NetworkManager integration |
| TLS |
mkcert — locally trusted CA |
Building
make build # → ./build/lerd
make install # → ~/.local/bin/lerd
make test # go test ./...
make clean # remove ./build/
Cross-compile for arm64:
GOARCH=arm64 GOOS=linux go build -o ./build/lerd-arm64 ./cmd/lerd
Service credentials (defaults)
| Service |
Host |
Port |
User |
Password |
DB |
| MySQL |
127.0.0.1 |
3306 |
root |
lerd |
lerd |
| PostgreSQL |
127.0.0.1 |
5432 |
postgres |
lerd |
lerd |
| Redis |
127.0.0.1 |
6379 |
— |
— |
— |
| Meilisearch |
127.0.0.1 |
7700 |
— |
— |
— |
| MinIO |
127.0.0.1 |
9000 |
lerd |
lerdpassword |
— |
MinIO console is available at http://127.0.0.1:9001.
Troubleshooting
.test domains not resolving
lerd dns:check
# If it fails:
sudo systemctl restart NetworkManager
lerd dns:check
Nginx not serving a site
lerd status # check nginx and FPM are running
podman logs lerd-nginx # nginx error log
cat ~/.local/share/lerd/nginx/conf.d/my-app.test.conf # check generated vhost
PHP-FPM container not running
systemctl --user status lerd-php84-fpm
systemctl --user start lerd-php84-fpm
podman logs lerd-php84-fpm
Permission denied on port 80/443
Rootless Podman cannot bind to ports below 1024 by default. Allow it:
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
# Make permanent:
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-lerd.conf
Watcher service not running
systemctl --user status lerd-watcher
systemctl --user start lerd-watcher