LocalRun

How it works

Three things stand between typing api.test in a browser and your dev server answering. LocalRun owns all three, and none of them require you to edit a file or type a password.

text
*.test  ──►  DNS responder  ──►  127.0.0.1

browser ──►  Caddy :80/:443  ──►  your project on 127.0.0.1:<port>

             daemon (DNS + Caddy + processes + IPC)  ◄── Electron tray UI

Nothing is installed into your project. LocalRun stores a name, a folder and a port, and generates its own Caddyfile. Remove the project and your folder is exactly as it was.

The name resolves

macOS lets you route a whole TLD to a resolver by dropping a file in /etc/resolver/. LocalRun writes /etc/resolver/test, and a bundled DNS responder answers every *.test query with 127.0.0.1.

The responder listens on an unprivileged high port, 15353 by default, and the resolver file names that port explicitly:

text
nameserver 127.0.0.1
port 15353

That port is not a detail you can skip. macOS lets an unprivileged process bind a privileged port on the wildcard address but not on a specific one: 0.0.0.0:53 is allowed, 127.0.0.1:53 returns EACCES. The responder binds loopback deliberately, so it stays off your LAN, which means it cannot use port 53, which means the resolver file has to say where it actually is.

The resolver file and the daemon must agree

If the daemon binds a different port than the resolver file names, every .test name stops resolving while everything else still looks healthy. Settings reports this as stale, and re-running setup rewrites the file.

Two smaller details matter more than they look. AAAA queries are answered NOERROR with zero answers rather than ignored; silence makes getaddrinfo wait out a timeout and every page load stalls about a second. And after the resolver file is written the DNS cache is flushed, because macOS caches negative lookups and your first request would otherwise still fail.

The certificate is real

Caddy listens on :80 and :443 and reverse-proxies each host to the right local port. For projects with TLS on, it issues a certificate from its own internal CA, and LocalRun trusts that CA in your login keychain.

Browsers see an ordinary valid certificate. There is no warning page and no exception to click through, because nothing is self-signed as far as the browser is concerned; the issuing CA is genuinely trusted on your machine.

If you already run Caddy yourself, LocalRun does not compete for the port. It writes a fragment and adds a single import line to your Caddyfile, then reloads through the admin API.

Your process is supervised

The daemon starts your run command, captures its output, watches whether it is alive, and can restart it. Everything the tray shows (the status dot, the log tail, the health check) comes from that supervision rather than from guessing.

It also notices servers it did not start. If something is already listening on a port, LocalRun can proxy to it without restarting anything, and it will say plainly that it does not own the process.

One seam

The daemon holds all the state and all the privilege. The tray app holds none: every button is a JSON-RPC call over a unix socket at ~/.localrun/daemon.sock.

That split is why the UI cannot mislead you about what it can do: the daemon classifies each service and returns what is actually possible, and the tray renders exactly that. It is also why .test names keep working while the tray app is closed, as long as the daemon is running.

Next: Service kinds explains what LocalRun can and cannot do to a given process, and why.