Troubleshooting localghost¶
Start with the hub status and logs:
If the CLI cannot reach Docker, use the raw commands:
docker ps --filter label=com.docker.compose.project=localghost
docker logs --tail=100 localghost-traefik-1
External network not found¶
Typical error:
The hub has not yet created its shared network. Run uvx localghost hub up
once, then rerun the application's docker compose up command.
Do not change the application network to a normal, implicitly created network. That would create a project-scoped network that Traefik cannot share reliably.
Network exists but every route is unavailable¶
Consumer containers can run while the hub container is stopped. Check that
the hub is running and healthy with ps, then inspect its logs. Reconcile it
with uvx localghost hub up if needed.
Also confirm the URL uses the configured LOCALGHOST_HTTP_PORT when it is
not 80.
Route returns 404¶
A Traefik 404 page not found usually means no router matched the request.
Confirm that:
- the project name rendered by Compose is unique and DNS-safe;
- the container has
traefik.enable=true; - it is attached to the external
localghostnetwork; traefik.docker.network=localghostis present;- the router name is unique; and
- the
Host(...)rule exactly matches the browser hostname.
Inspect resolved labels rather than only the source file:
docker compose config
docker inspect "$(docker compose ps -q web)" --format '{{json .Config.Labels}}'
The dashboard at http://traefik.localhost should list the expected router.
An unlabelled container intentionally produces a 404.
A hostname shows "is offline" instead of my app¶
The hub remembers hostnames it has routed before. If the application behind
one is stopped or was never started this session, the hub answers with a
ghost page (503 Service Unavailable) instead of routing to it. Start the
application with localghost run, or drop the stale entry with
localghost forget <name> if the project is gone for good.
Route returns 502¶
A 502 normally means the router matched but Traefik could not reach a valid backend. Confirm that:
- the application listens on
0.0.0.0, not container loopback; - the load-balancer label uses the application's container port;
- the process is actually listening on that port;
- the container is running; and
- both Traefik and the container are attached to
localghost.
Inspect network membership with:
Application logs usually reveal crashes or bind-address mistakes:
Port 80 is already allocated¶
Identify Docker containers already publishing the port:
Stop the conflicting listener if appropriate, or use
LOCALGHOST_HTTP_PORT consistently as described in
Operating the hub. The hub binds to
127.0.0.1, but a process bound to 0.0.0.0:80 still conflicts with it.
HTTP works but HTTPS does not¶
Inspect the managed trust and listener state first:
HTTPS requires mkcert, an installed Localghost public root, and a running hub
with the websecure entrypoint enabled. The application also needs a secure
router with websecure, its normal Host(...) rule and service, and tls=true.
Generated configurations include this router; compare hand-written labels with
Optional HTTPS.
If a custom LOCALGHOST_HTTPS_PORT is configured, include it in the URL. When
only a browser rejects the certificate, restart it and recheck its system or NSS
trust store. When every client fails to connect, inspect the hub logs and host
port publication instead.
Hostname resolution or HTTP proxy problems¶
.localhost is a special-use loopback domain, but local resolver or corporate
HTTP proxy settings can still interfere. Separate routing from resolution by
sending the Host header directly to loopback:
If this works but http://my-project.localhost does not, investigate the host
resolver, browser secure-DNS settings, VPN software, and HTTP_PROXY,
HTTPS_PROXY, or NO_PROXY environment variables.
For a non-default port, include it in the loopback URL and browser URL.
Route already exists¶
localghost run refuses to replace an existing container route for the
same hostname. Stop the other application normally, or inspect the reported
container and remove a stale bridge explicitly:
The foreground command cleans up its bridge on exit, Ctrl+C, and SIGTERM. A hard kill or Docker failure can still leave a stale container.
Framework rejects an otherwise working route¶
An application-generated invalid-host, CSRF, CORS, or origin error is outside Traefik routing. Add the generated hostname and origin to the framework's local development settings. See Framework configuration.
The most common shape of this is a Django project where browsing works and
every form post returns 403 Forbidden: GET requests never consult
CSRF_TRUSTED_ORIGINS, so routing looks healthy until the first POST. A
related pair, when the hub serves HTTPS, is a login that silently never stays
logged in, or http:// links generated inside an HTTPS page — both mean
Django has not been told to trust the hub's forwarded scheme. See
Django for all three settings.
For localghost run, Django needs its generated <name>.localhost in
ALLOWED_HOSTS and, when applicable, CSRF trusted origins. Vite HTTP, HMR, and
WebSocket traffic use the same bridge, and pass through the loopback relay
unaltered when one is in use.
The application is listening somewhere the hub cannot reach¶
The application bound loopback only, and no relay could be raised from the
Docker gateway address to stand in for a wider bind. Localghost stops the
application rather than leave it running behind a URL that cannot resolve.
Make it listen on all interfaces. For a Vite or Astro project the usual cause
is a dev script that wraps the tool and drops the --host localghost passes:
forward "$@" to the tool in the script, or name the command directly:
Host port is already in use¶
Something else is listening on the port the run planned, so run refuses to
start rather than put its URL in front of whatever is already there. On Linux
the error names the process and its working directory. Stop that process, or
pass another port with --port. Without --port, the run walks to the next
free port and warns about the one it skipped; a dev server that then ignores
--port and comes up on its own default is the wrapper-script case described
in Wrapper scripts.
The application is listening on another port¶
The run planned one port and the application came up on another, usually
because a dev script wraps the tool and drops the --port localghost passes,
so the server starts from its own default instead. On a native Linux Docker
daemon, when exactly one address is listening and it sits in the range the
tool would pick for itself, the run relays the planned port to it and says so.
Otherwise it stops the application rather than leave a URL that cannot
resolve, and the error lists every address the application's processes are
listening on.
Forward "$@" to the tool in the wrapper, or name the command directly:
A custom command has to listen on the port it is given: include {port} in it,
or pass --port to match where it already listens. See
Wrapper scripts.
The OAuth credential was not stored¶
localghost tailscale enable warns when it cannot save the credential in the
operating system keyring. Everything else still works; the cost is that
tailscale disable (and a later re-enable) will prompt for the client id and
secret again, or read them from TAILSCALE_CLIENT_ID and
TAILSCALE_CLIENT_SECRET.
The usual cause is a headless or minimal Linux session with no Secret Service
provider on D-Bus. Install and unlock one — gnome-keyring or KWallet on a
desktop, or keyring alternatives such as keyrings.alt where a desktop
service is not an option — and re-run enable, or simply keep supplying the
environment variables. A stored credential lives under the keyring service
name localghost-tailscale and can be inspected or removed with the system's
own keyring tools; localghost tailscale disable also deletes it.
The tailnet gateway is not ready¶
When the hub starts but only tailscale-gateway fails its healthcheck —
typically because the machine is offline or Tailscale is unreachable —
localghost warns and continues with the local .localhost routes. The
gateway container keeps retrying in the background; localghost tailscale
status shows its progress under Gateway health. No action is needed once
connectivity returns.
Split DNS already points at another address¶
localghost tailscale enable refuses to replace an existing split-DNS entry
for the chosen suffix, naming the addresses it found. Usually another machine
is already hosting that suffix: pick a distinct --suffix, or pass
--takeover to replace the mapping deliberately — the other machine's routes
for that suffix stop resolving when you do.
Hub start fails after pruning Docker volumes¶
Removing the localghost-*-ca-* volumes deletes the certificate signers that
Traefik's providers require. A failed HTTPS start now re-runs the idempotent
CA bootstraps once and retries automatically. If the offline root volume was
among those removed, the recreated tailnet authority is a new identity and
every client must run localghost tailscale trust again; the fingerprint in
localghost tailscale status shows whether it changed.
Terminal only scrolls part of the screen¶
After a localghost run was killed outright — kill -9, a crashed terminal
emulator, an out-of-memory kill — the shell scrolls only its upper rows and a
stale Localghost status bar sits frozen on the last line.
The status bar reserves the bottom row by setting a terminal scrolling region
over the rows above it, and releases that region when the run ends. The
release cannot run when the process is killed without a chance to clean up:
ordinary exits, Ctrl+C, SIGTERM, and errors all restore the terminal, but
nothing survives SIGKILL. The terminal is not damaged; it is still holding
the region.
Release it:
reset also clears it, at the cost of wiping the screen and its scrollback.
To avoid the region entirely, run with --no-status-bar as described in
Running host applications.