localghost architecture¶
Localghost separates a machine-wide concern from individual application checkouts. One long-lived Traefik container — the hub — owns the host HTTP port; each application remains a separate Docker Compose project, connected to the hub by a bridge: a generated Caddy container for host runs, or routing labels and network membership for Compose runs.
Ownership boundaries¶
The hub Compose project owns:
- the fixed Compose project name
localghost; - one Traefik container;
- the fixed Docker network
localghost; and - loopback publication of the configured HTTP port;
- optional loopback publication of the configured HTTPS port; and
- persistent private CA volumes when trusted HTTPS has been bootstrapped.
An application Compose project owns its containers, default network, data, and
application-specific configuration. It only references localghost as an
external network. Application lifecycle commands must never include the hub
Compose file, so stopping or rebuilding an application cannot recreate the
hub.
Multiple applications—and multiple checkouts of the same application—can run at once when every checkout has a unique Compose project name.
Request path¶
For a request to http://my-project.localhost:
.localhostresolves to the local loopback interface.- Docker forwards the loopback-bound host port to Traefik's
webentrypoint. - Traefik matches the request
Hostheader against opted-in container labels. - Traefik connects to the selected container over the shared
localghostnetwork and its explicitly labelled port.
No application host port is needed in this path. Application containers may also retain their ordinary Compose default network for databases and other private dependencies.
Docker delivers step 2's connections from an address of its own — the bridge
gateway on a native daemon, the VM's gateway under Docker Desktop — rather
than loopback. A middleware plugin on every entrypoint renames an IPv4 source
that is the gateway, or is on none of Traefik's networks and is not a
Tailscale address, to 127.0.0.1 before Traefik writes the forwarded headers.
Containers on the shared network and tailnet devices keep their own addresses.
Discovery and isolation¶
Traefik reads Docker metadata through a read-only bind mount of
/var/run/docker.sock. The Docker provider is configured with:
exposedByDefault=false, so unlabelled containers receive no route;network=localghost, so backend traffic uses the shared network; and- explicit consumer labels for router rules and backend ports.
The network setting controls Traefik's connection path; it does not prevent Traefik from seeing Docker metadata. See Security and trust for that distinction.
Names and hostnames¶
Compose automatically derives each application project name from its checkout directory. That name forms part of public local hostnames and Traefik object names, so it must be unique and contain only lowercase letters, digits, and hyphens. An explicit override is needed only when the derived name does not meet those conditions or is not the desired hostname.
| Role | Hostname | Router/service name pattern |
|---|---|---|
| Primary service | <project>.localhost |
<project>-web |
| Secondary service | <service>.<project>.localhost |
<project>-<service> |
| Hub dashboard | traefik.localhost |
localghost-dashboard |
These conventions make router and service identifiers unique across attached
Compose projects. .localhost is used because it is reserved for loopback and
does not require a wildcard DNS server.
Dashboard and health¶
Traefik's internal API service provides the dashboard through the normal web
entrypoint. The bare http://traefik.localhost URL redirects to
/dashboard/. Port 8080, used by Traefik's insecure API mode, is neither enabled
nor published.
The Traefik container health check calls traefik healthcheck --ping against
the enabled internal ping endpoint. Health describes the hub process; it does
not guarantee that every consumer application is healthy or correctly labelled.
Optional HTTPS path¶
HTTP remains the baseline entrypoint. After localghost trust install adds the
hub's public development root, the hub also publishes the websecure
entrypoint on loopback and loads its local certificate provider. The private
root and online signer stay in Docker volumes; only the public root is copied to
host state and passed to trust-store tooling.
Applications opt into HTTPS with a second, project-scoped router using the same
hostname and backend service as the HTTP router. The secure router selects the
websecure entrypoint and sets tls=true. The save workflow and checked-in examples
include both routers, so enabling or disabling the hub's HTTPS configuration
does not require regenerating application configuration.
Host-native applications¶
The CLI creates an ephemeral Caddy bridge for an HTTP process running directly
on the host. The bridge joins the shared network, carries the ordinary Traefik
labels, and forwards requests to host.docker.internal. This keeps
host-specific routes out of the persistent hub configuration.
The host process must be reachable from the bridge. On a native Linux daemon a
process bound only to loopback is not, so a foreground run relays to it from
the Docker gateway address; elsewhere the route to the host is proxied by a
process that reaches loopback. See
Loopback-bound servers.
localghost run creates the bridge model in memory with a unique internal
project and foreground ownership labels. Its
literal <name>-app Traefik objects disappear with the child process; the
hub is retained. save records repeatable host run settings in
.localghost.toml; it does not make the bridge itself persistent.
The hub is always started as the fixed localghost Compose project, even
when a host application sets COMPOSE_PROJECT_NAME for its own route.
Optional tailnet path¶
Tailnet hosting adds a dedicated userspace Tailscale gateway to the hub. It owns no host ports: tsnet supplies its tailnet listeners for DNS, HTTP, and HTTPS. Split DNS sends only the configured one-label suffix to the gateway. The gateway answers supported route depths with its own Tailscale addresses, forwards HTTP to Traefik with the original Host header and the client's tailnet address, and passes TLS through unchanged behind a PROXY protocol header naming the client, so Traefik still selects the certificate and router by SNI and still knows who connected. Traefik believes both only from private addresses, and only while tailnet hosting is enabled.
A second instance of the local certificate provider watches the same opted-in
Docker labels. It derives suffix-specific certificates and provider routers
from the .localhost model, while explicitly referring back to Docker-owned
services and middleware. This keeps one routing source of truth: applications
do not acquire Tailscale labels and cannot drift between the two hostname
systems.
The gateway is a separate container from Traefik on purpose. Traefik mounts
the Docker socket and must never be tailnet-reachable code; the gateway is
tailnet-reachable and therefore runs from a scratch image with a read-only
filesystem, no capabilities, no socket, and only the suffix authority's
public material mounted. Both belong to the one localghost Compose project,
so localghost hub up, localghost hub down, and reconciliation own them together.
Because the image ships no shell, the gateway's Docker healthcheck is the
gateway binary probing its own loopback health listener, which starts only
after the tsnet node is enrolled and every tailnet listener is up.
localghost tailscale status reports that observed container state alongside
the saved configuration.