Publishing a port on the host
Bind a real host port to a server running inside the box, so curl, your browser and any local tool can reach it — no relay, no service worker.
A box's ports are entries in a map, not sockets, so nothing outside the process can reach them. In the browser that's what the service worker or the SW-free preview is for. On a host with real sockets — the CLI, or any Node program — you can just bind a port:
lifo --expose 3000 # in-VM 3000 → http://127.0.0.1:3000
lifo --expose 3000:5000 # in-VM 3000 → http://127.0.0.1:5000
lifo --detach --expose 5173 # background sessions tooThen curl http://127.0.0.1:5000/ reaches the server inside the box, and so does
your browser, Postman, or anything else on the machine. Repeat --expose for
several ports.
From code
import * as http from "node:http";
import { exposePort } from "@lifo-sh/core";
const exposed = await exposePort(sandbox.kernel.portRegistry, {
vmPort: 3000,
hostPort: 5000, // omit for an OS-assigned free port
http, // node:http, injected
});
console.log(exposed.url); // http://127.0.0.1:5000
await exposed.close();http is passed in rather than imported, exactly as
NativeFsProvider takes its fs: @lifo-sh/core is bundled
for the browser, and a hard node:http dependency would either break that build
or need externalising.
| option | default | notes |
|---|---|---|
vmPort | — | the in-VM port to publish |
hostPort | 0 | 0/omitted lets the OS pick; read it back from exposed.hostPort |
host | 127.0.0.1 | loopback only unless you ask otherwise |
timeoutMs | 120000 | passed to the dispatcher |
onRequest | — | (method, url, status) hook, for logging |
What gets forwarded
- HTTP, through the same dispatcher every other transport uses — so slow servers, timeouts and unbound ports behave identically.
- WebSockets, forwarded at the byte level. The client's socket is handed to the in-VM server, which performs its own RFC 6455 handshake, so HMR works and nothing in between has to parse a frame.
Binding does not require the server to exist yet. A forwarder set up before
your dev server starts answers 404 with x-lifo: no-server until the port is
live — friendlier than a connection refused while something is still booting. The
CLI binds forwards before the shell starts for exactly that reason.
Gotcha
Loopback by default, deliberately: a box runs project code, and publishing it on
every interface is something you should have to ask for. Pass host: '0.0.0.0'
only when you mean it.
This is not the same as tunnel
They solve different problems and compose:
| reaches | needs | |
|---|---|---|
--expose / exposePort() | a local host port | nothing — no relay, no network, works offline |
tunnel 5173 (inside the shell) | a public URL | the tunnel.lifo.sh relay, network, maybe auth |
So share an in-VM port publicly with the in-shell tunnel
command; publish it to your own machine with --expose. If you want a host port
shared publicly, expose it first and then point lifo tunnel <hostPort> at it —
host-side lifo tunnel proxies a host port and cannot reach into the box on its
own.