Framework Workers
May 18, 2026 ยท View on GitHub
Every framework can define long-running workers (queue consumers, schedulers, WebSocket servers). This page covers the worker commands, conditional rules, conflicts, proxy wiring, project-specific custom workers, and orphan cleanup.
Each framework can define workers: long-running processes managed as systemd user services inside the PHP-FPM container.
| Command | Description |
|---|---|
lerd worker start <name> | Start a named worker for the current project |
lerd worker stop <name> | Stop a named worker |
lerd worker list | List all workers defined for this project's framework |
The shortcut commands lerd queue:start, lerd schedule:start, lerd reverb:start, and lerd horizon:start are aliases; they look up the worker from the framework definition and delegate to the generic handler. They work for any framework that defines a worker with that name.
Worker features
Conditional workers: Workers with a check rule only appear when the condition passes (e.g. laravel/horizon is in composer.json):
workers:
horizon:
command: php artisan horizon
check:
composer: laravel/horizon
Conflict resolution: Workers can declare conflicts. When a conflicting worker starts, the other is stopped automatically and hidden from the UI:
workers:
horizon:
command: php artisan horizon
conflicts_with:
- queue # stops queue before starting horizon; hides queue toggle in UI
WebSocket/HTTP proxy: Workers that need an nginx proxy block define a proxy config. Lerd auto-assigns a collision-free port and regenerates the nginx vhost:
workers:
reverb:
command: php artisan reverb:start
proxy:
path: /app # URL path for the proxy location block
port_env_key: REVERB_SERVER_PORT # env key holding the port
default_port: 8080 # starting port for auto-assignment
Port assignment scans all proxy port env keys across all sites to prevent collisions between different workers and frameworks.
Host workers: Workers that need to run on the host instead of inside the PHP-FPM container set host: true. The command runs via fnm at the project's pinned Node.js version. This is used for tools like Vite that need direct filesystem access for HMR:
workers:
vite:
label: Vite
command: npm run dev
restart: on-failure
host: true
check:
file: vite.config.js
The command is wrapped in /bin/sh -c so shell features (&&, |, env-var expansion, redirects) work as written. A composite command like npm run build && npm run preview runs end-to-end without quoting tricks.
Host workers auto-start in three places:
- when a worktree is created, with per-worktree units (
lerd-vite-<site>-<branch>, supervised by systemd on Linux and launchd on macOS) so multiple Vite instances can run simultaneously with auto-incremented ports. - at daemon boot, so worktree units recover after a host reboot or
lerd stop && lerd starteven when fsnotify hasn't fired. - on
lerd worktree remove, the matching unit is stopped and its file removed; without this the unit would restart-loop against the deletedWorkingDirectory.
Host workers run with lerd's bin dir prepended to PATH, so subprocesses spawned by npm run dev (for example Inertia's wayfinder Vite plugin shelling out to php artisan) reach lerd's php, composer and laravel shims and route into the containerised runtime. Stopping a host worker via the UI or lerd worker stop is now sticky: a HEAD-write event (commit, checkout, rebase, branch rename) inside a worktree no longer resurrects it, and on macOS the heal loop respects a missing plist as a user-stop signal instead of recreating it.
On macOS the unit is a launchd plist (~/Library/LaunchAgents/lerd-<worker>-<site>[-<branch>].plist) backed by a guard script under ~/.local/share/lerd/run/workers/ that cds into the site/worktree and fnm execs the command. The watcher self-heals the unit independently of the worker exec mode โ host workers always need launchd-level supervision because they aren't behind podman's --restart=always. Scheduled workers (schedule != "") still aren't supported on macOS; launchd's StartCalendarInterval isn't wired through the unit translator yet.
Project-specific custom workers
Add workers to .lerd.yaml for project-specific needs that don't belong in the framework definition:
# .lerd.yaml
framework: symfony
framework_version: "8"
workers:
- messenger
- pdf-generator
custom_workers:
pdf-generator:
label: PDF Generator
command: php bin/console app:generate-pdfs --daemon
restart: always
Custom workers with proxy support:
custom_workers:
mercure:
label: Mercure Hub
command: php bin/console mercure:run
restart: always
proxy:
path: /.well-known/mercure
port_env_key: MERCURE_PORT
default_port: 3000
Custom workers are merged with the framework's workers at runtime. They are committed to git so teammates get the same setup.
Worker logs
journalctl --user -u lerd-messenger-myapp -f
Managing custom workers
Use lerd worker add to add project-specific or global custom workers without manually editing YAML:
# Add a project-specific worker (saved to .lerd.yaml)
lerd worker add pulse --command "php artisan pulse:work" --label "Pulse" --check-composer laravel/pulse
# Add a worker that conflicts with another (stops it on start, hides it in UI)
lerd worker add custom-queue --command "php artisan queue:work --queue=emails" --conflicts-with queue
# Add a global worker (saved to ~/.config/lerd/frameworks/<name>.yaml)
lerd worker add pulse --command "php artisan pulse:work" --global
# Remove a custom worker (stops it if running)
lerd worker remove pulse
lerd worker remove pulse --global
Project workers (.lerd.yaml) apply to a single project and are committed to git. Global workers (user overlay) apply to all projects using that framework. Both survive framework store updates.
The resulting .lerd.yaml looks like:
framework: laravel
custom_workers:
pulse:
label: Pulse
command: php artisan pulse:work
check:
composer: laravel/pulse
custom-queue:
command: php artisan queue:work --queue=emails
conflicts_with:
- queue
After adding, start the worker with lerd worker start pulse.
When running lerd init --fresh, existing custom workers are shown in a multi-select step before the workers step. Deselecting a custom worker removes it from .lerd.yaml and excludes it from the workers selection. If the removed worker had conflicts_with, those workers become available again.
Orphaned workers
A worker becomes orphaned when its systemd unit is still running but its definition has been removed from .lerd.yaml (e.g. after a git pull or manual edit). Orphaned workers are detected and surfaced in several places:
lerd worker list: shows orphaned workers with a stop hintlerd worker stop <name>: can stop orphaned workers even without a definitionlerd setup: offers orphaned workers as pre-selected stop steps before framework worker starts- UI: the stop button works for orphaned workers directly
Web UI (worker toggles)
Framework workers appear as toggles in the Sites panel. Workers with a check rule only appear when the condition passes. Workers with conflicts_with suppress each other (e.g. when Horizon is available, the queue toggle is hidden).
Custom framework workers from .lerd.yaml also appear as toggles alongside the framework's standard workers.
See also: Frameworks for the framework store and Laravel definition; Framework definitions for the YAML schema.