name: hd-ports description: How HD does port forwarding between your workspace machine and your Mac — Lima's 0.0.0.0 port exposure (no Docker -p map), the code-server /proxy/<port>/ URL pattern that exposes any internal port, the port-forward registry for 127.0.0.1-only services that macOS won't expose, and the PortMappingsDialog UI. Use when the user asks "why isn't my server reachable", "how do I expose port X", "what's localhost:7380", "register a port", "expose a port", or "the dynamic port dialog". Trigger words — port forwarding, ports, hd ports, container port, proxy port, code-server proxy, /proxy/, port mappings dialog, port hints, expose port, register port, host port, dynamic port, ports.json, PortConfig, port resolver, localhost port not working.

HD Ports — forwarding workspace services to your Mac

This is the machine-runtime version (default). For the legacy Docker container runtime (HD_RUNTIME=docker) see the docker/ bucket.

The HD workspace is the Adom-Workspace machine (a systemd-nspawn machine booted inside the hd-builder Lima VM), not a Docker container. There is no Docker -p port mapping — Lima exposes the machine's listeners to the Mac for you. Any service that binds 0.0.0.0:<port> inside the machine is reachable at localhost:<sameport> on the Mac automatically. code-server itself binds 0.0.0.0:7380, so localhost:7380 on the Mac just works. For browser-facing HTTP/WS, code-server's /proxy/<port>/ URL pattern is still preferred and still exposes any internal port. For the cases Lima's exposure can't cover (127.0.0.1-only listeners, or when exposure is flaky), HD's port-forward registry creates a real host-side proxy on demand.

The first rule: bind 0.0.0.0

Bind address inside machine Reachable on the Mac?
0.0.0.0:N (all interfaces) YES — localhost:N automatically
127.0.0.1:N (loopback only) NO — Lima won't expose it

So before you do anything else: make your service bind 0.0.0.0. For most dev servers that's a flag (--host 0.0.0.0, vite --host, HOST=0.0.0.0, server.address("0.0.0.0")). Once it binds 0.0.0.0, Lima exposes it to the Mac — no registration needed.

The code-server proxy URL pattern

Any service listening on port N inside the machine is reachable at:

http://localhost:7380/proxy/N/

This works for HTTP services. WebSockets work too (ws://localhost:7380/proxy/N/). No port mapping needed, and it works for 0.0.0.0 AND 127.0.0.1-bound services (code-server is inside the machine, so it reaches loopback listeners that the Mac can't). This is the preferred path for anything you open in a browser.

Examples:

  • Your dev server on port 5173 → http://localhost:7380/proxy/5173/
  • The relay HTTP server on port 8766 → http://localhost:7380/proxy/8766/
  • adom-vscode's API on port 8821 → http://localhost:7380/proxy/8821/

This is how everything-inside-the-machine reaches everything-outside. Inside the machine, services that need to talk to "the host" use the adom-host gateway (adom-host:<port>) — macOS/vz does NOT mirror loopback, so 127.0.0.1 does NOT reach the Mac; the machine's /etc/hosts maps adom-host to the gateway — for HD's own services (HD's live control URL is in ~/.adom/hd-control-url), and the proxy URL for everything else.

The port-forward registry (for what Lima's exposure can't cover)

Lima's port exposure handles the common case. You still need the registry — HD's port-forward registry, serviced by the port-watcher daemon (see hd-port-watcher) — when:

  • The service binds 127.0.0.1 only and you can't change it (some OAuth callback servers, some native tools). Lima won't expose loopback-only listeners; the registry creates a real host proxy that reaches them through code-server.
  • Lima's port exposure is lagging or broken for a port (a real HD failure mode: "code-server alive in the machine but the Mac can't reach 127.0.0.1:7380"). The registry gives a reliable host-side proxy that doesn't depend on Lima's port exposure.

Decision order:

  1. Bind 0.0.0.0 → exposed by Lima, done.
  2. Can't bind 0.0.0.0 (loopback-only), or exposure is unreliable → register the port so HD proxies it.

Register a port (from inside your workspace)

# Tell HD: "please bind localhost:5555 on the Mac and forward into the machine"
adom-cli port-forward register 5555 --name "oauth-cb" --reason "127.0.0.1-only oauth callback" --visibility external

Visibility options:

  • external — bind localhost:5555 on the Mac via an HD-managed proxy AND keep the /proxy/ URL working. Best for OAuth callbacks, native clients, loopback-only servers.
  • internal — proxy URL only, no host binding. Best for webview panels that don't need a separate host port.
  • either — let HD decide based on heuristics (defaults to external).

Unregister

adom-cli port-forward unregister 5555

List

adom-cli port-forward list

The Port Mappings dialog

Open via: HD Adom menu → "Port Mappings", or POST http://127.0.0.1:47084/setup/panel/show then navigate to the Ports tab. Shows:

  • Exposed ports0.0.0.0 listeners Lima is exposing to the Mac
  • Registered ports — anything you've registered via port-forward register (HD-managed host proxies, for loopback-only / unreliable cases)

Each row shows the direct URL (http://localhost:<port>) AND the proxy URL (http://localhost:7380/proxy/<port>/). Click either to open in your browser via the Browser Picker.

HD's host-side port registry

HD reads/writes its host-side port assignments at ~/Library/Application Support/hydrogen-desktop/ports.json. The PortConfig struct (hd-control/src/ports.rs) holds exactly four service ports — code_server, proxy, control, cdp — plus the bind hostname. There are no relay-WS / relay-HTTP host ports in PortConfig: in the machine the relay binds 8765 (WS) / 8766 (HTTP) inside the workspace machine, reached at the same port on the Mac (Lima port exposure) or via /proxy/8766/, and discovered from ~/.adom/cli-relay-ports.json inside the machine. There is no host 47081/47082 listener.

Defaults (you can override via ports.json):

Service Default host port
code-server 7380
Discovery server 47080
Reverse proxy (proxy) 47083
HD control API (control) 47084
WebView2 CDP (cdp) 47085
KiCad bridge dynamic (adom-desktop picks at bridge launch)
Fusion bridge dynamic (adom-desktop picks at bridge launch)
Puppeteer bridge dynamic (adom-desktop picks at bridge launch)

Conflict resolution is NOT "next sequential port." On startup PortConfig::resolve_available() checks proxy, control, and cdp; if a preferred fixed port is taken, HD binds an ephemeral 127.0.0.1:0 port (the OS hands back a guaranteed-free one) and persists it to ports.json. It does not walk to 47086, 47087, …. code_server is exempt and never resolved (it's tied to the machine's exposed port). So a busy box does NOT predictably land on 47083/47085 — the AI must discover the live ports.

Never hardcode 127.0.0.1:9001 in machine code — that was the old default and has since moved. To find the real runtime ports, hit GET /ports on the control API (default 127.0.0.1:47084) — it returns {code_server, proxy, control, cdp, hostname} straight from the resolved PortConfig (hd-app/src/lib.rs get_ports). ports.json is the single source of truth every consumer reads.

Inside vs outside — which URL to use

You're calling from To reach a service on port N Use
Machine shell / app Another machine service http://localhost:N
Machine app The macOS host http://adom-host:N (vz does NOT mirror loopback — reach the host via the adom-host gateway alias; the host binds 0.0.0.0, no firewall rule)
Mac browser A 0.0.0.0-bound machine service http://localhost:N (exposed by Lima)
Mac browser Any machine service (HTTP/WS) http://localhost:7380/proxy/N/ (proxy URL, preferred)
Mac browser A 127.0.0.1-only machine service http://localhost:N (only after port-forward register ... external)
Machine HD's own control API (on macOS host) $(cat ~/.adom/hd-control-url)http://adom-host:<dynamic> (port is dynamic per launch)

Common bugs

"My dev server at port 5173 doesn't load in Chrome"

  • Most likely it's bound 127.0.0.1 only. Restart it bound to 0.0.0.0 (e.g. vite --host) and Lima exposes it at localhost:5173.
  • OR use the proxy URL: http://localhost:7380/proxy/5173/.
  • If it IS on 0.0.0.0 but still unreachable, Lima's port exposure may be lagging — register it: adom-cli port-forward register 5173 --visibility external.

"OAuth callback fails with refused connection"

  • The provider is hitting http://localhost:5555/callback but the callback server is loopback-only inside the machine, so Lima doesn't expose it.
  • Register first: adom-cli port-forward register 5555 --visibility external --reason "oauth callback".
  • HD auto-detects callback URLs and starts a proxy for them automatically in many cases (see HD log for [oauth] Callback port X detected), but registering explicitly is the reliable path.

"localhost:7380 shows nothing / code-server unreachable from the Mac"

  • code-server is alive in the machine but Lima's port exposure wedged (a real HD-logged failure mode). The port-forward registry / port-watcher daemon installs a host-side proxy to recover this — see hd-port-watcher. Or restart HD to re-establish the forward.
  • hd-networking — the broader picture: 0.0.0.0 Lima exposure, hostnames, proxy architecture, relay vs direct connect, the adom-host gateway, NEVER do these
  • hd-port-watcher — the retained daemon that registers 127.0.0.1-only listeners and covers Lima port-exposure gaps
  • hd-container — what's inside the workspace reachable on which port
  • hd-desktop-sse — the workspace API which runs on these ports
  • hd-browser-picker — what happens when you click a port-mappings URL

Bridge ports are dynamic

KiCad, Fusion 360, Puppeteer, and any 3rd-party bridges no longer have known fixed ports. When a bridge launches, adom-desktop picks an available port at runtime and registers it. To find a running bridge's current port, query adom-desktop:

adom-desktop status
# Look at desktop.apps.<bridge>.bridgePort

Or use the bridge directly via adom-desktop CLI — you don't need the port at all:

adom-desktop kicad_open_board '{"path":"..."}'  # adom-desktop routes to the right port
adom-desktop browser_eval '{"js":"..."}'

This means old code or skill examples that hardcoded fixed bridge ports (8771 / 8773 / 8851) are stale. Use the adom-desktop CLI or status lookup instead.