LocalRun

Install

Two things have to happen before https://myapp.test works: the app has to launch, and .test has to resolve. The app handles the second one for you; the first needs one command, because the build is not notarised yet.

Requirements: macOS 11 (Big Sur) or newer, Apple silicon. There is no Intel build. LocalRun is MIT-licensed and the source is at github.com/mindantic/localrun.

Download

Take LocalRun-<version>-arm64.dmg (~101 MiB) from Releases, open it, and drag LocalRun to Applications.

Install it to /Applications. If you move or rename the bundle later, any MCP registration pointing into it breaks; LocalRun repairs those at launch, but it is one less thing to think about.

First launch: get past Gatekeeper

LocalRun is ad-hoc signed; the signature is coherent, it is simply not a Developer ID and the app is not notarised. So spctl rejects it and the first launch gets the ordinary block:

"LocalRun" cannot be opened because the developer cannot be verified.

Right-click, then Open

Do not double-click. In Finder, right-click (or Control-click) /Applications/LocalRun.app, choose Open, and click Open again in the dialog that follows. That records an exception; from then on the app launches normally.

If macOS instead calls the app "damaged and can't be opened", the quarantine flag on the download is the problem rather than the signature. Strip it and open the app again:

bash
xattr -dr com.apple.quarantine /Applications/LocalRun.app

There is no window to look for after launching; LocalRun is an LSUIElement app, so it appears only in the menu bar, with no Dock icon and no window until you click it.

First run reaches the network

The DMG does not contain a Caddy binary. On first run LocalRun downloads the pinned release into ~/.localrun/bin/caddy, or adopts a Caddy you already have. On a locked-down network, install Caddy yourself first; the next section covers where LocalRun looks for it.

Finish the system setup

On first run the tray shows a Setup needs attention banner with a Fix button. It does two things:

  • Writes /etc/resolver/test and flushes the DNS cache, so every *.test lookup goes to LocalRun's DNS responder. This needs an administrator password; it writes outside your home directory. macOS shows its own native prompt.
  • Trusts Caddy's local root CA in your login keychain, so https://*.test gets a real padlock. No sudo here; macOS asks you to approve the keychain change.

You can re-run both later from Settings → System setup → Re-run setup. The same panel shows whether the resolver is installed and whether the CA is actually trusted, checked rather than assumed.

Why the login keychain

Trusting the CA in the System keychain forces an authorization dialog that a background process cannot render; the request fails with "no user interaction was possible". The login keychain covers every browser running as you, which is the whole point. See HTTPS & certs for the by-hand equivalent.

Where Caddy comes from

The app does not ship a Caddy binary. The daemon resolves one at startup, in this order:

  1. LOCALRUN_CADDY_BIN, if it points at an executable.
  2. ~/.localrun/bin/caddy: the copy LocalRun downloaded previously.
  3. A caddy already on your machine (Homebrew's included), if it meets the minimum version.
  4. Otherwise it downloads the pinned release into ~/.localrun/bin/caddy.

Settings → Caddy shows which one is in use and where it came from. If you already run Caddy for something else and it owns port 80, LocalRun does not compete for the port; it adds one import line to your Caddyfile instead and reloads through the admin API. That path needs no sudo, and your existing sites keep working.

Verify

Check that the resolver file exists and names the daemon's port:

bash
cat /etc/resolver/test
# nameserver 127.0.0.1
# port 15353

Then confirm a name under .test resolves to loopback; any name works, the responder answers the whole TLD:

bash
ping -c 1 anything.test
# PING anything.test (127.0.0.1): 56 data bytes

If the ping fails, the port in that file and the port the daemon actually bound have drifted apart. Re-run setup; that rewrites the file from the live port. Troubleshooting covers the rest.

State on disk

Everything LocalRun owns lives in ~/.localrun (override with LOCALRUN_DIR):

text
~/.localrun/
  projects.json      your sites
  settings.json      app preferences
  caddy/Caddyfile    generated: do not hand-edit
  logs/              per-project logs and daemon.log
  daemon.sock        the JSON-RPC socket

Build from source

You need Node 20+ and pnpm 11+.

bash
git clone https://github.com/mindantic/localrun.git
cd localrun
pnpm install
pnpm caddy:fetch                       # optional: pre-populate a local Caddy
pnpm --filter @localrun/ui dev         # tray app; spawns a daemon if none is running

pnpm caddy:fetch only fills resources/caddy/<platform>/ so a repo run skips the download; it is a dev convenience, not a build requirement. To run the daemon on its own while debugging:

bash
pnpm --filter @localrun/daemon dev

Next

Add a project turns a folder into a working .test address.