Skip to content

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:

  1. .localhost resolves to the local loopback interface.
  2. Docker forwards the loopback-bound host port to Traefik's web entrypoint.
  3. Traefik matches the request Host header against opted-in container labels.
  4. Traefik connects to the selected container over the shared localghost network 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.