On this page
All notable changes to lokal are listed here. Downloads and release notes for each version are on GitHub Releases.
lokal follows Semantic Versioning. Before 1.0, minor versions may change
the command line. The --json output ("schema": 1) and state.toml (version = 1) stay backwards compatible
until their schema numbers change.
After you upgrade, run lokal upgrade so the background service switches to the new version. See
Upgrading and uninstalling.
Unreleased#
To see what’s on main but not in a release,
compare v0.7.0 with main.
0.7.0#
Released 2026-11-10. Release on GitHub.
Added#
-
Wildcard services.
lokal service add '*' 3000routes every name under the project that no other service names, at any depth, to one port:https://tenant1.shop.lokal,https://a.b.shop.lokal.'*.v1'does the same for the names underv1. A service with its own name always wins, and the longer wildcard wins over the shorter one. A wildcard never covers its own base, soshop.lokalstays@’s."*"works as a key inlokal.tomltoo. Quote the*in a shell; the commands lokal suggests in its hints quote it already (lokal service update '*' --port 5000).- Each name gets its own certificate, naming exactly that name, on its first handshake (about a millisecond).
lokal still never issues wildcard certificates, and refuses to sign any name that contains
*. - lokal keeps up to 1024 of these certificates and drops the oldest when it needs room. The first time that
happens,
lokal logsshows a line saying so. - A wildcard has no single URL:
lokal open '*'is an error, andlokal open tenant1opens a name under it.lokal runandlokal upset noLOKAL_URLvariable for it. In--jsonoutput itssubdomainis"*"and itsurlishttps://*.shop.lokal, which isn’t a URL you can fetch. lokal doctorchecks it asdns.*.shop.lokal,tls.*.shop.lokaland so on, probing the namelokal-doctor.shop.lokal.- lokal 0.6.0 and earlier don’t serve a wildcard service, and can’t remove it on its own
(
lokal project remove --forcecan): after a downgrade, its names fail until you upgrade again.
- Each name gets its own certificate, naming exactly that name, on its first handshake (about a millisecond).
lokal still never issues wildcard certificates, and refuses to sign any name that contains
-
Routes in
lokal.toml. A[routes]table sends every name a host pattern covers to a service, under a path built from the name:"{site}" = "https://render.shop.lokal/demosite/{site}"serveshttps://acme.com.shop.lokal/aboutfrom render’s/demosite/acme.com/about. A pattern’s{captures}are whole labels; the leftmost takes one or more labels (acme.com), the others one each. The target is a service in the same file, by its full name, orhttp://localhost:<port>/….- lokal sends the target’s
Host, with the name asked for inX-Forwarded-Host, and maps redirects into the target’s path back:Location: /demosite/acme.com/newarrives as/new. It never rewrites response bodies or cookies, so routes are for pages. - Each name a route covers gets its own certificate, naming exactly that name, on its first handshake, as
under a wildcard service. lokal refuses to sign any name that contains
{or}, as it does for*. - A name with its own service wins over a route, and a more specific pattern over a less specific one
(
preview.{slug}over{site}). Two patterns that cover the same names, such as{site}and a'*'service, are an error. lokal upwrites the routes and prints a line for each;lokal downremoves them.lokal listshows them,--jsonadds aroutesarray to each project, andlokal doctorchecks each one (route.{site}.shop.lokal).- lokal 0.6.0 and earlier keep the routes in
state.tomlbut don’t serve them. - See Routes: one server, many sites.
- lokal sends the target’s
Changed#
- lokal’s messages no longer use em dashes. A fix follows a semicolon (
lokal isn't set up yet; run 'lokal setup'), a timing follows a comma (✓ background service started (pid 812), ready in 45ms), and a port warning inlokal setupandlokal doctorends with “To fix it, …”. Scripts that match the old text need the new punctuation.
Security#
- rustls is updated to 0.23.46, which fixes GHSA-c8m5-c952-6cm4, GHSA-h2v8-394m-p7vv, GHSA-w44r-93j7-qv28 and GHSA-vp88-798v-h67f. None of them reaches lokal’s configuration: lokal doesn’t enable brotli certificate compression or ECH, and doesn’t print TLS sessions.
0.6.0#
Released 2026-11-01. Release on GitHub.
Removed#
- lokal serves only
.lokalnames..localnames, which worked on macOS through mDNS, are gone, along with their publisher, so lokal resolves names the same way on macOS and Linux.lokal project create --domain shop.localanddomain = "shop.local"in alokal.tomlare now errors (exit 2) that name the.lokaldomain to use instead. A project an earlier lokal created on.localstill loads, but isn’t served: its URLs stop working.lokal status,lokal list,lokal project listandlokal doctorname it with the fix, andservice add,service update,lokal runandlokal uprefuse it. lokal doesn’t move it by itself, because a new URL breaks OAuth callbacks, cookies and bookmarks. See Moving a project to.lokal. lokal’s certificate authority still covers.local, so this needs no newlokal setupand no password. - The hidden
lokal daemon run --name-backendflag. The installed background service never passed it.
Changed#
- The status JSON’s
name_backend(lokal status --json) isdnson macOS and Linux. It wasmdns-local-only+dnson macOS andnone+dnson Linux. lokal setup’s self-test checks one name,lokal-selftest-<hex>.lokal, and no longer a.localone.lokal doctor’stld.lokalfailure, should.lokalever become a public TLD, no longer suggests moving to.local. It says that lokal hides the real.lokalsites on this computer, thatlokal uninstallremoves the resolver config, and asks you to report it. Doctor also has a newproject.<name>warning for a project that is still on.local.
0.5.2#
Released 2026-10-31. Release on GitHub.
Fixed#
- When another program (another local proxy, a web server, Docker) holds one of lokal’s ports as the background
service starts, lokal now says so. Before, macOS opened the other addresses and said nothing:
lokal setupandlokal doctorpassed while some requests reached the other program, and if both loopback addresses were taken, the self-test blamed trust. On Linux the whole service failed with only systemd’s “A dependency job … failed”. Now the daemon serves on the addresses it got,lokal statuslists the rest undermissing:, andsetup,start,restart,upgradeanddoctorname the program (127.0.0.1:443 is taken by nginx (pid 812), so lokal serves HTTPS only on [::1]:443) with the fix. When lokal can’t see the program, because another user runs it, they give thesudo lsoforsudo sscommand that names it. Port 80 alone is a warning, since HTTPS still works; any other port failssetupanddoctor. See Port 443 or 80 is already in use. lokal restartandlokal upgradeopen an address the service couldn’t open before, once the other program has let go of it. On macOS that takes a stop and a start, which they now do when an address is missing; a plain restart kept the dead socket.- On Linux,
lokal startandlokal restartwork after the service crashed or restarted five times in ten seconds. systemd refused them until its start limit was reset; lokal now resets it.
Changed#
lokal doctorwarns when another program listens on port 443 or 80 on every address: lokal takes127.0.0.1and::1from it, sohttps://localhostreaches lokal instead of that program.- The status JSON (
lokal status --json) haslisteners_missing: the addresses the service couldn’t open. - On Linux, the service no longer requires its socket units. One whose port another program holds fails alone, and the service starts on the others.
0.5.1#
Released 2026-10-31. Release on GitHub.
Added#
- Linux release builds, for x86_64 and arm64. Each release now has a
.debfor Ubuntu 26.04 and later, an.rpmfor Fedora 43 and later, and a tarball for other distributions, all with SHA-256 checksums. The Homebrew formula (brew install mehmetkose/tap/lokal) works on Linux too. The Linux binaries need glibc 2.39 or later. See Linux (preview). - The
.deband.rpmrequire systemd 258 or later, whichlokal setupneeds, so apt and dnf refuse them where setup couldn’t run, such as Ubuntu 24.04 and Debian 13. They also installcertutilas a recommended package, so Chrome and Firefox trust lokal. Like the Homebrew formula, they don’t run setup. Runlokal upgradeafter installing a newer one, andlokal uninstallbefore removing the package.
0.5.0#
Released 2026-10-31. Release on GitHub.
Added#
- lokal runs on Linux, as a preview: x86_64 and arm64, built from source with
make install. There are no Linux release builds or packages yet. On Linux,lokal setuptrusts lokal’s CA in the system certificate store and in Chrome’s and Firefox’s certificate databases, sends.lokallookups to lokal through a systemd-resolved DNS delegate, and runs the background service as systemd user units that hold ports 443 and 80. It needs systemd 258 or later with systemd-resolved running, such as Ubuntu 26.04, and stops before it changes anything if a piece is missing. Only.lokalnames work on Linux;.localnames don’t yet. Tested on Ubuntu 26.04 under WSL2. See Linux (preview). - On Linux, a user service can’t bind ports below
net.ipv4.ip_unprivileged_port_start, 1024 by default, so setup lowers it to 80 with/etc/sysctl.d/50-lokal.conf. This lets every local user bind ports 80–1023.lokal uninstallremoves the file and sets the kernel’s default, 1024, back.
Changed#
- When a service of
lokal upsays a port is in use, lokal also tries to bind the service’s own port itself (without listening, released at once) to tell whether it’s taken. A port held by a socket that neither a connect nor lsof shows, such as another user’s that is bound without listening, now gets the retry on a new port instead of the hint about debugger ports.
0.4.0#
Released 2026-10-26. Release on GitHub.
Added#
lokal restart <SUB>...restarts services of a runninglokal up, and returns once their URLs work again:✓ restarted https://web.shop.lokal (1.2s). Thelokal upstops the service’s process group as Ctrl-C would, checks that nothing else took its port, and starts its command again on the same port, with the same log. It also starts a service that isn’t running, because it exited orlokal upwasn’t asked to start it. When the new run exits first,lokal restartexits 1 and shows that run’s last log lines; when something holds the port, it names it. It runs only the commandslokal upstarted with, so a changedlokal.tomlisn’t read (it says so). Plainlokal restart, with no SUB, still restarts the background service. See Restart one service.- A second
lokal upfor a project that is already running points atlokal restart. lokal service removetakes several names:lokal service remove api web. It removes them all in one change, and removes nothing if any name doesn’t exist.lokal upnotices a server that its command started in a session of its own before exiting 0, asastro devdoes under a coding agent, andpg_ctl start,docker compose up -dandpm2 startdo. It says thatlokal logswon’t show that server’s output and stoppinglokal upwon’t stop it, and links to Servers that detach themselves. Before it exits, it names the servers that still run, and the nextlokal upthat finds such a port busy says which service left it running. lokal never stops these servers itself.
Changed#
lokal upadopts a service it doesn’t manage when its route is the onelokal.tomlgives it: hostlocalhost, the same host-header mode, and the file’sport, or, when the file gives none, a port that no other service in the file fixes. Moving a project whose services you added by hand to alokal.toml, or using one afterlokal run, just works, and no URL moves.lokal downremoves adopted services, and lokal says so when it adopts them.- A service
lokal updoesn’t manage whose route differs from the file’s is still an error, which now says what differs. Its hint is one command for all of them,lokal service remove api web -p shop, instead of one per service joined with;. lokal up’s ready line names the services it doesn’t count as ready, and the servers that detached:✓ ready: 4/6 services accepting connections (6.5s); not ready: api (exited 0), web (exited 1). A script that waits for✓ ready: N/Nkeeps working; one that matches the whole line needs to change.
Fixed#
lokal up’s ready line counted a service whose command exited 0 by a race, so the same stack could print1/2or2/2. It now counts by what the command left on its port.- The wrong-port hint of
lokal runandlokal upwaits until a server’s listeners settle, so a server that opens helper listeners before its own port, such as Astro on Cloudflare’s Vite plugin, no longer gets a hint a moment before it’s ready. The hint offers a port only when the server listens on exactly one, and never offers to replace a port thatlokal.tomlfixes or thatlokal run --portgave. - lokal keeps fields it doesn’t know when it rewrites
state.toml, so an older lokal editing state no longer erases a newer lokal’s settings. lokal 0.3.0 and earlier still drop them. lokal upretries a service on a new port only when its own port is taken. A server that reports a clash on another port, such as workerd’s debugger port 9229, now gets a hint that links to debugger ports instead of a retry that can’t help.
0.3.0#
Released 2026-10-25. Release on GitHub.
Added#
lokal.toml,lokal upandlokal down. A repo commits alokal.tomlthat lists its services and their dev server commands.lokal uproutes every service in it at once (each URL works before any command starts), then starts each command with/bin/shin a process group of its own, in the service’scwd, withPORT,{port},LOKAL_URLandLOKAL_URL_<SUB>aslokal rungives them. Each line of output is prefixed with the service’s name,✓ ready: N/N services accepting connectionsprints once every service accepts connections, and Ctrl-C stops every service and everything it started, keeping the routes. Before running anything from a file,lokal uplists its commands and asks a person to approve them, once per file and again whenever the file changes;--yesruns them without asking and remembers nothing.lokal up <SUB>...starts only those services. Onelokal upruns a project at a time.lokal downremoves the services thatlokal upadded.- Every service in the
--jsonoutput oflist,project list,project showandservice listhas amanagedfield:"lokal.toml"for a service thatlokal upadded from a repo’slokal.toml,nullotherwise."schema"stays 1. - A log file for each service that
lokal upstarts,~/Library/Logs/lokal/up/<project>/<sub>.log: its output from both streams, each line after a timestamp indaemon.log’s format, and lokal’s own lines about it (lokal: started on port …,lokal: exited 1,lokal: stopping (SIGINT)). Eachlokal upstarts it afresh, and it moves to<sub>.log.1at 5 MB.lokal logs <SUB>reads it, also from another terminal and afterlokal down; the project comes from-p, thelokal.tomlfound from the current directory, or the current project. Plainlokal logsstill shows the background service’s log. When a .lokal URL fails,lokal logs <sub> -n 50shows that service’s recent output. lokal upgives a service whose server listens on another port the hintlokal rungives, with the fix forlokal.toml:hint: web listens on localhost:5173, not 51234. Set port = 5173 for web in lokal.toml, or put {port} in its run: vite --port {port}. A service without a fixedportwhose server exits withEADDRINUSEoraddress already in usebefore it’s ready (something took the port after lokal chose it) is moved to a new free port and started again, once:note: web's port 51234 was taken; retrying on 51302. Both go into the service’s log too.
0.2.0#
Released 2026-10-25. Release on GitHub.
Added#
lokal run <SUBDOMAIN> -- <COMMAND>: picks a free port (or reuses the service’s), routeshttps://<SUBDOMAIN>.<project>.lokalto it, and starts your dev server on it with the terminal attached. The command getsPORT,HOST,LOKAL_URLand aLOKAL_URL_<SUB>for every service in the project, and{port}in any argument becomes the port. lokal prints✓ ready: https://…on stderr once the port accepts connections, hints when the server listens on a different port, lets Ctrl-C reach the server exactly once, and exits with the server’s exit code. A port that’s already in use stops it before anything starts.--rmremoves the service when the command exits, if the run added it.
0.1.0#
The first public release. macOS 14 Sonoma or later. Tested on Apple Silicon. Intel builds are published but haven’t been tested on an Intel Mac. Release on GitHub.
Added#
lokal setup: one-time onboarding. It creates a name-constrained CA (.lokaland.localonly, no IP addresses), trusts it in the System keychain, installs/etc/resolver/lokal, installs the per-user LaunchAgent with launchd-owned sockets on 443 and 80, and runs a self-test. It is idempotent,--dry-runshows the exact sudo commands, and running it again repairs a broken install..lokalnames through a built-in DNS server (wildcard loopback answers, TTL 0), and.localnames through mDNSResponder local-only records.- Projects and services:
project create|list|show|use|remove,service add|list|update|remove,list. Every change is live when the command returns (ready in Nms), and setup-type commands are safe to re-run. - HTTPS reverse proxy: HTTP/1.1 and HTTP/2, WebSockets, server-sent events, streaming bodies,
localhostupstreams on IPv4 or IPv6,X-Forwarded-*headers,--host-header upstreamfor Host-checking dev servers, 308 redirects from HTTP, and 404, 421 and 502 pages. lokal doctor: checks every layer concurrently and gives a fix hint for each problem, including a check that.lokalisn’t a public TLD.lokal env(Node, Python and curl trust),ca path,open,status,logs,start,stop,restart,upgrade,uninstall [--purge].--jsonoutput forlist,status,doctor,project list|showandservice list.- Prebuilt binaries for Apple Silicon and Intel Macs on GitHub Releases, with SHA-256 checksums, and a Homebrew
tap:
brew install mehmetkose/tap/lokal.