L lerd lerd-env/lerd v1.27.1…v1.28.0 ↗
ENGINEERING DIGEST · 2026-07-01 → 2026-07-08

v1.28.0

TIMING, DIAGNOSED · Every site tells you where its time goes

The per-request signal lerd already carried, once starved down to a bare hostname for idle-suspend, becomes a full request-timing analytics view for every site, with slow-route detection, cold-start exclusion, and one-click profiling. Service presets follow frameworks out of the binary into an external store that updates without a release. And the whole thing joins up for agents: route_timing finds the slow route, optimize_route hands back the N+1 queries and CPU hotspots behind it, from real traffic rather than reading code. Alongside, certificates renew themselves, DNS toggles through dedicated commands, database client tools become host shims, and the dashboard grows a QR code and a mobile session.

79
commits
45
merged PRs
373
files
+19,045
insertions
−2,075
deletions
2
contributors
web UI 29% cli 16% request timing 8% services + shims 7% config 8% rest 32%
01

Request Timing

FEAT

The nginx access feed was widened from the bare request host it fed idle-suspend to carry status, request time, method and URI, and a framework-agnostic aggregator in the watcher keeps a rolling per-site, per-route window off that same syslog feed, running whether or not idle-suspend is on. The site Overview gains a Request timing section backed by a durable SQLite record: a range picker, KPIs for the typical and p95 response, request count and error rate, a response-time distribution, a throughput chart, the slowest routes, and routes and recent-requests panels.

SYSLOG → SNAPSHOT → SIGNALone feed, three consumers
nginx access
status · time · method · uri
query strings dropped
──►
aggregator
per-route window
id-like segments collapsed
──►
p95 vs typical
flag a slow route
min sample count first
──►
reaches you
panel · doctor · push
no capture needed
Grouped, not raw — routes aggregate after id-like path segments are collapsed and query strings dropped, so no query-string value is ever kept in memory and traffic aggregates meaningfully.
FEAT

Analytics on the Overview #797

A durable store drives the range picker, the typical and p95 KPIs, the distribution, a live throughput series, and the slowest routes with click-to-profile.

internal/reqstats
FEAT

Cold starts don't skew it #797

The first request after a site sits idle past the suspend timeout is recorded with a cold flag and kept out of every timing figure, while still counting toward the total and showing in the recent list with a marker.

FEAT

The finding follows you #724

A slow route surfaces as a Response Time warning in the site doctor that the CLI, dashboard, and MCP all inherit, and as an opt-in slow_route push that fires once and rearms when the route recovers.

02

AI Diagnosis

FEAT

The timing signal is only half the story. An agent asking "why is this route slow?" wants the symptom and its cause in one call, without reading controllers to guess at N+1s. route_timing returns the response-time table from real traffic, and optimize_route joins each slow route to the queries and the CPU behind it. A new spxreport package parses SPX's on-disk trace into a flat profile and keeps only the outliers, so a CPU-bound route shows where its time went next to its queries.

OPTIMIZE-ROUTE JOINsymptom + both causes
1
route_timing

the per-site response-time table, the routes whose p95 runs well above the site's typical, read from the snapshot with no capture needed #726

2
enable capture, hit the route

dumps_toggle for queries and profiler_toggle for SPX, then a few real hits, an authenticated POST just works, no synthetic request to forge

3
optimize_route

each slow route paired with its N+1 and slow queries at file:line, plus a profile block of the top functions by exclusive wall time from the freshest capture #728

The same distilled hotspots that lerd profile run --flat and profiler_report already return for a command, now in a route-scoped, historical form from passive traffic.
drive it from real traffic, not from reading codemcp
route_timing(site: "shop")              # which routes are slow
dumps_toggle(enable) · profiler_toggle(enable)
# … hit GET /orders a few times …
optimize_route(site: "shop", route: "/orders")
  → N+1: OrderController@index:42  eager-load `items`
  → CPU: CurrencyFormatter::format  61% excl. wall
03

Service Store

FEATSTORE

Service presets adopt the same fetch-and-cache model frameworks already use. The binary now embeds only the default stack, mysql, redis, postgres, meilisearch, rustfs, mailpit, and every add-on lives in the lerd-env/services store, fetched on demand into a local cache, refreshed after 24 hours, with the embedded defaults kept as an offline fallback so older installs keep resolving. A new service ships to every install without a binary release, which is the whole point.

FEAT

Search and install from the store #714

lerd service search browses the store, lerd service preset <name> fetches a store-only preset on demand, and a reconcile backfill on start pulls the definitions of already-installed add-ons so their file mounts, families and dashboards survive the move out of the binary.

  • YAML config-file mounts, inline or via a named generator
  • keeps phpMyAdmin, pgAdmin, RedisInsight and RabbitMQ dashboards working from the store
internal/serviceops · internal/store
FEATUI

Picker search + palette install #720

The preset picker filters live by name, description or image with an empty state, and every installable preset is promoted into the global command palette under an Install service group, so "install redis" installs it without opening the picker. Service tools render as cards #765.

04

Client Shims

FEAT

The client tools that ship inside each database service image, mysqldump, pg_dump, psql, redis-cli, mongodump and friends, are surfaced as pass-through shims on the host PATH. Dump or query a database that lives outside lerd, or point PhpStorm at a real client binary, without installing the tool yourself. Each shim runs the tool in a throwaway container built from the service image with your home directory mounted, so output files and CA certs behave like a native binary and the running database container is never touched.

pg_dump ON THE HOST PATHno local client install
you type
pg_dump mydb
host shell
──►
shim
adopt project DB
read from DB_HOST
──►
throwaway container
service image + $HOME
files land natively
Hostless adopts the project's own service — a mariadb or postgres-18 backed project routes to its own server; an explicit host passes straight through for external databases. When several same-family services expose a tool, one owns the shim and the others show it disabled.
list, toggle, and a Tools tab per database servicebash
lerd shims                    # list every exposed client tool
lerd shims add psql          # put psql on the host PATH
pg_dump mydb > dump.sql      # runs in a throwaway container
05

TLS & DNS

FEAT

Two long-standing sharp edges, rounded off. A secured site used to serve its mkcert leaf until it expired and then keep serving the expired one, and switching lerd-managed DNS on or off meant re-running the whole installer. Certificates now renew themselves, and DNS gets three dedicated commands.

FEAT

Certificates renew before they expire #741

IssueCert is now expiry-aware: it keeps an existing certificate only while it parses, is valid, and sits more than thirty days from expiry, otherwise it reissues. The watcher boot scan runs a new EnsureCert for secured sites so an ordinary lerd start heals an aging cert, covering the site's own domains and every worktree wildcard SAN.

  • lerd secure --renew and MCP tls_renew for an on-demand reissue
internal/certs
FEAT

DNS switches through commands #746

The install-time DNS question is asked once, on a genuine first install, and every later install or update honours the saved choice silently, the same way the Node-management choice does.

  • dns:enable turns lerd-managed DNS on
  • dns:disable moves sites to localhost
  • dns:repair re-runs setup to fix a broken but enabled resolver
06

Ports & Env

FEAT

Two power-user gaps closed. Nothing you started on a raw TCP port inside lerd shell used to be reachable from the host, and filling in a project's missing .env keys meant hunting for where each one belonged.

FEAT

Extra host ports per PHP version #755

Each PHP version carries its own list of extra host ports, published on that version's shared FPM container so localhost:PORT reaches a Vite dev server, a websocket, or an ad-hoc listener you start in the shell. A requested port already claimed shifts to the next free one, and changing a version's list restarts only that version's FPM.

  • a Ports tab per version, lerd php:ports add|remove|list
  • MCP runtime ports_list / ports_add / ports_remove
FEATUI

Insert missing .env keys in place #718

An anchor-based merge places each key the project's .env is missing next to the neighbours it has in .env.example, carrying its inline comment and value verbatim, purely additively. lerd env:check --fix shows a unified diff; the dashboard Env tab opens a Review keys modal #735 listing each missing key with its value and a checkbox, required keys pre-ticked.

07

Disk Cleanup

FEAT

Cleanup only reaped provably-lerd orphaned images by default, so upgraded service versions and the dangling leftovers repeated rebuilds pile up went untouched unless someone remembered --deep. On a long-lived install that let podman grow tens of gigabytes while both lerd cleanup and the daily sweep reported nothing to do. Deep is now the default #740, with a --safe flag to drop back to the conservative pass.

DEFAULT NOW RECLAIMS--safe drops back to conservative
reap
catalog service images
nothing references them
+
reap
every dangling image
untagged, unreferenced
+
retry across passes
build chains
reclaim in one run
·
skip
held by a container
podman refuses anyway
Held by a running container? Cleanup reports how much a restart would free instead of a bare nothing to do, because recreating the containers on the current images releases the old ones it was holding.
08

Mobile & UI

UIFEAT

Nearly a third of the release lands in the web UI. The dashboard learns to be driven from a phone over a shared LAN, the site page is rebuilt as a resource view, and every tooltip is rewritten to escape the panels that used to clip it.

FEAT

Remote address + mobile session #783

The Remote dashboard card shows the http://<lan-ip>:7073 address next to a scannable QR. A first Basic-auth success sets an HttpOnly, HMAC-signed lerd_session cookie valid for a week, so iOS Safari stops re-prompting on every refresh; rotating the credentials invalidates every session.

UI

Phone polish #785

Toolbars wrap, the framework badge hides on narrow screens, the open action targets the LAN share URL where .test won't resolve, Tinker no longer zooms on iOS focus, and loopback-only launchers drop out of a remote dashboard.

UI

Resource-view site page #794

The Overview becomes a stack of sections, a runtime-and-workers row with the doctor and framework commands as a matched pair, a service-card grid where each running service opens its dashboard, and live logs on their own tab. The project path opens the folder in the host file manager.

UI

Themed hover tooltips #781

A shared use:tooltip action renders one themed bubble portaled to the body, so tooltips escape the panels and scroll containers that clipped the native title labels, follow the light or dark theme, and clamp to the viewport. Every trigger keeps an aria-label.

FEAT

queue:start from the framework #769

A tune_command template with {queue}/{tries}/{timeout} and a restart_command on the framework worker replace the hardcoded php artisan queue:work, so CodeIgniter's php spark queue:work and any non-Laravel framework declare their own flag syntax. Detection also resolves offline from the cached store index #776.

09

Fixes & Docs

FIXCHORE

Fixes that landed against previously shipped code, plus the org move. Correctness fixes to features born in this cycle are folded into their chapters above, not listed here.

FixWhat changedPR
AI config pollutionmcp:inject stops writing lerd's block into a shared, committed AGENTS.md or guidelines file that belongs to the project#795
Install idempotencythe DNS sudoers rule is no longer rewritten on every install, prompts stop swallowing typed-ahead input, and podman-mac-helper's sudo prompt is skipped when already installed#731 #759 #761
Host-worker consentkeyed on the command that actually runs, so the v1.27 gate remembers the approval instead of re-prompting#772
MCP + UI feedbackpodman hot-path calls are bounded and a stalled machine self-heals; the check-for-updates button gives live feedback#716 #787
macOS healthe machine heal, DNS teardown, and CA trust check are hardened#791
v1.27 reviewcorrectness, macOS-compatibility and safety follow-ups from reviewing the v1.27 changes#763 #773 #799 #802

Under the hood, distribution and CI moved from geodro/lerd to the lerd-env organisation #757, Go dependencies were updated including the x/net DoS fix #767, and the repo gained a CLAUDE.md agent guide with contributor-agent skills #722.