Skip to content

Web UI ​

Lerd includes a browser dashboard available at http://lerd.localhost, served by the lerd-ui systemd service (started automatically with lerd install).

bash
lerd dashboard   # open the dashboard in its own window

The .localhost TLD resolves to 127.0.0.1 natively on all modern systems, no DNS configuration needed. The dashboard is also reachable directly at http://127.0.0.1:7073 if nginx is not running.

Real-time updates ​

The dashboard opens a single WebSocket to /api/ws on load and receives state changes as they happen. No polling, no stale panels. Every surface that mutates lerd state (browser actions, lerd CLI commands, the MCP server, the file watcher) pushes a fresh snapshot to every connected tab within about 200 ms. If the WebSocket ever drops (e.g. lerd-ui restart), the dashboard falls back to a 5 s polling loop and reconnects in the background with exponential backoff, so a restart is transparent. Each status payload carries the id of the process that answered, so a dashboard that reconnects to a restarted lerd-ui reloads itself onto the assets that server now ships rather than running the previous build's page against it.

Behind the scenes a background container poll runs every 15 s when at least one tab is visible and the desktop session is active, and drops to 60 s otherwise (every tab hidden, or the session reported idle or locked by systemd-logind). Battery-aware: a focused tab on a locked laptop still falls back to the slow cadence.

Install as an app ​

The dashboard is a Progressive Web App (PWA). You can install it as a standalone desktop app from any Chromium-based browser (Chrome, Brave, Edge):

  1. Open http://lerd.localhost
  2. Click the install icon (⊕) in the address bar
  3. Click Install

Once installed, Lerd opens in its own window, with Chromium's slim app title bar above it.

On Linux you don't need to install anything for a window of its own: when the Lerd desktop app isn't installed, lerd dashboard and the tray's Open Dashboard open the dashboard as a Chromium --app window, which has no tabs, no address bar and no browser title bar, the same way Omarchy launches its web apps. Your default browser is used if it's Chromium-based, otherwise the first of Chromium, Chrome, Brave, Edge, Vivaldi or Helium that is installed, native or flatpak. With none of them installed, or on GNOME, the dashboard opens in a normal browser tab: GNOME has every window draw its own title bar, and Chromium draws a fixed grey one for app windows that ignores the dashboard's colours. On Hyprland (Omarchy included) and KDE Plasma, running it again brings the open window to the front instead of opening a second one. lerd also leaves a hidden entry in ~/.local/share/applications named after the window, so the taskbar shows the Lerd icon; lerd uninstall removes it.

lerd dashboard, the tray's Open Dashboard and the macOS Lerd app all start lerd first when nothing is serving it, so opening the dashboard on a stopped environment brings it up rather than landing on a page whose only content is a button to press. The command shows the same spinner lerd start does; the macOS app shows a native progress window. A dashboard already open in a browser tab is the one case that cannot shell out; that is what the Start when the dashboard opens setting below covers.

A service worker ships with the dashboard, so when lerd is stopped (including via lerd quit) or restarting, the installed PWA shows a small offline landing page instead of the usual browser "this site can't be reached" error. The page leads with what you can click: a Start Lerd button, which starts the stack from there and streams the same per-unit progress the dashboard shows before reloading into it, and Retry now. The lerd start command and the tray hint follow underneath as the terminal fallback. Start Lerd appears only while the UI daemon still answers on its own port, which is the case after lerd stop (nginx is a container that stops with the stack; the daemon is a host process that does not) and not after lerd quit, where nothing is left listening to be asked. The page re-probes the vhost every 5s and reloads as soon as it serves again, so a start from the CLI or the tray also lands you back on the dashboard.

http://127.0.0.1:7073 is the daemon's own port and exists as a fallback for a stopped stack, not as a second home for the dashboard. A loopback session there moves itself to http://lerd.localhost as soon as nginx answers, and while it sits on the fallback it will not ask for notification permission or register a push subscription: both are per-origin, so settling on the port would leave two browsers subscribed and every notification arriving twice. A LAN session, which is served from the host's own IP on the same port, is left alone.


Layout ​

The default landing page is a Dashboard with at-a-glance widgets across sites, services, workers, and system health. Selecting Sites, Services, or System switches to a three-pane layout:

  • Left icon rail: the lerd logo at the top is the link back to the Dashboard; below it sit the Sites, Services, and System icon buttons; a separator further down lists a per-service icon for every running service that exposes a dashboard (phpMyAdmin, pgAdmin, Mailpit, RustFS, Meilisearch, Mongo Express, Selenium, etc.), each drawn as the mark its preset ships rather than a generic glyph for its category, in the rail's own colour so it still lights up on hover and when its dashboard is the one open, and clicking one opens that dashboard inline as a full-width iframe over the middle and detail panels; the notification bell, theme switcher and documentation button at the bottom, the last of which opens the built-in documentation rather than the website, with the version under them. A beta build shows its release above a beta badge, and a development build shows it above a dev badge whose tooltip names the commit it was built from; clicking that badge copies the commit hash. The bell keeps recent notifications with an unread count; the theme switcher is a single icon showing the mode in effect (sun, moon, or a half circle for following the system) that opens a menu to pick between Light, Dark and System, with the tooltip naming what System currently resolves to. Hovering any rail icon reveals its label in a floating tooltip to the right, so the collapsed rail stays readable at a glance
  • Middle list panel: scrollable list of all items in the active section; status dots, compact rows, collapsible groups (hidden on the Dashboard). The panel icon in its header slides it away, giving the detail panel the full width on a narrow or half-screen window, and the same icon appears left of the page title to bring it back; the command palette has the toggle too, and the browser remembers the choice
  • Detail panel: full controls and live logs for the selected item

On mobile the dashboard, list, and detail panels are full-screen with a bottom tab bar for navigation that includes Dashboard alongside Sites, Services, and System, plus an Apps entry holding what the rail's dashboard launchers hold on a wide screen, as a grid of cards drawing the same preset marks in their brand colours.

Theme ​

Light, dark and follow-the-system live in the icon rail's theme switcher. The colours themselves are a separate choice: System → Lerd → Theme picks between lerd, the bright brand red, muted and Ocean for anyone who finds the default too sharp, the classic editor schemes (Solarized Dark, Monokai, Cobalt, Dracula, Nord, Gruvbox Dark) and the desktop ones (Breeze, Adwaita, macOS). You can add your own by writing a small YAML file, either by hand or through the import button; see Dashboard themes.

Keyboard and screen readers ​

Everything in the dashboard works from the keyboard. The first Tab stop is a Skip to content button that jumps past the rail and the list straight into the page. Dialogs take focus when they open, keep Tab inside them, close on Escape and hand focus back to whatever opened them. Detail tabs (Overview, Logs, Env and the rest) move with the arrow keys, Home and End, and a site row can be reordered by focusing it, pressing Space and moving it with the arrows.

For screen readers, toggles announce themselves as switches with their on or off state, the rail marks the page you are on, and the small icons on a site row (sharing, worktrees, sleeping or failing workers) are read out rather than left to a hover tooltip. A service starting or stopping is announced as it happens, and the browser tab's title names the site or service that is open.

Language ​

The dashboard ships in fourteen languages: English, German, Spanish, French, Indonesian, Dutch, Portuguese, Turkish, Simplified Chinese, Japanese, Romanian, Italian, Polish, and Vietnamese. The first time you open the dashboard, the language is autodetected from your browser's preferred locale (Paraglide reads navigator.language and picks the closest match, falling back to English). To change it later, open System → Lerd and use the language picker in the settings section; the choice persists in localStorage so subsequent sessions open in the same language. Strings are wired through Paraglide; new keys fall back to English when a locale hasn't been translated yet.

Service dashboards opened inside lerd, such as Adminer, phpMyAdmin and pgAdmin, follow the same language where they are translated. Left alone they already match, since they read the browser's preference just as lerd does. A language picked in lerd is passed on to them as well, so a dashboard set to Romanian opens Adminer in Romanian even when the browser prefers English. A dashboard that only ships in English stays in English. A language picked in lerd also outranks the one a dashboard remembers on its own, which phpMyAdmin stores in a cookie from the first page it serves; with nothing picked in lerd, that remembered language stays.

System → Lerd settings with the language picker


Dashboard ​

Dashboard root page

The Dashboard is the root page (#dashboard) and the default destination when the UI loads. It hides the middle list panel and fills the main pane with a responsive grid of widgets:

  • Sites: a running-vs-total summary pill that turns red while any worker is failing, and one card per site, the same card the Sites tab draws, with the site icon or favicon, the app name as the title, the domain, framework and PHP version on the subline, the TLS padlock, the share, worktree, idle and worker indicators, and an open-in-browser button. Paused sites are left to the Sites tab and the rest follow whatever order the Sites tab is sorted by. A Link site call to action opens the same wizard as the Sites tab + button.

  • Services: an active-vs-total summary pill, a second pill counting the services with updates available, and one card per core service, the same card the Services tab draws, with the service icon, its running state in words, the version, an update arrow and a shortcut to its admin dashboard. Services with an update sort to the top so their arrow stays in view, and below them the services the most sites use come first. An Add button opens the preset picker, and a link leads into the Services tab.

  • Workers: one collapsible group per worker type (Queues, Schedules, Horizon, Reverb, Stripe, custom Workers), each headed by that worker's mark, its name and a summary counting only the states present, "3 up · 1 idle". The busiest group leads, on the number of workers it has up and then on how many it holds in all. Opening a group reveals one line per unit: state dot, the site it runs for (or site/branch for a worktree unit), and for a failing unit the last line it printed. An idle-suspended worker keeps its line with a moon and an "idle" tag rather than vanishing. Groups start closed, except one holding a failing worker, which opens itself so the error is readable without a click; whatever you open or close is remembered, and a group you closed stays closed. Each line links through to its site, and a Heal all button runs the same heal flow as the worker-health banner. Otherwise shows an "All healthy" pill.

    The worker type and its framework live on the heading rather than on every line, since a group is normally one of each. When a group does span two frameworks, the heading drops its brand tone and each line takes its own small framework mark, so the thing that actually differs between the rows is the only thing drawn on them.

    The marks come from the framework store, not from lerd: a worker declares icon (a built-in glyph, or a mark the store ships under workers/<icon>.svg) and optionally color. A worker that declares no colour is inked in its framework's, so a Laravel queue and a Symfony messenger read as different products at a glance; one that declares no icon at all falls back to its framework's own mark, and then to a plain gear. See Framework Workers.

  • Status line: sits in the header strip beside the search, so it costs no row of its own. A healthy stack reads as a quiet green pill counting running sites and active services. When DNS, Nginx or the watcher is down it turns red, names what is down, and carries a Start Lerd button that runs the same start as the CLI, so a stopped environment comes back without opening a terminal; an Open system link sits next to it for the component detail. The start streams its progress back, so the button counts units as they come up ("Starting... 7/20") and the pill names the stage it is in, the same sequence the CLI spinner shows: preparing, checking images, then each unit by name, then DNS. A start driven from the dashboard leaves lerd-ui alone; restarting it would take down the process serving the page. Failing workers are left to the worker-health toast at the bottom of the screen, with its Heal action, and an available update is left to the Lerd card, which scrolls away like any other card rather than standing over the grid until you update.

  • System health: overall pill (Healthy / Attention / Problem) derived from DNS, Nginx, and the file watcher, plus a row per component and a chip per installed PHP-FPM version coloured by its running state.

  • Lerd: current version, "Up to date" or a yellow "update available" banner with an Open terminal & update button and a View what's new link that opens the release notes in a modal, Autostart and LAN status pills, plus Check for updates and Manage → in the footer.

  • Resources: total CPU%, total memory, and reclaimable disk across lerd's whole footprint, the lerd-* containers plus lerd's own host-side processes (the UI, watcher, and tray daemons and any host worker such as a Vite dev server), with the memory bar also showing its share of host RAM, and a ranked list of the heaviest contributors by combined CPU and memory. Memory is what each row actually holds, read from its cgroup with the page cache taken out, so a service that has read a lot of files is not reported holding memory the kernel can reclaim the moment anything else wants it. A container row covers everything its service runs, the container plus the rootless helpers podman starts alongside it, which is why a small container can still show tens of megabytes against its name. CPU is a share of the whole machine rather than of one core, so the rows add up to the headline, and it is averaged over the interval since the previous refresh rather than sampled for an instant, so a row reads as the cost that service is really carrying. Two disk figures sit alongside: what lerd's own images occupy with a shared layer counted once, which opens a breakdown of every image in that total marked in use or idle, each row carrying the full cost of its own layers so rows overlap where they share a base, and what lerd cleanup would reclaim, shown only when there is something to reclaim and split into lerd's own leftovers and other images on the machine. A Clean up button runs it from a modal that previews what goes, grouped the same way and named by image ref, so you can see the space before deciding to take it.

Every widget is driven by the same Svelte stores that power the rest of the dashboard, so all values stay live over the WebSocket without polling.

On a fresh install the grid opens with a Get started with Lerd card in the first slot, standing in for the Lerd card. It lists its steps with a progress ring: add your first site (or create a project in the parked ~/Lerd), start a service, turn on notifications, match your desktop's theme (only when Lerd detects one), turn on Start when the dashboard opens, turn on idle-suspend, and connect your AI assistant, which registers lerd's MCP server exactly as the System page's AI assistants (MCP) switch does. Because it asks about notifications and the theme itself, the banners that offer those two stay hidden while it runs and after it is finished or dismissed; they are kept for installs that never went through Get started. Each step ticks itself off from the same state the rest of the dashboard reads, so doing it from the CLI or the System page counts too. When the last one ticks, the card says so for a moment and hands its place back to the Lerd card, which returns to the end of the grid. Dismissing it does the same straight away. It only starts when a dashboard first opens on an install with no sites, so an install already in use keeps its Lerd card. Where it stands is kept in the global config (ui.setup), so a browser, the desktop app and any other window all show the same checklist, and finishing or dismissing it in one clears it from the rest. Once finished or dismissed it stays gone.

Command palette ​

Command palette overlay

Press Cmd+K (macOS) / Ctrl+K (Linux/Windows), or / anywhere outside an input, to open a global command palette overlay. It searches across:

  • Pages: Dashboard, Sites, Services, System
  • Sites: every linked domain, with framework hint, led by the framework's own mark
  • Services: every core service, with version hint, led by the mark its preset ships in the same way
  • Install service: every installable bundled preset, so searching "install redis" installs it inline without opening the picker modal
  • Actions: Add a site, Add a service, Heal failing workers (when any), Check for updates, Open documentation, Open current site in browser, Toggle theme

Use ↑ / ↓ to move the selection, ↵ to execute, esc to close. The palette is available on every tab, not just the dashboard.


Sites ​

Sites tab

The middle panel lists all registered projects. Each row leads with the site's own favicon, falling back to the mark of the framework it runs when the site serves none, and to a status dot (green when FPM is running) only when neither is available. Then comes the domain name, and small indicator dots for running workers (amber for queue/horizon, sky for reverb, emerald for schedule, violet for custom workers). Paused sites appear in a separate collapsible section.

A sort button floats in the bottom-right corner of the list; clicking it opens a small menu with Recently used (the site that served a request most recently floats to the top), Most used (the site that served the most requests over the last seven days), Alphabetical (by domain), and Newest first (reverses the order sites were linked in). The two traffic modes read the same request store the request timing view is built from, so they count only the requests your app actually served: static assets, WebSocket upgrades, anything a dev server answered, and anything nginx timed at zero are left out, and a site's worktrees count toward it. A site with no traffic in the window sorts below every site that has some, alphabetically among the others. With workspaces configured, sorting orders the rows inside each workspace; it never moves a site out of its section. The choice is remembered across reloads. A drag handle appears on a site row when you hover it; grab it to drag the row into a new position, with the dragged row and the rest of the list animating smoothly into place. Dragging switches the list into a manual order seeded from whatever you were looking at, so there's no separate "manual" mode to pick. The new order is saved to sites.yaml and pushed live to any other open tab. Grouped subdomains always travel with their main site, and the paused section stays at the bottom.

Before you pick a site the detail panel shows a sites overview instead of an empty prompt. The header is titled Sites and carries the running-vs-total count, a paused tally, and a failing-workers indicator. Below it your sites are laid out as click-through tiles grouped by framework (Laravel, WordPress, and so on, with anything unrecognised folded into a trailing Other group), and a final Paused group for any suspended sites. Each tile is titled by the Laravel APP_NAME when the site sets a custom one (falling back to the domain, which then moves into the subline), and shows the site favicon, the mark of its framework when it has no favicon, or a status dot when neither is available, a framework and PHP/Node subline, a TLS lock, a worktree marker, the same running-worker dots as the list, and an open-in-browser shortcut. An Add a site action sits at the bottom in loopback mode.

Site detail with the Overview tab open

Selecting a site opens the detail panel with:

  • Address bar header: a browser-style row with the site's favicon, scheme, and domain. The leading lock icon toggles TLS in one click (green closed when enabled, gray open when disabled, static on worktrees and when DNS is off). Clicking the domain opens the Manage Domains modal, and the sliders button at the end of the bar edits the site's nginx override. Past the domain sits the framework badge, wearing the brand colour its framework declares, with the framework's own mark inside it; a framework that declares no colour keeps the default tone. A site served by FrankenPHP rather than the shared PHP-FPM gets an orange FrankenPHP badge beside it, reading FrankenPHP · worker in worker mode. To the right sits an action toolbar, every button the same size: open in browser, group, a share button, xdebug, terminal, and a ⋮ menu holding restart, pin, pause/resume, and unlink. On narrow panels the secondary actions fold into the ⋮ menu, group, workspace and share among them, with the workspace choices listed there as a section of their own, and the framework badge shrinks to its mark alone, naming the framework in a tooltip when hovered. The project path shows at the right of the tab row (Overview, Logs, Env and so on), centred against the tabs. Host actions such as terminal run on the machine that runs Lerd. The terminal action opens $TERMINAL when it is set, then the terminal you chose as your system default: on macOS the one LaunchServices records, so Warp, iTerm2, Ghostty and the rest are honoured without any setting of their own, and on Linux xdg-terminal-exec, the distribution's x-terminal-emulator alternative, or the terminal named in KDE's or GNOME's settings, whichever the system carries. A chosen terminal lerd already knows is launched with its own flags rather than a generic wrapper, since kitty and ghostty take the program directly. When nothing was ever picked it falls back to the list of common emulators, and on macOS to Warp, iTerm2 and Terminal.app in that order.

  • Share menu: clicking the wifi button toggles LAN sharing exactly as before, while hovering (or keyboard-focusing) it opens a menu with both share modes. The Local network section mirrors the LAN toggle with the share URL inline. The Public tunnel section starts a lerd share tunnel without leaving the dashboard: an auto entry picks the same tool a bare lerd share would, each supported tool (ngrok, Cloudflare Tunnel, Expose, Serveo, localhost.run) is listed beneath it, and tools missing from the machine show up disabled with an install hint. Starting a tunnel waits for the tool to print its public URL, which then appears as a violet chip next to the domain with the same hover-QR as the LAN link, and the menu offers a Stop action. Tunnels started from the UI are owned by lerd-ui: they end when stopped or when the daemon shuts down, and unlike LAN shares they are never resurrected on restart. Tunnels front the site's primary domain, so the section hides while a worktree tab is active, and on narrow panels the tunnel start/stop actions live in the ⋮ menu. The cog next to ngrok holds its settings: the auth token that lets a machine without ngrok installed share anyway, and the extra flags every ngrok share passes to ngrok for the features lerd has no setting of its own for (see Sharing Sites).

  • Overview layout: the Overview tab is a stack of sections rather than one long strip, a Runtime & workers row with the PHP/Node pickers, worker toggles, a Doctor button, and the Commands ▾ dropdown, and a Services grid of icon cards. Live logs moved out to their own Logs tab.

  • PHP / Node dropdowns: change the version per site; writes .php-version / .node-version into the project and regenerates the nginx vhost on the fly

  • Queue toggle: start or stop the queue worker; amber when running; live log stream below

  • Schedule toggle: start or stop the task scheduler; live log stream below

  • Reverb toggle: start or stop the Reverb WebSocket server; only shown when the project uses Reverb (detected via composer or .env)

  • Framework worker toggles: additional workers defined by the site's framework (e.g. Symfony messenger, Laravel horizon) appear as indigo toggles

  • Stripe toggle: start or stop the Stripe webhook listener

  • Worker logs shortcut: every worker toggle carries a small log-lines button on its right that opens the Logs tab with that worker's journal already selected, the same segment the queue's options gear sits in. The selected source lives in the address (#sites/<domain>/logs/<worker>), so a worker's logs can be linked and bookmarked directly

  • Pause / Resume: suspend a site's nginx vhost without unlinking it; the site stays registered and FPM keeps running. When a paused site is selected, the detail pane hides the overview/logs/tinker/dumps tabs and shows a centered Resume placeholder so it's obvious the site is offline on purpose rather than broken

    Paused site detail with the Resume placeholder

  • Add a site: a CTA on the dashboard Sites widget and a + button next to the Sites list header open the site wizard. It starts by asking whether you are linking a project that already exists or creating a new one. Linking browses your directories the way it always has; creating asks for a parent directory, a name, and which framework and major to scaffold, from the catalogue the store publishes. Both paths then meet at the questions lerd init asks on the terminal, PHP version, HTTPS, the database, services and workers, saved to the project's .lerd.yaml, with each service a package suggests ticked and the package named under it, and end on the setup step list, where the steps run one at a time with their output streaming into the modal. The wizard finishes on the new site.

    Scaffolding is minutes of composer, so the work runs in lerd-ui rather than in the request: closing the modal or reloading the page leaves it going and reopening the wizard picks it back up where it was. Because it creates directories and runs commands on the host, a remote dashboard reaches it only after lerd remote-control full-access on.

    Link Site modal

  • Unlink button: remove a site from nginx without touching the terminal. It opens a confirmation modal naming the site, and on confirm the site is unlinked while its files stay on disk; a refused unlink reports the reason inside the modal

  • Git Worktrees: on a git-backed site the address bar grows a tab strip above it. A site that is not under git yet shows an Initialize git button there instead, which runs git init in the site's folder. The first tab is the main checkout, followed by one tab per worktree (branch name); every tab carries a git-branch icon, and selecting one switches the path on the tab row to that worktree's checkout. A tab whose checkout has uncommitted work shows a * after the branch name, the way a shell prompt does, with ⇡ or ⇣ when it is ahead of or behind its upstream and the * in red on a merge conflict; hovering spells it out ("3 untracked · 11 modified"), and a clean checkout shows nothing. When the selected tab's branch tracks an upstream, the right end of the tab strip carries Fetch, Pull and Push for that checkout, the main one or a worktree alike. Pull and Push show how many commits the branch is behind or ahead, and Push is disabled when there is nothing to send. Fetch refreshes those counts; Push asks for confirmation first. Pull opens the same comparison as Switch branch: it fetches, shows how many commits are coming in and how many files they touch, and offers composer install, the project's JS install and the framework definition's migrate command, each ticked when the incoming changes call for it, with Snapshot the database first ticked when they bring migrations. When the branch has nothing to pull, the dialog says it is up to date and offers only OK. Files with uncommitted work the pull would overwrite are listed and hold it back, and the ticked steps run right after the fast-forward. On a branch that has diverged, ahead and behind at once, Pull and Push are held back with a tooltip saying so, since git refuses both until the branch is merged or rebased. Pull only ever fast-forwards and Push is never forced: a diverged branch, local changes in the way or a remote that moved on make git refuse, and its reason is shown, so nothing is left mid-merge and no one's commits are overwritten. After each one the button shows a check with git's summary for a few seconds. The main checkout's tab also has a Switch branch button that checks out another local or remote branch in place, never forced: uncommitted changes come along, and git refuses when the switch would overwrite them. A branch already open in a worktree is not offered. The same dialog can create a branch instead: New branch takes a name and a base, the current branch unless you pick another, and the comparison below is made against that base; branching off the current commit changes no code, so only a step that is genuinely due, such as an install never done here, is listed. The new branch tracks nothing, so its first push does not land on the base's upstream. A branch with no upstream shows Publish in place of Fetch, Pull and Push when the repository has an origin remote: after a confirmation it pushes the branch to origin under the same name, never forced, and tracks it from then on, so the usual three buttons take over. Branches are listed newest commit first with how long ago each last moved, and the list can be searched by typing; the Add Worktree branch pickers use the same list. Picking a branch compares it with the current one before anything changes, showing how many commits and files differ and offering what to run afterwards: composer install, the project's JS install (such as npm ci) and the framework definition's migrate command. Each is ticked when the diff calls for it, a changed lockfile, a missing vendor or node_modules, or new migrations in the folder the definition declares, and can be ticked or cleared before switching. Migrations the branch lacks raise a warning that the database is ahead of the code; nothing is rolled back. Files with uncommitted work that the target branch also changes are listed before switching, since git would refuse to overwrite them, and the switch waits until they are committed or stashed. When the site's database engine takes snapshots, the list starts with Snapshot the database first, ticked whenever the two branches' migrations differ: the database is copied before anything moves and saved as branch-<name> after the branch being left, so it serves both as the undo for a migration that goes wrong, offered as Restore snapshot if a later step fails, and as that branch's copy for when you come back. Each branch keeps one such copy per database: a newer one replaces the last once it is written, and snapshots taken by hand are never touched. When worktrees still share the main checkout's database and a switch or a pull of the main checkout brings migrations, Give worktrees their own database first is ticked too: each of them gets a copy of the database as it is before anything moves, the same isolation lerd db:isolate gives, so the migrations leave their schema alone. Isolating a worktree, here or with lerd db:isolate and lerd db:share, restarts the workers running for it, so none keeps a connection to the database it left. If a snapshot tagged with the target branch exists, the newest one is offered as Restore branch's database, never ticked by default since it replaces whatever the database holds now; it loads right after the switch, before any migration runs on top of it, and is refused if the target branch points the site at a different database. Worktree tabs have no switch, since a worktree's domain is named after its branch. The state is read while the site is open and refreshed every few seconds and whenever the window regains focus. Each worktree tab has an inline × that opens the Remove Worktree modal scoped to that branch (force / drop-db checkboxes). A trailing + opens the Add Worktree modal. Each worktree's framework-worker toggles still appear in the Overview's Runtime & workers row, so a per-worktree Vite dev server, queue, or schedule worker can be flipped on without affecting the parent. Worktree workers run under lerd-<wname>-<site>-<wt> units; the Workers group on the Services tab groups them under the same label as the parent site (e.g. "Vite") with a branch chip per row

  • Add Worktree modal: asks for a new branch or existing branch, the database choice (share parent / isolated empty / clone main / clone another worktree / reuse a preserved isolated DB), an optional Run migrations checkbox, and the frontend-asset build choice (Automatic / asset worker / npm script / skip). Submission streams the underlying lerd worktree add output live in the modal, including the Automatic: ... resolution line so the picked build path is never silent. The modal closes on success, or stays open with a warnings banner if any [WARN] lines were emitted

  • Remove Worktree modal: opens scoped to a single branch when its tab's × is clicked. Offers a Discard uncommitted changes (force) checkbox and, when isolated, an Also drop database checkbox. Runs lerd worktree remove and closes once the branch is gone

  • Live PHP-FPM log: streams FPM output for the selected site; tab switches to queue/horizon/schedule/reverb logs when those workers are running

  • Coloured output: every live log pane renders the ANSI colours the tool emitted, so Vite, Pest, artisan and composer read the same as they do in a terminal. Workers and UI-run commands are started with FORCE_COLOR, CLICOLOR_FORCE and a colour-capable TERM because they write to a pipe or a log file rather than a terminal and would otherwise strip their own colours; setting NO_COLOR in the environment lerd starts from turns all of that back off

  • Severity colours: log lines that carry no colour of their own are tinted by what they say, the same way in every pane: errors, fatals and 5xx responses red, warnings, PHP notices and 4xx responses amber, php-fpm and nginx notices blue, and successful access lines green. PHP's own diagnostics outrank the level word next to them, so a PHP Warning from the site reads as an error rather than as fpm housekeeping

  • Follow in terminal: the terminal icon in a log pane's header opens the host's terminal emulator tailing the same unit (podman logs -f, tail -f, or journalctl -f depending on the platform and the unit), so a long tail can outlive the browser tab. It picks the emulator the same way the site's terminal action does: $TERMINAL first, then the terminal the desktop is set to use, then the list of common ones. Remote dashboards show the same action once lerd remote-control full-access on is set; the terminal opens on the host that runs Lerd.

    Live PHP-FPM log tab

  • App logs tab: parses every *.log file the framework declares (Laravel: storage/logs/*.log) into level-coloured entries with click-to-expand stack traces and a live-search box. An expanded entry carries Copy for agent, which copies a short prompt quoting the entry's first line and pointing an AI assistant at that log file through the MCP logs tool, ending at that entry, so it reads the entry in full with what led up to it before fixing the cause. The button is left off entries the logs tool cannot reach: undated ones, and ones older than the last 512 KiB of the file it reads. The dropdown switches between log files. The newest entries load first and older ones arrive a page at a time as you scroll up, so a log of hundreds of megabytes opens as quickly as a small one; the search box filters what is loaded. The trash button (its tooltip shows the total log size) reclaims disk by deleting those files; clicking it opens a confirmation modal, and on confirm the active log is recreated by the app on its next write.

    App Logs tab on the site detail

  • Env tab: edit the project's .env (and any .env.* variant) right in the browser. Saving goes through a confirmation modal with an optional back-up-first checkbox and an atomic write that preserves the file mode; when a backup exists a Restore button opens a diff before rolling back. See Environment Setup.

  • Doctor: a stethoscope button in the Overview's Runtime & workers row, next to Commands, opens a modal that runs a battery of app-level health checks for any framework. Every site gets a universal baseline: the env file is present, keys declared in the example env file but absent from the real one (the check scans the project's PHP for how each key is read, so it warns only on keys the app reads without a default, plus VITE_ keys the frontend build needs, and stays quiet about keys that have a code fallback), the application key is set when the framework declares one, composer and node dependencies are installed with their lockfiles in step, composer audit and the audit of whichever package manager the project uses, npm, pnpm, yarn or bun, report no known advisories, and the resolved PHP version sits inside the framework's supported range. On top of that each framework contributes its own checks from its store definition: Laravel adds the APP_DEBUG-in-production footgun, the public/storage symlink, and pending database migrations (which reads migrate:status in the container and degrades to "unknown" when the app or its database is down). The checks run on demand when the modal is opened rather than on every site view, all at once rather than one after another, and each row appears the moment its check finishes, so the file and config findings show up before the slower container commands and audits return. They render as a compact list with a pass/warning/error icon on the right of each row. Findings that have a one-click fix show a Fix button that runs the matching command and streams its output in the command-run modal, then re-checks. Framework findings run a named framework command (e.g. key:generate, storage:link, migrate); the universal dependency and audit findings run an allowlisted package-manager command instead (composer install, composer update, npm install, npm audit fix). The environment-variables finding offers an Edit shortcut that jumps straight to the Env tab. When any check fails or warns, Copy for agent in the modal's header copies a short prompt naming those checks, which tells an AI assistant to read them through the MCP diag tool's site_doctor action, fix them, and run the checks again; on a worktree it points the assistant at that checkout's path. With a worktree tab selected the checks run against that worktree's checkout and its own env file (materialised first if the branch hasn't been opened yet), so a worktree is never diagnosed against the main branch's files or database. The button only appears when the site actually has something to check: a host-proxy Python, Ruby, or Go dev server with no framework and no composer.json or package.json has no applicable check, so it hides the button rather than offering an empty report.

  • Edit nginx: the sliders button at the end of the address bar opens the site's nginx override in a modal code editor with Save / Reset / Restore and timestamped backups, and every save runs nginx -t before committing. With a worktree tab selected it edits that worktree's override instead of the main branch's. See Nginx Overrides.

  • Services: the Overview's Services section lists every service from the project's .lerd.yaml as an icon card (a category-tinted service icon, its name, and a running/stopped status). Click a card to jump to that service's detail panel on the Services tab. A running service that ships a dashboard (mailpit, an admin UI, and so on) also carries an open-dashboard button on its card that opens the dashboard overlay directly. A service with no dashboard of its own falls back to its paired admin UI when that one is installed, so a mysql or mariadb card opens phpMyAdmin and a postgres card opens pgAdmin, starting the admin service first if it is stopped. A service the project references but that was never installed shows an amber Not installed status instead of a misleading Stopped; clicking its card opens a modal that offers to install the bundled preset for that name inline (with a version dropdown for multi-version presets) and jumps to the new service when it comes up. When no bundled preset matches the name the modal explains that it is a custom service to define under ~/.config/lerd/services/. A service one of the project's composer packages suggests (Redis for predis/predis, Solr for drupal/search_api_solr, and so on, as the package definitions declare) and that the site does not use yet shows as a Suggested card that says why (the tooltip names the package), with an add button, which puts it in .lerd.yaml, installs and starts it (asking first when that downloads an image, and bringing back one removed with lerd service remove), and wires it into the env file, and a dismiss ×, which stops suggesting it for that site. The × sits on the card's top-right corner like a window close and fades in only while the card is hovered or the button has keyboard focus (on touch screens it always shows). A package whose suggestions are alternatives, Redis or Valkey for predis/predis, puts forward only one, the one this machine already runs, and none when the site already uses either. Dismissals are kept per site in sites.yaml, so they stay on this machine. A service listed in .lerd.yaml also carries a remove ×, the same corner × a suggestion has, which asks for confirmation, takes it out of .lerd.yaml and points the env file away from it: the keys the framework wires for it go back to the project's example values, and any other key still naming its container is cleared. Keys that pick it as a driver, such as a cache store, are left for you to change, and the service itself and its data stay. The removal is remembered per site in sites.yaml, so a later lerd env, lerd setup or link does not detect the keys left behind and wire the service back; it stays offered as a suggestion, and adding it again, or listing it in .lerd.yaml, brings it back. A service the site reaches only through its env file carries an add button instead, which records it in .lerd.yaml without rewiring anything. The section is hidden entirely when the project declares no services and nothing is suggested.

Services ​

Services tab

The middle panel lists core infrastructure services (MySQL, Redis, PostgreSQL, Meilisearch, RustFS, Mailpit), any installed preset alternates (e.g. MySQL 5.7, MariaDB 11, MongoDB) and admin UIs (phpMyAdmin, pgAdmin, Mongo Express), grouped by service type under labelled sections (Databases, Cache, Messaging, Search, Mail & PDF, Admin UIs, Storage, Testing) in the same order the discovery grid uses, plus grouped per-site workers (Queues, Horizon, Schedules, Workers, Stripe, Reverb) below them.

The header has a + button that opens the preset picker modal: a one-click installer for the bundled service presets. Every row leads with the mark its preset ships, in the brand colour the preset declares, falling back to the category-tinted glyph the preset names when the store carries no mark for it. A search box at the top filters the list by name, description, or image as you type. Multi-version presets like mysql and mariadb show a version dropdown next to the Add button. Already-installed entries are filtered out.

Service preset picker modal

Before you pick a service the detail panel shows a services dashboard instead of an empty prompt. The header is titled Services and carries the running-vs-total count, an updates-available indicator, and the number of sites currently served. Below it an Installed grid lists every core service as a click-through card carrying the same category-tinted service icon the presets below it use, with its running/stopped status, version, pending-update arrow with the version it would move to (v1.27 → v1.28), and linked-site count. Services with an update lead the grid, and while any has one the section heading carries an Update all button: it lists each outdated service with its current and new version and a switch, all on to begin with, so any can be left out, then updates the ones still switched on one at a time, showing each one's progress on its row and reporting how many were updated and which failed, with the reason, without the failures stopping the rest. A running card that ships a dashboard also carries an open-dashboard button, and one with no dashboard of its own falls back to its paired admin UI when that is installed, so the mysql card opens phpMyAdmin and the postgres card opens pgAdmin, starting the admin service first if it is stopped. A Discover services section then promotes the bundled presets you have not installed yet, grouped by category (Databases, Cache, Messaging, Search, Mail & PDF, Admin UIs, Storage, Testing). Each preset is a card showing its service icon, name, and a one-line description, with an Add button that installs it inline with live phase feedback and jumps straight to the new service when it comes up. A preset only shows here while you run none of it, so an existing mysql or mariadb install is never promoted again just to offer its other versions; adding an alternate version stays in the preset-picker modal, which the section's + shortcut still opens.

Selecting a service opens the detail panel with Start, Stop, and Restart controls, status, and the correct .env connection values with a one-click copy button. The header leads with the same service mark the installed grid draws, larger and standing on its own without the tinted plate, inked in the brand colour the preset declares. An admin UI that ships no mark of its own draws the mark of the engine it administers instead, in that engine's colour, so pgAdmin comes up as PostgreSQL's elephant rather than a stand-in glyph; a per-site worker carries no mark of its own, so its header starts at the label. The header states how many sites use the service on a button next to the action menu, which drops down the list of them: each row is coloured by what that site is doing (serving, paused, stopped) and links through to it, and a service nothing uses shows no button at all. A per-site worker answers to one site, so its header names that site inline instead. Restart is available for every built-in and custom service and wraps podman restart (clears the paused flag on success); the grouped per-site workers (Queues, Horizon, Schedules, Workers, Stripe, Reverb) remain start/stop only. A Check for updates action sits in the service's action menu (non-worker services only); it bypasses the cached availability lookup, re-fetches the registry tag list, and shows either an "Already up to date" hint or a "Update available: {tag}" banner that becomes the live Update button moments later. Database service detail panels (mysql, postgres, mongo, and any installed alternate like mysql-5-7) get a few extras:

  • Databases tab: the panel opens on it, listing what is actually inside the running engine as cards with their sizes. From a card you can create and drop a database, export it to a plain SQL dump or import one, copy a per-database connection string, and open it in an installed admin tool, with that database's snapshots to take, restore, delete or download on the same card. Cards link back to the site that owns the database, a _testing database folds into the card of the database it tests and is offered up alongside it when that database is dropped, and a worktree's isolated database is shown under its own branch domain. It requires dashboard-control authority, which remote sessions receive only after lerd remote-control full-access on. See Databases.
  • Snapshots tab: next to Databases, the automatic snapshot schedule stated at a glance (with a button to the settings dialog) and what the engine's snapshots cost on disk, then every snapshot on the engine as a paged table filterable by site and by when it was taken, each row offering keep, download, restore and delete, with checkboxes for removing several at once behind one confirmation. Whether a given database is covered is set on that database's own snapshots dialog, or with one click on the clock beside it in the Databases grid. Same authority as the Databases tab.
  • Env tab: the service's connection URL and every env var its preset wires into a site's .env, one row per key. The header's copy button takes the whole block as KEY=value lines ready to paste into a .env, each row copies its own line, and a copy the browser refuses marks the button red instead of failing silently.
  • Suggestion banner: a sky-blue tip offering to install the paired admin UI (phpMyAdmin / pgAdmin / Mongo Express) when it isn't installed yet. Dismissable per-preset; dismissal persists in localStorage.
  • Open admin button: when the paired admin UI is installed, a button on the header opens its dashboard inline as a full-width iframe overlay and auto-starts the admin service if needed. When no admin UI is installed and the service is active, a fallback Open connection URL anchor hands the mysql:// / postgresql:// / mongodb:// URL to your registered DB client (DBeaver, TablePlus, Compass, etc.).
  • Dashboard button: for any service that exposes a dashboard URL (Mailpit, RustFS, Meilisearch, phpMyAdmin, etc.), a Dashboard button in the header opens it as an inline full-width iframe. The iframe overlay has its own header, leading with the service's own mark inked in the brand colour its preset declares, then the service URL, an Open in new tab escape hatch, and a close button. Clicking one of the main nav icons (Sites / Services / System) also closes the overlay.

Any action that has to fetch a container image (installing a preset, updating, migrating, rolling back or reinstalling a service, and on the System tab installing or rebuilding a PHP version) asks first, naming the image and roughly how much it will download, read from the registry manifest without pulling anything. Confirm and it runs as before; decline and nothing is fetched. When the image is already in the local store there is no download to disclose, so the action starts immediately with no extra click. See Image downloads.

Services with a tuning mount (mysql, mariadb, redis, postgres, and any custom service that declares a tuning: block) also get a Config tab on their detail panel. It edits the runtime tuning override in a code editor with the same Save / Reset / Revert / Restore-from-backup flow as the nginx and .env editors; saving restarts the service so it re-reads the file. See Tuning a service.

System ​

System tab

The middle panel lists individual system components: DNS, Nginx, Watcher, Snapshots, each installed PHP-FPM version, each installed Node.js version, the Node install form, Autostart toggle, and the Lerd version entry.

Selecting an item opens its detail panel:

  • PHP-FPM cards: a strip of version cards across the top carries the version number, the default star, and the running status; picking one opens its detail below. The version's tab strip (Logs, php.ini, Ports, Extensions) carries the Xdebug toggle with an inline mode selector (debug, coverage, debug-plus-coverage, develop, profile, trace, gcstats, visible when Xdebug is on), the sites button, and the version action menu on its right, so the version, star, and status are stated once on the card rather than repeated below it. The sites button counts the sites on the version and drops down the list, each row coloured by what that site is doing (serving, paused, stopped) and linking through to it; a version nothing uses shows no button. The tabs show the custom extension list and a live FPM log stream. For versions with no active sites, a manual Start/Stop action is available.
  • Terminal: the version action group leads with a Terminal button that opens a shell inside that version's FPM container in the terminal emulator you chose, the same session lerd shell gives from a project. It needs the container to be running, so it is disabled while the version is stopped.
  • Edit php.ini: each PHP version's detail has a php.ini tab (next to Logs) that edits that version's user php.ini override in a code editor, with Save / Reset / Revert and timestamped backups you can Restore. Changes apply to the shared FPM container for that version. An Editing dropdown at the top of the tab switches the scope between this version's file and the version-agnostic shared file applied to every version (a per-version file still overrides it). The same files are reachable from the CLI via lerd php:ini <version> and lerd php:ini shared.
  • Install a PHP version: a + button sits after the last PHP version tab (browser-tab style). It opens a modal with a dropdown of the supported versions that are not already installed; picking one and clicking Install builds the FPM image and streams the build log live. The build runs server-side, so closing the modal does not cancel it, and an operation finished/failed notification fires when it is done (see Notifications).
  • Site Xdebug button: every PHP site detail shows an Xdebug (bug) button in the address-bar row, between the LAN-share and terminal buttons. It turns green when Xdebug is on for that site's PHP version, and clicking it toggles Xdebug on or off in place (restarting the shared FPM container for that version). The button is hidden for static sites, custom containers, and FrankenPHP sites.
  • Node.js cards: carry a sites count on the right that drops down the list of sites on that version, each row coloured by what the site is doing and linking through to it, next to a remove button. A version nothing runs on shows no button at all. Sites that pin no version of their own count against the default. The Install Node.js version entry has an inline form; enter a version number (e.g. 22) and click Install, equivalent to lerd node:install <version>.
  • Nginx card: a Logs tab streaming the lerd-nginx container log, and a Config tab that edits the global http-level nginx override (gzip, proxy buffers, a global client_max_body_size, custom map blocks) with the same Save / Reset / Restore / backup flow as the per-site editor; every save runs nginx -t first. See Nginx Overrides.
  • Snapshots card: the automatic database snapshot schedule and its retention, the same controls the snapshot settings dialog and each engine's Snapshots tab carry. On by default in opt-in mode, so it takes nothing until a database is included, which is set per database on that database's snapshots dialog.
  • Watcher card: shows whether lerd-watcher is running; a Start button appears when stopped. Streams live watcher logs (DNS repair events, fsnotify errors, worktree timeouts).
  • Notifications card: per-category toggles (mail captured, worker failures, finished service operations, service updates, possible N+1 queries, dumps), a Send a test notification button, and the list of subscribed browsers with Forget actions. See Notifications.
  • Autostart card: enable or disable automatic start of all services at login.
  • Start when the dashboard opens card: off by default. Turn it on and opening the dashboard on a stopped lerd starts it for you, once per page load, so clicking the app or a bookmark is enough. Independent of Autostart: this one reacts to you opening the dashboard rather than to logging in.
  • AI assistants (MCP) card: registers lerd's MCP server with every supported AI assistant (Claude Code, Cursor, Codex, Gemini, Copilot, Windsurf, Junie, Antigravity and opencode), or removes it, exactly as lerd mcp:enable-global and lerd mcp:disable-global do. The switch shows whether lerd is registered, read from the files that registration writes, and a failed registration says so under it. Only a local dashboard can change it, since it writes into each assistant's config in your home directory; a remote session sees the state only, unless full access is on.
  • Lerd card: shows the current version and a Check for updates button. Clicking it spins the button and queries GitHub live, bypassing the 24-hour cache, so the result reflects the newest release right now rather than a stale cached answer. The status dot next to the entry is green when DNS, nginx, and the watcher are all running, red when any of them is down, and yellow when an update is available. When an update is available, an Open terminal & update button spawns the host's preferred terminal emulator with lerd update pre-filled. The release notes for the versions in between open in a modal from What's new rather than expanding inside the card. A small yellow dot also appears on the lerd logo in the left rail; clicking the logo always returns to the Dashboard, where the Lerd card carries the same notice.

The Start / Stop buttons in the System panel header start or stop all core services (DNS, nginx, and all PHP-FPM containers for versions that have active sites).

Documentation ​

The book icon at the bottom of the icon rail opens this documentation inside the dashboard. The pages are the ones embedded in the binary, the same set lerd man reads in the terminal, so they open on a machine with no internet instead of sending you to lerd.sh. Pick a page from the list on the left, or type in the search box above it to search the full text of every page; each result shows the sentence the match sits in.

Links between pages stay inside the dashboard and headings scroll into view, so a cross-reference reads the way it does on the website. The address follows along (#docs/usage/sites), which makes any page a bookmark, and the header keeps a link to the same page on lerd.sh for when you do want the website. Screenshots come out of the binary too, and because the pages are ordinary requests to the daemon, the service worker keeps the ones you have opened for later.

Updates ​

Shows the current version. When an update is available, the Lerd entry exposes an Open terminal & update button that launches the host's terminal emulator running lerd update. A remote dashboard shows the same action once lerd remote-control full-access on is set; the terminal opens on the host that runs Lerd.

Released under the MIT License.