On this page
- Synopsis
- Global options
- Get started
- lokal setup
- lokal doctor
- lokal uninstall
- Projects
- lokal project create
- lokal project list
- lokal project show
- lokal project use
- lokal project remove
- How the project is chosen
- Services
- lokal service add
- lokal service list
- lokal service update
- lokal service remove
- lokal run
- lokal up
- lokal down
- lokal list
- Background service
- lokal start
- lokal stop
- lokal restart
- lokal status
- lokal logs
- lokal upgrade
- Utilities
- lokal env
- lokal ca path
- lokal open
- lokal help
- JSON output
- lokal list --json
- lokal project show --json
- lokal service list --json
- lokal status --json
- lokal doctor --json
- Exit codes
- Environment variables
This page lists every lokal command, grouped the way lokal --help groups them. The binary documents itself
too: lokal --help lists every command, and lokal help <command> shows a command’s flags and examples.
$ lokal --help
$ lokal help service addSynopsis#
lokal setup [--yes] [--dry-run] alias: install
lokal doctor [--json]
lokal uninstall [--purge] [--yes]
lokal project create <NAME> [--domain <DOMAIN>] alias: project new
lokal project list [--json] alias: project ls
lokal project show [NAME] [--json]
lokal project use <NAME> alias: project switch
lokal project remove <NAME> [--force] alias: project rm
lokal service add <SUBDOMAIN> <PORT> [--host <HOST>] [--host-header <MODE>] [-p <PROJECT>]
alias: service new
lokal service list [-p <PROJECT>] [--json] alias: service ls
lokal service update <SUBDOMAIN> [--port <PORT>] [--host <HOST>] [--host-header <MODE>] [-p <PROJECT>]
alias: service set
lokal service remove <SUBDOMAIN>... [-p <PROJECT>] alias: service rm
lokal run <SUBDOMAIN> [--port <PORT>] [--host-header <MODE>] [--rm] [-p <PROJECT>] -- <COMMAND>...
lokal up [SUB]... [--yes]
lokal down [--remove-project] [-p <PROJECT>]
lokal list [--json] alias: ls
lokal start
lokal stop
lokal restart [SUB]... [-p <PROJECT>]
lokal status [--json]
lokal logs [SUB [-p <PROJECT>]] [-f] [-n <LINES>]
lokal upgrade
lokal env [--shell <sh|fish>]
lokal ca path
lokal open <SUBDOMAIN> [-p <PROJECT>]
lokal help [COMMAND...]p is short for project and s for service, so lokal s add api 4000 does the same as
lokal service add api 4000. These two aliases don’t appear in the help output.
Running lokal with no arguments prints the root help. Before setup has run, it first prints a one-line welcome
and Get started: lokal setup.
Global options#
These work with every command, before or after it. The exception is --version, which works only on lokal
itself.
| Option | What it does |
|---|---|
-h, --help | Print help. On a subcommand, -h prints a summary and --help the full text. Both end with examples |
-V, --version | Print the version and build: lokal <version> (<git hash> <build time>) |
--no-color | Turn off coloured output. NO_COLOR does the same, and colour is off anyway when stdout isn’t a terminal |
-v, --verbose | Print debug detail to stderr. Repeatable |
$ lokal --version
lokal 0.1.0 (af13985 2026-10-20T23:26:47Z)Get started#
lokal setup#
lokal setup [--yes] [--dry-run]Alias: lokal install.
One-time onboarding for this computer. Setup creates lokal’s certificate authority, trusts it in the macOS System
keychain, installs /etc/resolver/lokal so .lokal lookups go to lokal, installs the background service and runs
a self-test. Trusting the certificate and installing the resolver file are the only steps that ask for your
password.
On Linux, setup trusts the CA in the system certificate store and in your browsers’ certificate databases, sends
.lokal lookups to lokal through a systemd-resolved DNS delegate, and installs the background service as systemd
user units. It also lets user services bind ports 80 and up, so the units can hold ports 443 and 80. That’s the
one extra step that asks for your password, in the same sudo prompt.
| Flag | What it does |
|---|---|
-y, --yes | Don’t ask Continue? [Y/n]. Password prompts still appear |
--dry-run | Print the plan and the exact privileged commands, then exit without changing anything |
- Setup is safe to re-run. It repairs whatever is missing, and asks for your password only if a privileged step is missing.
- Run it as yourself. It refuses to run as root.
- If it needs sudo but can’t ask for a password (no terminal is attached, or
LOKAL_FORBID_SUDO=1is set), it stops before it changes anything and exits with code 4. - With no terminal and no
--yes, it can’t ask for confirmation and exits with code 2. - On Linux, if a step can’t work on this machine, such as when systemd-resolved isn’t running or is older than
258, setup stops before it changes anything and exits with code 1:
lokal can't be set up here yet: <reason>.--dry-runprints the plan, then the same reason, and exits 1.
$ lokal setup --dry-run # show the plan and the exact sudo commands, change nothing
$ lokal setupSetting up lokal walks through each step and the password prompts, and On Linux shows Linux’s plan.
lokal doctor#
lokal doctor [--json]Checks every layer at once and explains how to fix anything that’s broken. On a healthy Mac it finishes in under a second.
| Check | What it checks |
|---|---|
setup.done | Setup has run: install.json and the LaunchAgent exist |
ca.files | The CA files exist, and the key is mode 0600 and owned by you |
ca.trusted | macOS trusts lokal’s CA |
ca.trust_scope | macOS trusts it for HTTPS only, not for email, code signing or anything else |
ca.scope | The CA’s name constraints are exactly .lokal and .local (lokal’s TLD before 0.6.0), with every IP address excluded |
daemon.running | The background service answers |
daemon.build | It runs the same build as this lokal |
listeners | Port 443 accepts connections on 127.0.0.1 and ::1. If another program held one of lokal’s addresses when the background service started, it names the program and the fix (Port 443 or 80 is already in use). That fails for port 443 or the DNS port, and is a warning for port 80 alone, or for a program that listens on every address on 443 or 80 |
selftest | The full setup self-test: names, connection, TLS verified by macOS, and an HTTP request |
resolver.lokal | /etc/resolver/lokal points at 127.0.0.1:53535, and macOS uses it |
tld.lokal | .lokal isn’t a real top-level domain. Asks a DNS root server, and reports skip when you’re offline. If it fails, lokal hides real .lokal websites on this Mac: lokal uninstall removes the resolver config, and the detail asks you to report it |
firefox | If Firefox is installed, whether it’s set up to trust lokal |
dns.<fqdn> | Each service’s name resolves to 127.0.0.1 and ::1. Warns if the lookup takes over 20ms |
dns.conflict.<fqdn> | /etc/hosts has no entry that points the name somewhere else |
tls.<fqdn> | A TLS handshake for the name verifies |
upstream.<fqdn> | Something is listening on the service’s port. Only a warning: your dev server may just be stopped |
route.<pattern> | For each route, such as route.{site}.shop.lokal: its target service still exists (fails if not, until lokal up adds it back), and something listens where it forwards (a warning if not). A route’s dns. and tls. checks probe the pattern with each capture replaced by lokal-doctor |
project.<name> | Listed only for a project that an earlier lokal created on .local, instead of its per-service checks. A warning: the project isn’t served, with the commands that move it to .lokal |
Each check reports pass, warn, fail or skip. lokal doctor exits with code 1 if any check fails.
Warnings don’t change the exit code. The tld.lokal query is the only network traffic lokal sends off your Mac.
On Linux, the same checks run against Linux’s pieces:
setup.donelooks for the systemd user units, andca.trustedverifies against the system certificate store (Linux trusts lokal's CA).ca.trust_scopealways passes: the system store has no per-use trust settings to narrow.resolver.lokalchecks the DNS delegate,/etc/systemd/dns-delegate.d/lokal.dns-delegate: that it points at127.0.0.1:53535, that systemd-resolved has loaded it, and that programs here ask systemd-resolved for names.firefoxreports your browsers’ certificate databases (Chrome’s and each Firefox profile’s): whether each holds lokal’s CA, or thatcertutilis missing. It appears only if one of those browsers is installed.listenersnames the process that holds a port withssinstead oflsof.
$ lokal doctor
$ lokal doctor --jsonlokal uninstall#
lokal uninstall [--purge] [--yes]Undoes setup. It stops and removes the background service, untrusts and deletes lokal’s CA from the System
keychain, removes /etc/resolver/lokal and removes lokal’s Firefox setting. Untrusting the CA and removing the
resolver file ask for your password.
On Linux, it removes the systemd user units, lokal’s CA from the system certificate store and your browsers’ certificate databases, and the DNS delegate, and sets the lowest port user services may bind back to 1024. sudo asks for your password once. Uninstalling on Linux lists the commands.
| Flag | What it does |
|---|---|
--purge | Also delete your projects, the CA files and the logs |
-y, --yes | Don’t ask Continue? [y/N] |
A plain uninstall keeps state.toml and the CA files, so running lokal setup later restores everything as it
was. Uninstall succeeds, with warnings, if some pieces are already gone. Like setup, it refuses to run as root.
Files and paths lists exactly what each form removes.
$ lokal uninstall # keeps your projects and the CA files
$ lokal uninstall --purge # deletes everythingProjects#
A project owns one domain, <name>.lokal by default. Projects and services
covers the naming rules in more depth.
lokal project create#
lokal project create <NAME> [--domain <DOMAIN>]Alias: lokal project new.
| Argument or flag | What it does |
|---|---|
<NAME> | Project name: letters, digits and -, up to 63 characters. Names are lowercased |
--domain <DOMAIN> | Use this domain instead of <NAME>.lokal. It must end in .lokal |
The new project becomes the current project. A project’s domain can’t equal,
contain or sit under another project’s domain. A .local domain fails with
.local names aren't supported since 0.6.0 and a hint naming the .lokal domain to use. Any other TLD fails with
only .lokal is supported (other TLDs could shadow real websites).
project create is safe to re-run. If the project already exists, it just becomes the current project. Passing
a different --domain for an existing project is an error.
$ lokal project create shop
✓ Created shop.lokal (current project) (done in 6ms)
Next: lokal service add api 4000
$ lokal project create store --domain store.acme.lokallokal project list#
lokal project list [--json]Alias: lokal project ls.
Lists every project with its domain and number of services, and marks the current one. --json prints the same
document as lokal list --json.
$ lokal project list
shop shop.lokal 2 service(s) (current)A project that an earlier lokal created on .local is marked (not served), and the list ends with a warning
that names it and how to move it to .lokal.
lokal project show#
lokal project show [NAME] [--json]Shows a project’s domain and its services. Without NAME, it picks the project the same way service commands
do (see How the project is chosen).
$ lokal project show
$ lokal project show shop --jsonlokal project use#
lokal project use <NAME>Alias: lokal project switch.
Sets the current project. service commands, project show and open use it when you don’t pass -p.
$ lokal project use shop
✓ Current project: shop (shop.lokal)lokal project remove#
lokal project remove <NAME> [--force]Alias: lokal project rm.
| Flag | What it does |
|---|---|
--force | Remove the project even if it still has services, and remove those services too |
Without --force, removing a project that still has services fails. If you remove the current project, no
project is current afterwards. The command returns once the project’s names are no longer served.
$ lokal project remove shop
$ lokal project remove shop --force # also removes its servicesHow the project is chosen#
service commands, run, project show and open work on one project. lokal picks it in this order:
- the project you pass with
-p(or--project); - the current project;
- the only project, if exactly one exists.
Otherwise the command fails with no project selected: run 'lokal project use <NAME>' or pass -p <NAME>.
lokal up takes its project from the repo’s lokal.toml instead, and lokal down
from -p or, without it, from lokal.toml. lokal logs <SUB> takes it from -p, then from
lokal.toml, then from the list above.
Services#
A service is a name under a project’s domain, routed to a port on your Mac.
lokal service add#
lokal service add <SUBDOMAIN> <PORT> [--host <HOST>] [--host-header <MODE>] [-p <PROJECT>]Alias: lokal service new.
Routes https://<SUBDOMAIN>.<project domain> to <HOST>:<PORT>. The URL works as soon as the command returns.
| Argument or flag | Default | What it does |
|---|---|---|
<SUBDOMAIN> | The name under the project’s domain. @ is the domain itself, and nested names such as v1.api work. A full name such as api.shop.lokal is accepted too. '*' is a wildcard service: every other name under the project, at any depth; '*.v1' every name under v1. Quote the * so the shell leaves it alone | |
<PORT> | Upstream port, from 1 to 65535. Ports 443 and 80 on a loopback host are rejected, because they’re lokal itself | |
--host <HOST> | localhost | Upstream host name or IP address, without a scheme or port. lokal warns if it isn’t a loopback address |
--host-header <MODE> | preserve | preserve sends the original Host header upstream. upstream sends Host: <HOST>:<PORT>, for dev servers such as Vite that check it |
-p, --project <PROJECT> | current project | The project to add the service to |
$ lokal service add api 4000
✓ api.myapp.lokal → localhost:4000 (ready in 8ms)
https://api.myapp.lokal
$ lokal service add @ 3000 # https://myapp.lokal
$ lokal service add v1.api 4001
$ lokal service add '*' 3000 # every other name: https://tenant1.myapp.lokal
$ lokal service add web 5173 --host-header upstream # Vite without allowedHosts
$ lokal service add db-ui 8080 --host 127.0.0.1 -p shopservice addis safe to re-run. Adding an identical service changes nothing, still waits until the URL works, and printsalready configured, ready in …. Adding an existing name with a different port, host or host-header mode fails, and the error names thelokal service updatecommand to run instead.localhostupstreams work whether your dev server listens on127.0.0.1or only on::1.- If nothing is listening on the port yet, the command still succeeds and adds a note, such as
note: nothing is listening on localhost:4000 yet. - A project that an earlier lokal created on
.localisn’t served, soservice add,service updateandlokal runrefuse it (exit 1) withproject `shop` is on shop.local, and lokal serves only .lokal names since 0.6.0and the commands that move it to.lokal.
Frameworks covers Vite’s allowedHosts and the --host-header upstream option.
lokal service list#
lokal service list [-p <PROJECT>] [--json]Alias: lokal service ls.
Lists one project’s services, with their URLs and upstreams.
$ lokal service list
$ lokal service list -p shop --jsonlokal service update#
lokal service update <SUBDOMAIN> [--port <PORT>] [--host <HOST>] [--host-header <MODE>] [-p <PROJECT>]Alias: lokal service set.
Changes only what you pass. You must pass at least one of --port, --host and --host-header. Like
service add, it returns once the change is live.
| Flag | What it does |
|---|---|
--port <PORT> | New upstream port |
--host <HOST> | New upstream host |
--host-header <MODE> | New host-header mode: preserve or upstream |
-p, --project <PROJECT> | The project (default: the current project) |
$ lokal service update api --port 4001
$ lokal service update web --host-header upstreamlokal service remove#
lokal service remove <SUBDOMAIN>... [-p <PROJECT>]Alias: lokal service rm.
Removes the services’ routes and names, in one change. The command returns once none of the names is served any
more, and prints one ✓ Removed line per service. A name you repeat counts once. If any of the names isn’t a
service in the project, it’s a usage error (exit code 2) that names it, and nothing is removed.
$ lokal service remove api
$ lokal service remove api web -p shoplokal run#
lokal run <SUBDOMAIN> [--port <PORT>] [--host-header <MODE>] [--rm] [-p <PROJECT>] -- <COMMAND>...Starts a dev server behind its URL without you choosing a port. lokal picks a free port, routes
https://<SUBDOMAIN>.<project domain> to localhost:<port>, waits until the route is ready, and then starts the
command on that port with your terminal attached.
| Argument or flag | Default | What it does |
|---|---|---|
<SUBDOMAIN> | The name under the project’s domain, as in service add. @ is the domain itself | |
--port <PORT> | a free port | The port for a new service. A service that already exists keeps its own port |
--host-header <MODE> | preserve | For a new service: preserve or upstream, as in service add |
--rm | Remove the service when the command exits, if this run added it | |
-p, --project <PROJECT> | current project | The project |
-- <COMMAND>... | The dev server’s command and its arguments. The -- is required, because run has a --port of its own |
$ lokal run web -- npm run dev
✓ https://web.myapp.lokal → localhost:51234 npm run dev (route ready in 12ms)
… your dev server's output …
✓ ready: https://web.myapp.lokal (1.4s)
$ lokal run web --host-header upstream -- vite --port {port}
$ lokal run api --port 4000 -- cargo run
$ lokal run docs --rm -- mkdocs serve -a 127.0.0.1:{port}-
The command runs as you typed it, never through a shell.
{port}in any argument is replaced with the port, for servers that don’t readPORT. It keeps your terminal, so colours and keyboard shortcuts work. It gets these environment variables:Variable Value PORTThe port HOST127.0.0.1, the address to listen onLOKAL_URLThe command’s own URL, such as https://web.myapp.lokalLOKAL_URL_<SUB>One for every service in the project, this one included: LOKAL_URL_API,LOKAL_URL_V1_APIforv1.api,LOKAL_URL_APEXfor@If two services map to the same variable (
v1.apiandv1-api), lokal sets neither and prints a warning. A wildcard service ('*') has no single URL: it gets noLOKAL_URL_<SUB>, andlokal run '*'sets noLOKAL_URL. -
lokal’s own lines go to stderr. Your dev server’s output passes through untouched.
✓ ready: …appears once the port accepts connections, so a script or agent can wait for that line: the URL works from then on. If the background service isn’t running, or lokal isn’t set up, the route is saved, a note says so, the command starts anyway, and the line reads✓ listening on localhost:<port>instead. -
A server on the wrong port. lokal checks what the command (and any process it started, such as the
nodethatnpm run devstarts) listens on 3 seconds after the start, then every 2 seconds until the port is ready. If two checks in a row find the same ports, and not lokal’s, lokal prints ahint:line once, with the two ways to fix it: point the route at that port withlokal service update, or pass--port {port}to the server. If the server listens on several ports, the hint lists up to three, then how many more, and offers none of them, because one may be a debugger. When you gave the port with--port, the hint never offers another one: it asks for the server to listen on yours. -
An existing service is reused as it is: same port, host and host-header mode, so the port stays the same from one run to the next, and a route you added by hand gets its server started. A different
--portor--host-headerfails with exit code 2 and names thelokal service updatecommand to run. A service that routes to another machine can’t be run. -
A port that’s already in use stops
runbefore anything starts (exit code 1). The error names the process that listens there and how to move the route. lokal never moves a port by itself. -
Ctrl-C reaches your dev server exactly once, as it does without lokal. When there’s no terminal, for example when an agent runs
kill -INTon lokal, lokal passes SIGINT on to the server. SIGTERM and SIGHUP are always passed on. -
The exit code is the command’s. If a signal stopped it, the code is 128 + the signal number: 130 for SIGINT, 143 for SIGTERM. A command that isn’t found exits 127, one that can’t be executed 126.
-
Afterwards the route stays, and requests get lokal’s 502 page until the server runs again. With
--rm, lokal removes the service, but only if this run added it.
Frameworks lists the {port} patterns for common dev
servers.
lokal up#
lokal up [SUB]... [--yes]Gets a repo’s whole stack running behind its domains. lokal reads the repo’s lokal.toml, routes every service in
it, waits until every URL works, and then starts each service’s dev server and shows their output. It stays in the
foreground until the servers exit or you stop it. lokal up is the many-service form of lokal run:
its commands get the same ports, {port} and environment variables.
| Argument or flag | What it does |
|---|---|
[SUB]... | The services to start (default: all of them). Every service in the file is routed either way |
-y, --yes | Run the commands without asking, and remember nothing. For agents and CI in your own repos (see the trust check) |
$ lokal up
✓ https://shop.lokal → localhost:3000 npm start
✓ https://api.shop.lokal → localhost:51234 cargo run -p api
✓ https://web.shop.lokal → localhost:51240 vite --port 51240
✓ https://docs.shop.lokal → localhost:51251 mkdocs serve -a 127.0.0.1:51251
routes ready in 41ms
@ | > shop@1.0.0 start
api | Compiling api v0.1.0 (/Users/you/shop/services/api)
web | VITE v5.4.10 ready in 312 ms
… the dev servers' own output …
✓ ready: 4/4 services accepting connections (2.8s)
^C
stopping 4 services… done. Routes kept; 'lokal down' removes them.
$ lokal up api web # route every service, start only api and web
$ lokal up --yes # agents and CI: no question, nothing rememberedWhile it runs, lokal restart <SUB> restarts one of its services from any terminal, and returns
once the service’s URL works again.
The lokal.toml file
A repo commits one lokal.toml, usually at its root. It works like a Procfile with domains: each service has a
name, which becomes its subdomain, and the command that starts its dev server.
# lokal.toml at the repo root
project = "shop" # optional; default: the directory name
# domain = "shop.lokal" # optional; default: <project>.lokal
[services]
"@" = { run = "npm start", port = 3000 } # https://shop.lokal, fixed port
api = { run = "cargo run -p api", cwd = "services/api" } # lokal picks a free port
web = { run = "vite --port {port}", host_header = "upstream" }
docs = "mkdocs serve -a 127.0.0.1:{port}" # short form: the string is the command| Key | Default | What it is |
|---|---|---|
project | the directory’s name | The project name, following the same rules as project create. The default comes from the directory that holds lokal.toml, lowercased, with other characters turned into -, and lokal prints a note saying so |
domain | <project>.lokal | The project’s domain. It must end in .lokal. A .local domain fails at its line and column, with the hint set domain = "<name>.lokal" |
[services] | Required: one entry per service. The key is the subdomain. "@" is the domain itself, nested names such as v1.api work, and "*" is a wildcard service | |
[routes] | Optional: a host pattern with {captures} as the key, and the URL to send it to as the value, a service of the file (https://render.shop.lokal/demosite/{site}) or http://localhost:<port>/…. See Routes |
Each service is a table with these keys, or just a string, which is short for { run = "…" }:
| Key | Default | What it does |
|---|---|---|
run | Required. The shell command that starts the dev server. {port} in it becomes the port | |
port | a free port | A fixed port. Without it, lokal picks a free port the first time and keeps it from one lokal up to the next |
cwd | the directory of lokal.toml | The directory the command runs in, relative to lokal.toml. It must exist |
host_header | "preserve" | "preserve" or "upstream", as --host-header in service add |
env | Extra environment variables for the command, as a table of strings. PORT, HOST, LOKAL_URL and the LOKAL_URL_* variables are lokal’s, so setting one is an error |
- Every service needs
run, soapi = 4000is an error. There’s nohostkey: lokal starts these servers on your Mac, so the upstream is alwayslocalhost. Servers that lokal doesn’t start, such as a container, stay withlokal service add. lokal uplooks forlokal.tomlin the current directory, then in each parent, and the first one it finds wins. Nothing merges: a nestedlokal.tomlis a project of its own. In a monorepo, put one file at the root, with acwdfor each service.- A symlinked
lokal.tomlcounts as the file it points to: its directory, its project name and its approval. - An error in the file is a usage error (exit code 2) that names the file, line and column:
error: <path>:<line>:<col>: <message>. An unknown key only gets a warning, so a typo is flagged and a file written for a newer lokal still runs.
What lokal up does
- Finds and checks
lokal.toml. A SUB that isn’t in the file is a usage error that lists the file’s services. - Runs the trust check. It comes before anything changes, so a refusal routes nothing and starts nothing.
- Takes the project’s lock, so only one
lokal upruns a project at a time. A second one exits 1 withlokal up is already running for shop (pid 1234). - Routes every service in one state change and waits until every name is ready, so every URL works before any
command starts. It creates the project if it’s missing and makes it the current project. Services that left the
file are removed. Services you added by hand stay, unless
lokal upadopts them (below). The project’s routes become the file’s[routes]. - Prints one line per service, with
{port}filled in:✓ https://api.shop.lokal → localhost:51234 cargo run -p api. A service thislokal updoesn’t start gets a dim(not started). Then one line per route,✓ https://{site}.shop.lokal → https://render.shop.lokal/demosite/{site}, androutes ready in 41ms. - Starts the commands and shows their output, then prints the ready line. It runs until every command has exited or you stop it.
-
The commands. Each one runs as
/bin/sh -c '<run>', with{port}replaced by the port and nothing else added. It runs as you, in itscwd, in a process group of its own, with stdin from/dev/null. It gets your environment plus the variableslokal runsets (PORT,HOST=127.0.0.1,LOKAL_URLand aLOKAL_URL_<SUB>for every service in the project), and then the file’senv. -
Ports. A
portin the file is used as it is. A service without one keeps the port it got last time, so its port stays the same from onelokal upto the next. A new service gets a free port, picked the waylokal runpicks one. -
A port that’s already in use stops
lokal upbefore it changes or starts anything (exit code 1). The error names the process that listens there, such aslocalhost:3000 is already in use by node (pid 412); not starting @, and the hint says how to fix it. lokal doesn’t move the port to get around it. Services thislokal updoesn’t start aren’t checked: the server there may be your own. When it’s a server that a command left running at an earlierlokal up, the error says so, and the hint says how to stop it:error: localhost:9903 is already in use by node (pid 25898), which web left running at the last lokal up; not starting web hint: stop it: kill 25898. web's run must keep its server in the foreground. See https://mehmetkose.github.io/lokal/docs/guides/frameworks#servers-that-detach-themselves -
A port that’s taken after all. Something can take a port after lokal checked it, or hold it without listening on it. When a service prints
EADDRINUSEoraddress already in use(in any case) before it’s ready, lokal checks the service’s own port. It counts as taken when a process outside the service’s process group listens on it or has it bound without listening, when it accepts connections from a process lokal can’t see (another user’s), or when lokal can’t bind it itself (it tries, without listening, and lets go at once). If it’s taken, and the service has noportin the file and exits with an error before it’s ready, lokal moves the service to a new free port, waits until its URL points there, and starts the command again, once. The exit line comes first:web | Error: listen EADDRINUSE: address already in use :::51234 web exited 1 note: web's port 51234 was taken; retrying on 51302{port}andPORTget the new port; the URLs stay the same. A second failure is reported as a normal exit. Aportyou fixed in the file is never moved, and if the service’s route changed whilelokal upran (withlokal service update, say), lokal says so and doesn’t retry. -
A port in use that isn’t the service’s. If the service’s own port isn’t taken, the clash was on another port the server opens, such as a debugger port that two servers started at once both picked. Moving the service wouldn’t help, so lokal doesn’t retry. After the exit line it prints a hint, also for a fixed
port, and the hint goes into the service’s log too:w | workerd/server/server.c++:1882: bind(): Address already in use; port 9229 w exited 1 hint: w says a port is in use, but its own port 53840 is free. Another port it opens may be shared with another service, such as a debugger port (9229): give each server its own. See https://mehmetkose.github.io/lokal/docs/guides/frameworks#several-node-or-workers-servers-debugger-portsDebugger ports has the setting for each tool.
-
A service you added by hand (or that
lokal runleft) with the same name as one in the file is adopted when its route is the one the file gives it: hostlocalhost, the file’shost_header(defaultpreserve), and the file’sport, or, if the file gives none, a port that no other service in the file fixes. Its URL doesn’t move, it keeps its port, and from then onlokal upmanages it, solokal downremoves it. After the route lines, one note names every adopted service:note: adopted api, web (added by hand or by lokal run, with the routes lokal.toml gives them); lokal down removes them now -
A service you added by hand with another route is a usage error (exit code 2), and nothing changes. The error says what differs, for each such service, and the hint is one command that removes them all, so the next
lokal upadds them with the file’s routes:error: services api, web already exist with other routes than lokal.toml gives them (api: port 4000, not 9904; web: host 127.0.0.1, not localhost), and lokal.toml doesn't manage them (added by hand, or left by lokal run) hint: lokal service remove api web -p shopA difference is
host X, not localhost,port X, not Y(the file fixesY),port X, which lokal.toml gives to Z(another service fixes that port), orhost_header "upstream", not "preserve"(or the other way round). -
A different domain. If the file sets
domainand the project already exists with another one,lokal upfails, asproject createdoes with a different--domain. A project still on.localfails either way, before anything changes:project `shop` already exists with domain shop.local, not shop.lokal, with the hintlokal serves only .lokal names since 0.6.0; move the project: lokal project remove shop --force, then lokal up again. -
Before setup, or while the background service isn’t running,
lokal upsaves the routes, prints a note saying what to run, and starts the commands anyway, so the servers still work onlocalhost.
The trust check
A lokal.toml runs commands, and the repo may be someone else’s. So before anything changes, lokal up lists every
command in the file, with its cwd and env, and asks a person to approve them:
/Users/you/shop/lokal.toml runs these commands:
@ npm start
api cargo run -p api in services/api
web vite --port {port}
docs mkdocs serve -a 127.0.0.1:{port}
run these commands? [y/N]- It asks the first time it runs a file, and again whenever the file changes.
yrecords the approval and goes on. Anything else printsNot approved; nothing was started.and exits 1. - Once a person has approved the file,
lokal upruns it without asking, also with no terminal, until the file changes. - With no terminal to ask on and no
--yes, it lists the commands, changes nothing and exits 1:lokal.toml has commands nobody has approved yet: <path>, orlokal.toml changed since it was approved: <path>. The hint asks a person to review the file and runlokal upin a terminal. --yesruns the commands without asking. It still lists them, and it records no approval, so a stored approval always means a person said yes.- The listing shows control characters and invisible characters escaped, and puts each extra line of a multi-line
value on a line of its own starting with
↳, so nothing in the file can hide what runs.
Approving a file means trusting the repo, because the commands run the repo’s own code: npm start runs the
scripts in package.json. The security model explains
what lokal stores.
Output and the ready line
-
Each line a service prints shows up as
name | line, with the names padded to the same width, and coloured when colour is on. A service’s stdout lines go to lokal’s stdout, and its stderr lines to lokal’s stderr. Lines longer than 16 KiB are split. -
lokal’s own lines go to stdout, unlike
lokal run’s, because no single command owns stdout here.warning:anderror:lines go to stderr. -
A service that exits on its own is reported as
api exited 1, after its last lines of output, and the others keep running. If its shell can’t start, lokal printsapi: cannot start: <error>on stderr. After an exit 0, lokal first looks at the service’s port, in case the command left its server running (below). -
A server on the wrong port. lokal checks what a service’s shell (and any process it started, such as the
nodethatnpm run devstarts) listens on 3 seconds after the service starts, then every 2 seconds until it’s ready. If two checks in a row find the same ports, and not the service’s, lokal prints ahint:line once for that service, with the fixes 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}The second fix appears only when the
runhas no{port}yet. A server that listens on several ports gets a list (up to three, then how many more) and no port to switch to, because one may be a debugger (If one of them is its server, set port to it for web in lokal.toml). When the file fixes the service’sport, the hint never suggests another one:hint: web isn't listening on 3000, the port lokal.toml gives it (it listens on localhost:5173). Make its server listen on 3000, or put {port} in its run: vite --port {port}lokal says nothing while the server listens on nothing yet (still compiling), or while its ports keep changing. It doesn’t suggest
lokal service update, because the nextlokal upwould set the port back. -
The ready line. lokal probes the port of each service it started. Once every one of them accepts connections or has exited, with its exit line printed, it prints
✓ ready: 4/4 services accepting connections (2.8s), timed from the first start. The second number counts the services thislokal upstarted, and the first those that accept connections, so a service that exited doesn’t count, unless its server detached and still runs. After the count, the line names the services that aren’t simply ready:✓ ready: 4/6 services accepting connections (6.5s); detached: render; not ready: api (exited 0), web (exited 1)This is the line a script or a coding agent waits for: wait for the
✓ ready: N/Nat its start. Until a service is ready, its URL gets lokal’s 502 page. -
If the routes didn’t take, because lokal isn’t set up or the background service isn’t running, the URLs don’t work, so the line reads
✓ listening: 4/4 services on localhostinstead, with the same names after it.
A server that detaches
Some commands start their server in the background, in a session of its own, and exit 0: astro dev under a
coding agent, pg_ctl start, docker compose up -d, pm2 start. Such a server leaves the service’s process
group, so lokal can’t show its output or stop it. When a command exits 0, lokal looks at the service’s port before
it reports the exit: at once if the port already accepted connections, and otherwise for up to 2 seconds, since
the server may start after its command has exited. Meanwhile the exit line and the ready line wait, so a command
that exits 0 without a server holds the ready line up to 2 seconds longer.
If a process outside the service’s process group listens on the port, the exit line names it, and a hint follows it, on the terminal and in the service’s log:
web exited 0, and left localhost:9903 to node (pid 25898), which runs outside lokal up: lokal logs web won't show its output, and stopping lokal up won't stop it.
hint: web's run must keep its server in the foreground. See https://mehmetkose.github.io/lokal/docs/guides/frameworks#servers-that-detach-themselves- The ready line counts the service once, as
detached. - A server left in the service’s own process group, such as a background job (
server & exit 0), isn’t detached: lokal still stops it with the group, and the service counts as ready. - lokal never stops a detached server. Before it exits, it names the ones that still run:
2 servers keep running outside lokal up: web (node, pid 25898), render (node, pid 25901). - lokal records them next to its lock, so that if the next
lokal upfinds such a server’s port still in use, the error says which service left it there (above).
Servers that detach themselves has the fix for each tool.
Logs
Each service that lokal up starts also writes to a log file, ~/Library/Logs/lokal/up/<project>/<SUB>.log
(@.log for the apex; on Linux, under ~/.local/state/lokal/up/<project>/), which
lokal logs <SUB> reads:
2026-10-25T10:44:34.635510Z lokal: started on port 51234: cargo run -p api
2026-10-25T10:44:35.012345Z Compiling api v0.1.0 (/Users/you/shop/services/api)
…
2026-10-25T10:51:02.400118Z lokal: stopping (SIGINT)
2026-10-25T10:51:02.431907Z lokal: exited 130- Every line the service prints, from both streams and in order, goes in after a timestamp, without the
name |prefix. The timestamp is UTC with microseconds, the same format as the background service’s log. - lokal’s own lines about the service are marked
lokal::started on port …,cannot start: …,stopping (SIGINT)(andSIGTERM,SIGKILL),exited 1, also while stopping,port 51234 was taken; retrying on 51302,exited 0, and left localhost:9903 to node (pid 25898), …,restarting (lokal restart),hint: …andwarning: …. Someone who reads only the file still sees what happened. - Each line is written at once, so
lokal logs -fshows it straight away. - Each
lokal upstarts the log of each service it starts afresh. A service it doesn’t start keeps its old log. A restart goes on in the same log. - At 5 MB, the log moves to
<SUB>.log.1, replacing an older one, and a new file starts, so a service keeps at most about 10 MB. - If lokal can’t write a log, it prints one
warning:and stops logging that service. The service keeps running.
Stopping
The first Ctrl-C (SIGINT), SIGTERM, SIGHUP (the terminal closed) or SIGQUIT stops every service:
- lokal prints
stopping 4 services…and sends SIGINT to each service’s process group, SIGTERM at 5 seconds to any that are left, and SIGKILL at 10 seconds. A second signal sends SIGKILL at once. - It waits until every process group is empty, not just until each shell has exited, so the background jobs a command started, which ignore SIGINT, stop too.
- It prints
done. Routes kept; 'lokal down' removes them., and then names any server that a command left running outside lokal (a server that detaches), which it doesn’t stop.
- The services aren’t in lokal’s process group, so the terminal’s Ctrl-C reaches only lokal, and lokal signals each service once.
- A closed stdout or stderr, as in
lokal up | head, stops the services the same way. - The routes stay. Requests get lokal’s 502 page until the servers run again, and
lokal downremoves the routes. - If lokal itself is killed with SIGKILL, it can’t stop anything, and the commands keep running. The next
lokal upfinds their ports in use and names the process that holds each one.
How lokal up exits
lokal up exits with 128 + the first signal it got: 130 for Ctrl-C, 143 for SIGTERM, and 141 when its stdout or
stderr closed. If every command exits on its own, it exits 0 when all of them exited 0, and 1 otherwise, also
when a server a command left running still runs.
Errors before any command starts keep the usual exit codes: 2 for a usage error (an error in
lokal.toml, an unknown SUB, a service you added by hand with another route, a different domain), and 1 for no
lokal.toml, a refused trust check, another lokal up running the project, or a port in use. When another
lokal up runs the project, the hint points at it: restart a service: lokal restart <SUB> -p shop; see its output: lokal logs <SUB>.
Services that lokal up manages
These are the services lokal up added or adopted.
lokal list,project showandservice listmark them with a dim(lokal.toml), and theirmanagedfield in JSON output is"lokal.toml".service updateandservice removestill work on them, with a note that the nextlokal upsets the service back, or adds it back.lokal run <SUB>reuses such a service as it is. Whilelokal upruns it, its port is in use, solokal runrefuses and names the process.
lokal down#
lokal down [--remove-project] [-p <PROJECT>]Removes the services that lokal up added or adopted, and the routes it wrote from [routes]. Other
services you added by hand stay.
| Flag | What it does |
|---|---|
--remove-project | Also remove the project, if nothing else is left in it |
-p, --project <PROJECT> | The project (default: the one in lokal.toml) |
$ lokal down
✓ Removed 3 services from lokal.toml (done in 30ms)
$ lokal down # with routes
✓ Removed 1 service and 2 routes from lokal.toml (done in 30ms)
$ lokal down --remove-project # also the project, if nothing else is left in it
$ lokal down -p shop- Without
-p, the project comes from thelokal.tomlthatlokal upwould find. Only its project name matters, so a file with other errors still names it. With no file and no-p,downexits 1 and suggestslokal down -p <NAME>. - It removes the services in one change and returns once their names are no longer served.
--remove-projectalso removes the project when nothing is left in it, and clears the current project if it was this one. If services you added by hand remain, it keeps the project and says so.- With nothing to remove, it says so and exits 0, so a repeated cleanup doesn’t fail.
- While
lokal upruns the project,downchanges nothing and exits 1:lokal up is running for shop (pid 1234); stop it first (Ctrl-C in its terminal). - If you rename the project in
lokal.toml, the old project’s services stay untillokal down -p <old name>.
lokal list#
lokal list [--json]Alias: lokal ls.
Shows every project with its services, URLs and upstreams, then its routes, if any. --json
is a stable contract for scripts and coding
agents (see JSON output).
$ lokal list
myproject myproject.lokal (current)
api https://api.myproject.lokal → localhost:4000
web https://web.myproject.lokal → localhost:3000
shop shop.lokal
(no services)A project still on .local (from before 0.6.0) isn’t served, and the list ends with a warning that names it, as
project list does.
Background service#
The background service is a LaunchAgent (dev.lokal.daemon) that runs as you. On Linux, it’s the systemd user
service dev.lokal.daemon.service, with three socket units that hold its ports. start, stop, restart and
upgrade need setup to have run. Before that, they exit with code 3. None of them asks for a password.
lokal restart <SUB> is about lokal up’s services instead, and doesn’t need setup.
lokal start#
lokal startStarts the background service and waits until it answers. If it’s already running, start says so and does
nothing. If another program holds one of lokal’s addresses, start, restart and upgrade print a warning:
that names it and a hint: with the fix, and still exit 0: the service runs on the other addresses
(Port 443 or 80 is already in use).
lokal stop#
lokal stopStops the background service. It stays installed, and starts again at your next login or with lokal start.
While it’s stopped, lokal URLs don’t work.
lokal restart#
lokal restart
lokal restart <SUB>... [-p <PROJECT>]Without SUB, restarts the background service and waits until the new process answers. launchd (on Linux,
systemd’s socket units) holds the ports, so connections that arrive during the restart wait in its queue instead
of failing. If the service couldn’t open an address when it started, because another program held it, restart
stops and starts the service instead, so it opens that address now that it’s free.
With SUB, it restarts those services of the lokal up that runs the project, and returns once their
URLs work again:
$ lokal restart web
✓ restarted https://web.shop.lokal (1.2s)- The running
lokal updoes the restart. It stops the service’s process group as Ctrl-C would (SIGINT, SIGTERM after 5 seconds, SIGKILL after 10), checks that nothing else took its port, and starts its command again, on the same port and with the same log. Its terminal showsrestarting web, thenweb ready (1.2s). - A service that isn’t running starts: one whose server exited, or one that
lokal upwasn’t asked to start. - It runs the command
lokal upstarted with, the one you approved. It doesn’t read alokal.tomlthat changed since, and says so: stoplokal upand start it again to use the new file. - When a service doesn’t come back, it says why. If the new server exits first:
error: web exited 1 before it was ready, the last 20 lines of that run’s log, andhint: lokal logs web -n 50. If something else listens on the port, the error names it, aslokal up’s does, and says when the service’s own command left it running. - The project comes from
-p, else from thelokal.tomlin the current directory or a parent, else from the current project, as forlokal logs <SUB>. With nolokal uprunning the project, it exits 1:no lokal up is running for shop. - Several services restart side by side, and the command returns when all of them are done. It exits 0 when
all are ready, 1 when one isn’t, and 2 for a service that isn’t in
lokal.toml. There’s no timeout: Ctrl-C stops only the waiting, andlokal upfinishes the restart.
| Argument or flag | What it does |
|---|---|
[SUB]... | The lokal up services to restart (@ for the apex). Without it, the background service |
-p, --project <PROJECT> | The project of SUB |
$ lokal restart # the background service
$ lokal restart web # web of the running lokal up
$ lokal restart web api -p shoplokal status#
lokal status [--json]Shows whether the background service is running, its build, its listeners and the state of every published name.
Addresses that another program held when the service started are listed under missing:. A project still on
.local (from before 0.6.0) gets a warning at the end, with the commands that move it to .lokal. It also works before setup, and then says that lokal isn’t set up yet. --json includes setup_done and
running, which scripts and coding agents can use to check that lokal is ready.
$ lokal status
$ lokal status --jsonlokal logs#
lokal logs [SUB [-p <PROJECT>]] [-f] [-n <LINES>]Shows the background service’s log, ~/Library/Logs/lokal/daemon.log (on Linux,
~/.local/state/lokal/daemon.log).
With SUB, it shows instead the log of a service that lokal up started: what the service
printed, each line after a timestamp, and lokal’s own lines about it. When a .lokal URL fails,
lokal logs <sub> -n 50 shows that service’s recent output.
- The project comes from
-p, else from thelokal.tomlin the current directory or a parent, else from the current project (how the project is chosen). So an agent in one repo reads that repo’s logs, even when another project ranlokal uplast.-pneeds aSUB. -ncounts back across the log and its older part,<SUB>.log.1.-fkeeps following when the nextlokal upstarts the log afresh, and when it moves to.log.1.- A log stays readable after its service is gone, for example after
lokal down. - No log: a service that
lokal updidn’t start, such as one added withlokal service addor bylokal run, prints to its own terminal and has no log. lokal says so and exits 1. A service that isn’t in the project is a usage error (exit 2). - Reading a service’s log doesn’t need setup, just as
lokal updoesn’t.
| Argument or flag | Default | What it does |
|---|---|---|
[SUB] | The lokal up service whose log to show (@ for the apex) | |
-p, --project <PROJECT> | The project of SUB | |
-f, --follow | Keep printing new lines | |
-n, --lines <LINES> | 50 | Number of lines to show |
$ lokal logs -n 200
$ lokal logs -f
$ lokal logs api -n 50 # what lokal up's api service printed last
$ lokal logs web -f -p shoplokal upgrade#
lokal upgradeMakes the background service run this lokal binary. It copies the binary to the service’s own copy, restarts
the service and waits until the new build answers. Run it after every brew upgrade lokal, or after you replace
the binary. Until you do, lokal warns:
the background service runs a different lokal build; run 'lokal upgrade'. On Linux, run it after you install a
newer .deb or .rpm too: the packages don’t run it.
$ lokal upgrade
✓ background service now running lokal 0.1.0 (70ec3e3), ready in 1.2sUtilities#
lokal env#
lokal env [--shell <sh|fish>]Prints shell exports so tools that ship their own certificate bundle (Node, Python, curl with CURL_CA_BUNDLE)
trust lokal’s certificate. Browsers and macOS tools already do. On Linux, so do programs that read the system
bundle, and Chrome and Firefox once setup has added the CA to their certificate databases.
| Flag | Default | What it does |
|---|---|---|
--shell <SHELL> | sh | Output syntax: sh (also right for zsh and bash) or fish |
$ lokal env
export NODE_EXTRA_CA_CERTS='/Users/you/Library/Application Support/lokal/ca/root.pem'
export SSL_CERT_FILE='/Users/you/Library/Application Support/lokal/ca/bundle.pem'
export REQUESTS_CA_BUNDLE='/Users/you/Library/Application Support/lokal/ca/bundle.pem'
export CURL_CA_BUNDLE='/Users/you/Library/Application Support/lokal/ca/bundle.pem'
$ eval "$(lokal env)" # zsh, bash
$ lokal env --shell fish | source # fishbundle.pem holds the macOS system roots plus lokal’s CA, so public HTTPS sites keep working. On Linux, it holds
the system bundle plus lokal’s CA, and the paths are under ~/.local/share/lokal/ca/. lokal env rebuilds it
when it’s older than 30 days. Node, Python and curl has the details.
lokal ca path#
lokal ca pathPrints the path of lokal’s CA certificate. On Linux, it’s ~/.local/share/lokal/ca/root.pem.
$ lokal ca path
/Users/you/Library/Application Support/lokal/ca/root.pem
$ NODE_EXTRA_CA_CERTS="$(lokal ca path)" node app.jslokal open#
lokal open <SUBDOMAIN> [-p <PROJECT>]Opens the service’s https:// URL in your default browser. @ opens the project’s own domain. On Linux, it
uses xdg-open (/usr/bin/xdg-open, from xdg-utils).
A name that a wildcard service covers opens too: with '*'
configured, lokal open tenant1 opens https://tenant1.myapp.lokal. The wildcard itself has no single URL, so
lokal open '*' fails (exit 2) with `*` is a wildcard service and has no single URL.
$ lokal open api
$ lokal open web -p shop
$ lokal open tenant1 # a name under '*'lokal help#
lokal help [COMMAND...]The same as --help. lokal help prints the root help, and lokal help service add prints the full help for
that command, with examples.
JSON output#
list, status, doctor, project list, project show and service list take --json. That output is a
stable contract:
- It is pretty-printed, has no colour codes, and always contains
"schema": 1. - Fields may be added, but they aren’t renamed or removed without a new schema number.
- A wildcard service has the same fields:
"subdomain": "*","fqdn": "*.myproject.lokal"and"url": "https://*.myproject.lokal". Thaturlisn’t a page you can fetch, so skip URLs that contain*.
lokal list --json#
lokal project list --json prints the same document.
{
"schema": 1,
"current_project": "myproject",
"projects": [
{
"name": "myproject",
"domain": "myproject.lokal",
"services": [
{
"subdomain": "api",
"fqdn": "api.myproject.lokal",
"url": "https://api.myproject.lokal",
"host": "localhost",
"port": 4000,
"host_header": "preserve",
"managed": null
}
],
"routes": [
{
"pattern": "{site}",
"fqdn": "{site}.myproject.lokal",
"to": "https://render.myproject.lokal/demosite/{site}"
}
]
}
]
}current_project is null when no project is current. A service’s managed is "lokal.toml" when
lokal up added it, and null otherwise. routes lists the project’s routes from lokal.toml, and
is [] without any. A route’s fqdn is its pattern, not a page you can fetch.
lokal project show --json#
One project, in the same shape as an entry of projects above, plus whether it’s the current project.
{
"schema": 1,
"current": true,
"project": {
"name": "myproject",
"domain": "myproject.lokal",
"services": [
{
"subdomain": "api",
"fqdn": "api.myproject.lokal",
"url": "https://api.myproject.lokal",
"host": "localhost",
"port": 4000,
"host_header": "preserve",
"managed": null
}
],
"routes": []
}
}lokal service list --json#
{
"schema": 1,
"project": "myproject",
"domain": "myproject.lokal",
"services": [
{
"subdomain": "api",
"fqdn": "api.myproject.lokal",
"url": "https://api.myproject.lokal",
"host": "localhost",
"port": 4000,
"host_header": "preserve",
"managed": null
}
]
}lokal status --json#
{
"schema": 1,
"setup_done": true,
"running": true,
"pid": 1234,
"version": "0.1.0",
"build_id": "0.1.0+abc1234.20261010T120000Z",
"uptime_secs": 42,
"listeners": [
"127.0.0.1:443",
"[::1]:443",
"127.0.0.1:80",
"[::1]:80",
"127.0.0.1:53535/udp"
],
"listeners_missing": [],
"listener_source": "launchd",
"name_backend": "dns",
"names": [
{
"fqdn": "api.myproject.lokal",
"state": "registered"
}
],
"certs_cached": 2
}A name’s state is registered, pending or failed: <reason>. listeners_missing lists the addresses the
background service couldn’t open when it started, usually because another program held them, such as
"127.0.0.1:443". When it isn’t empty, lokal doctor names that program. When the background service isn’t running,
running is false, listeners and names are empty, and the fields that describe the running process (pid,
version, build_id, uptime_secs, listener_source, name_backend) are left out. Check setup_done first:
if it’s false, the user needs to run lokal setup.
name_backend is dns on macOS and Linux: every name is a .lokal name, answered by lokal’s DNS server. Before
0.6.0 it was mdns-local-only+dns on macOS and none+dns on Linux. On Linux, listener_source is systemd.
lokal doctor --json#
{
"schema": 1,
"ok": true,
"checks": [
{
"id": "ca.trusted",
"status": "pass",
"detail": "macOS trusts lokal's CA",
"ms": 31
},
{
"id": "upstream.api.myproject.lokal",
"status": "warn",
"detail": "nothing listening on localhost:4000. Is your service running?",
"ms": 50
}
]
}ok is false when at least one check has "status": "fail". Warnings don’t affect it.
Exit codes#
| Code | Meaning |
|---|---|
0 | Success |
1 | Runtime error, or lokal doctor found at least one failing check |
2 | Usage error: an unknown command or flag, a value that fails validation, such as an invalid name or port, or an error in lokal.toml |
3 | Setup hasn’t run. Run lokal setup |
4 | Sudo would be needed but can’t run: LOKAL_FORBID_SUDO=1 is set, or there’s no terminal to ask for the password |
Human output goes to stdout, except lokal run’s own lines, which go to stderr because the dev server it starts
owns stdout. lokal up’s own lines go to stdout, and each service’s lines keep their stream: its stdout on stdout,
its stderr on stderr. Errors go to stderr as error: <message>, often followed by hint: <suggestion>. Apart from
setup, uninstall and lokal up’s trust check, no command ever prompts.
Once its command has started, lokal run exits with the command’s exit code instead (see
lokal run). Once its commands have started, lokal up exits with 128 + the signal that stopped it
(130 for Ctrl-C, 143 for SIGTERM), or, when the commands all exit on their own, with 0 if every one exited 0 and 1
otherwise (see How lokal up exits).
Before setup has run, project and service commands still save your changes and exit 0. They print a note that
lokal isn’t set up yet, and the URLs start working once you run lokal setup.
Environment variables#
| Variable | What it does |
|---|---|
NO_COLOR | Any non-empty value turns off coloured output, like --no-color |
LOKAL_HOME | Test hook. LOKAL_HOME=<dir> moves lokal’s data into <dir> and its logs into <dir>/logs, as a sandbox. lokal then never touches the real background service: setup, uninstall, start, stop, restart and upgrade refuse to run. lokal’s own integration tests use it |
LOKAL_FORBID_SUDO | Test hook. With LOKAL_FORBID_SUDO=1, a command that would need sudo fails with exit code 4 instead of asking for a password. The live test uses it to prove that re-running setup on a set-up Mac needs no password |
lokal env prints NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE for your other
tools. lokal itself doesn’t read them.