L lerd geodro/lerd v1.26.0…v1.27.0 ↗
ENGINEERING DIGEST · 2026-06-24 → 2026-07-01

v1.27.0

SERVICE PORTS · FRAMEWORK-AGNOSTIC DOCTOR · SELF-HEALING RUNTIME · SSH + CLEANUP

Service ports become something you manage rather than fight: a containerised database coexists with a host-installed server of the same engine by auto-shifting its published port, and published and extra ports are editable from the dashboard, the CLI, and MCP. The Laravel-only doctor is rebuilt framework-agnostic, lerd auth ssh lets composer reach private git repos, and lerd cleanup reclaims the podman disk rebuilds leave behind. Underneath, lerd now self-heals rootless podman after a major upgrade, recovers ghost containers and a lost nginx port after suspend, and closes the host-execution boundary against untrusted projects. This digest also folds in the 1.26.1 and 1.26.2 patch releases.

75
commits
47
merged PRs
240
files
+13.2k
insertions
−1.0k
deletions
3
contributors
cli · ports + auth + cleanup 28% serviceops + config · port layer 18% podman · seam + auto-heal 12% ui · dashboard + tui 11% sitedoctor 8% watcher · cleanup · docs · rest 23%
01

Service ports you manage

FEAT

When a DB engine's default published port (3306, 5432) is already owned by a host-installed server, lerd's container can't bind it, and the collision is worst at boot, where systemd autostarts the quadlet with no lerd process around to step aside. The port-ownership guard runs at quadlet-write time: if a host server owns the port and no override is set, lerd publishes on the next free port, persists the choice, and rewrites the connection URL. The container-internal port never changes, so containerised apps that reach the service by name are unaffected. Detection is liveness-independent, a populated data dir or a live socket counts, so a cleanly stopped host DB still reclaims its port on boot.

QUADLET WRITE → PORT GUARD → URL REWRITEruns before boot autostart
write quadlet
mysql · postgres
preset port
──►
host owns it?
data dir · socket
not just listening
──►
auto-shift
next free port
persisted
──►
rewrite URL
host-proxy .env follows
internal port unchanged
coexist, don't collide — the one case .env.lerd_override + LERD_EXTERNAL_SERVICES couldn't cover, now handled at write time so even boot autostart never fights the host server.
move or reset a published portbash
lerd service port mysql 3307     # free 3306 for a host MySQL; pre-flights, restarts, reports
lerd service port mysql --reset   # back to the preset default
FEAT

Auto-shift guard #633

Coexist with a host MySQL/MariaDB/Postgres. Reserves preset ports in the free-port search, fails closed if the override can't persist, and checks dual-stack bindability.

internal/serviceops
UI

Ports modal #688

A cog on each service page opens a Ports modal for the published host port and extra ports, through one shared serviceops layer the CLI, dashboard and MCP all call.

FIX

Presets publish their ports #708

The gate now keys off preset ownership, not default-stack membership, so optional presets like gotenberg autofill their host port and can add extra ports.

02

A framework-agnostic site doctor

FEAT

The site doctor was Laravel only. It is rebuilt as a universal baseline that runs for every site, the env file, env drift against the example, the application key, composer and node install and lockfile state, composer and npm security audits, a missing or empty SQLite database, and the PHP version range, alongside each framework's own checks declared in the lerd-frameworks store. Store checks are fully declarative through four typed evaluators, so a framework gains coverage by editing the store rather than Go, and Laravel's app_debug, storage_link and migration checks move into that model unchanged.

UNIVERSAL BASELINE + STORE CHECKSeverywhere, not gated to Laravel
baseline
env · key · deps · audit · php
every site
──►
store checks
env_key_set · env_combo · symlink · command
declarative, per framework
──►
surfaces
web · tui · cli · mcp
site:doctor · diag
──►
fix
allowlisted pm command
loopback-only, streamed
edit the store, not Go — a framework declares its doctor checks in YAML; the four typed evaluators run them for any site the framework backs.
run the doctor anywherebash
lerd site:doctor            # the site in the current dir
lerd site:doctor app.test   # another site by domain
lerd site:doctor --json     # machine-readable; also over MCP: diag site_doctor
FEAT

Universal baseline #686

Env, application key, dependency install and lockfile state, security audits, database presence and PHP range run for every site regardless of framework.

internal/sitedoctor
FEAT

Store-declared checks #686

Four typed evaluators (env_key_set, env_combo, symlink, command) mean a framework adds coverage by editing lerd-frameworks, not the binary.

FEAT

Everywhere, with fixes #686

Web button, TUI tab, lerd site:doctor --json, and MCP diag site_doctor. Dependency and audit findings expose an allowlisted, loopback-only Fix button.

03

lerd auth ssh, for composer over SSH

FEAT

Mirroring ddev auth ssh, a shared lerd-ssh-agent sidecar holds unlocked keys in memory so lerd composer can authenticate to private git repositories, including passphrase-protected keys. Keys without a passphrase already work via the bind-mounted ~/.ssh once the image has an SSH client; the agent covers the passphrase case, which needs an agent the FPM container can reach. On macOS the host agent socket can't cross the podman-machine VM boundary, so the agent runs inside podman and its socket lives on a named volume mounted into every FPM container.

AGENT SIDECAR → NAMED VOLUME → FPMsocket never crosses the host-VM boundary
lerd auth ssh
ssh-add keys
~/.ssh read-only
──►
lerd-ssh-agent
ssh-agent sidecar
reuses the FPM image
──►
named volume
agent.sock
shared into FPM
──►
composer / php
SSH_AUTH_SOCK injected
only when agent is up
no extra image — the sidecar reuses the FPM image, whose openssh-client provides ssh-agent/ssh-add; keys clear when the agent stops or the machine restarts.
load keys, then composer just worksbash
lerd auth ssh                 # default ~/.ssh/id_*
lerd auth ssh ~/.ssh/id_work  # a specific key; prompts for the passphrase once
lerd auth ssh --list          # show loaded keys  ·  --remove to clear + stop
FEAT

Shared agent sidecar #646

One lerd-ssh-agent container holds the unlocked keys; its socket on a named volume is reachable from every FPM container, macOS included.

internal/cli · auth
FEAT

Injected only when up #646

lerd composer/lerd php inject SSH_AUTH_SOCK into the exec only when the agent is running, falling back to on-disk keys otherwise.

FEAT

git + ssh in the images #645

The FPM and FrankenPHP runtime images gain git and openssh-client, so on-disk keys work and the agent has the binaries it needs.

04

lerd cleanup reclaims podman disk

FEAT

Every PHP rebuild orphans the previous :local image, every base-image hash bump strands the old base, and every service upgrade leaves the old version behind, gigabytes nothing reaps today. Unlike a blunt podman system prune -a, lerd cleanup only ever touches images that are provably lerd's, enforced structurally by a dev.lerd.* label or the base repo name, so it can never remove a database, a named volume, or another tool's images. Reported sizes count an image's unique layers only, so the figure reflects disk that is actually freed.

TierWhat it reclaimsHow it's bounded
Safe default + automaticOrphaned PHP/FrankenPHP build images, and pre-built base images nothing live is built onIn-use vs orphaned decided by layer ancestry, so the current base is always kept
Deep --deep, on demandService images no installed service references any more (e.g. an old mysql:8.0)Keeps each service's current image and its one-back rollback target
NeverDatabases, named volumes, other tools' images, any image carrying a tag you addedOwnership matched against lerd's own preset catalogue
preview, reclaim, or automatebash
lerd cleanup --dry-run       # show what would go and the size
lerd cleanup                 # preview, confirm, reclaim the safe tier
lerd cleanup --deep          # also unused service images
lerd cleanup auto off        # disable the daily watcher sweep (on by default)
FEAT

Safe by construction #647

The safe tier requires a dev.lerd.* label or the lerd-php*-fpm-base repo name; the deep tier matches lerd's preset catalogue.

internal/cleanup
FEAT

Automatic, throttled #647

lerd-watcher runs the safe tier about once a day (persisted timestamp), and a rebuild or service update reaps the exact image it just superseded.

DOCTOR

Reported in doctor #647

lerd doctor shows the reclaimable total as a read-only line when there's disk to reclaim; the destructive command stays CLI-only.

05

A self-healing runtime

FEATFIX

A podman major-version or network-backend change silently reshuffles rootless storage and networking, and containers on lerd's bridge then fail with an opaque failed to mount runtime directory for rootless netns. lerd now fingerprints the host podman version and backend, and on the next run after a change runs the remediation before bringing containers up. The fingerprint is recorded up front so a mid-heal failure can't re-run the destructive steps every invocation.

UPGRADE HEALlinux · fires once per major bump
1
Detect the change

Compare the recorded podman version + backend in podman-env.json against the live one (server version via podman info, so a client-only brew upgrade can't false-trigger). #658

2
Rebuild the bridge

Recreate the lerd network in the new backend's format (CNI is removed and not auto-migrated in 5.0); stopped containers are handed back for the caller to restart.

3
Migrate & clear

podman system migrate, then clear the stale rootless-netns. lerd doctor also checks for the netavark, aardvark-dns and pasta/slirp4netns helpers a fresh host can lack.

FIX

Ghost containers #699

An unclean Machine shutdown leaves a container stuck Created with no storage layer; lerd purges the stuck lerd-* ones inside the VM and retries once (darwin-only, status=created).

FIX

nginx after resume #669

The DNS watcher dials 443 and, after two consecutive down reads on a server it has seen healthy, restarts lerd-nginx so 443 binds again after a long suspend.

FEAT

FPM auto-start #652

php, artisan, tinker and shell start a stopped FPM container, or offer to install a missing version; over MCP they return a clean error instead of a raw podman failure.

REFACTOR

One podman seam #631

Roughly 145 call sites funnel through Cmd/Run/CmdContext in internal/podman, with a guard against building a podman exec outside the package.

REFACTOR

Service YAML is truth #679

Custom-service install state resolves from the definition YAML; a lerd start pass regenerates a missing unit and reaps an orphan quadlet with no backing YAML.

FEAT

Install & build preflights #628

lerd install warns about a foreign listener on 80/443/5300 before setup, and the doctor gains rootless podman build preflight checks #648.

06

Idle-suspend, macOS & LAN polish

FIX

A wave of fixes to shipped behaviour: a PHP site keeps serving while idle-suspended because FPM stays up, but a host-proxy site has no FPM, so stopping its dev server leaves nginx pointing at a dead port. Suspend now swaps the vhost to an auto-refreshing waking page and restores it once the dev server is back, and the rest of the wave sharpens idle-suspend, macOS and LAN-share.

FIX

Waking page, not 502 #673

A suspended host-proxy site serves an auto-refreshing landing page; resume waits (bounded 60s) for the dev-server port before restoring the proxy vhost.

FIX

Pause on worker stop #670

Stopping a host-proxy site's dev-server worker pauses the site rather than leaving nginx to 502.

FIX

macOS idle wake & reap #681

Suspended workers wake on web access on macOS #681, and the in-container worker is reaped on stop so idle-suspend actually frees CPU #662.

FIX

Postgres native on ARM #676

Postgres runs native on Apple Silicon via an imresamu/postgis rewrite instead of emulation.

FIX

LAN-share over HTTPS #653

LAN-shared sites that force HTTPS no longer hit ERR_SSL_PROTOCOL_ERROR #653, and the proxy carries app WebSockets while only diverting Vite's HMR socket #657.

FIX

Watcher fd budget #675

Source-file watches are bounded so an asset dump can't exhaust file descriptors; the idle moon now shows in real time on resume #703.

07

Consent at the host boundary

SECURITY

An untrusted .lerd.yaml could still run commands on the host with no consent, through host: true workers, an embedded framework_def, and commands run via lerd run, the dashboard, and the commands_run MCP tool. An imported framework_def is now stripped of its host workers and its whole commands list, so the store never holds project-origin host execution, and a project's own host extensions still work, with consent.

PROJECT-ORIGIN → GATE → REMEMBEREDtrusted store / built-in / overlay unaffected
untrusted .lerd.yaml
host worker · command
tagged project-origin
──►
host boundary
show exact command
ask once
──►
remembered per site
registry
boot restore won't re-prompt
self-healing — a store yaml imported by an older build may still hold a poisoned framework_def; detection rewrites the sanitised def on the next run. host_commands.disabled / skip_confirmation mirror the host-proxy knobs.
SEC

framework_def sanitised #697

An embedded framework_def loses its host workers and commands on import, the same way its command doctor checks were already stripped.

SEC

Gated at the boundary #697

Starting a host custom worker or running a project command shows it and asks once; --yes, the confirm modal, and force: true carry that consent.

SAFE

Trusted paths untouched #697

Trusted store, built-in and user-overlay definitions, in-container workers, and framework-provided commands are unaffected.

08

Shipped along the way: 1.26.1 & 1.26.2

FIX

Between digests, two patch releases went out. Both centred on a silent fallback that made most Linux hosts rebuild every PHP image from source instead of pulling the published prebuilt base, plus cherry-picked macOS and feedback fixes.

1.26.2

Prebuilt base on all podman #638

1.26.1 gated the pull's --policy=always behind podman 5.0, but --policy landed later still; the flag is dropped entirely (pull already defaults to always) and the real pull error surfaces.

1.26.1

Silent build fallback #638

podman pull --policy was rejected on podman below 5.0, its stderr discarded, so Ubuntu/Debian hosts quietly built from source; the failure now surfaces.

1.26.1

Setup reports build failures #622

lerd setup returns npm/bun build failures through the feedback step rather than calling os.Exit, so the failure shows in context.

1.26.1

macOS LAN exposure #618

Bind-all IPv6 publish lines are rewritten to 0.0.0.0 so lan.exposed containers keep their ports #618, and the LAN DNS forwarder is skipped where dnsmasq already binds the LAN address #619.

1.26.1

Host worker remedies #616

Host workers whose dependencies aren't installed surface a remedy instead of dropping silently, and lerd update's service restarts warn rather than error.

DOCS

Host DB override recipe #626

The host database override example uses host.containers.internal.

09

Docs & platforms

DOCS

NixOS gets a first-class path: its declarative model doesn't fit the one-line installer's imperative DNS and self-install steps, so lerd ships a flake that packages the binary and the configuration.nix blocks the stack needs. The MCP reference is also brought back in line with the current tool surface.

run lerd on NixOSbash
nix run github:lerd-env/lerd-nixos -- --help   # the flake packages the binary + config blocks
DOCS

lerd-nixos flake #710

A documented flake and a NixOS guide for the declarative stack, since the imperative installer's DNS and self-install steps don't fit NixOS.

DOCS

MCP reference synced #706

The MCP reference is brought back in step with the current tool surface, including the new diag site_doctor and service-port actions.