Web Servers and Ports
How to reach a web server running inside a sand VM — a ddev project,
npm run dev, python3 -m http.server, anything listening on a TCP port.
The answer depends on where the VM lives: on the machine you're sitting at
(the Local profile), or on a remote host (a remote-ssh
Connection Profile).
Local VMs: ports appear on localhost
Lima automatically forwards every TCP port that starts listening inside the
guest to 127.0.0.1 on your machine, same port number. sand doesn't add
or restrict anything here — it inherits Lima's default behavior — so a
server started in the guest:
is immediately reachable at http://localhost:8000 on your machine. No
configuration, no flags: forwarding follows the guest's listening sockets
automatically and goes away when the server stops.
Two properties worth knowing:
- Loopback only. Forwarded ports bind
127.0.0.1, never a LAN interface. Running a server in a VM does not expose it to your network — see the security model. - Privileged ports on Linux hosts. Lima runs unprivileged, and on a
Linux host an unprivileged process cannot bind ports below 1024 — a guest
server on port 80 or 443 is silently skipped (Lima logs a warning in the
instance's
ha.stderr.log). macOS hosts don't have this restriction.
ddev URLs
ddev project URLs work from your host browser as-is on
a macOS host: *.ddev.site resolves to 127.0.0.1 in public DNS, ddev's
router listens on ports 80/443 in the guest, and Lima forwards them to your
loopback — so https://myproj.ddev.site just works.
On a Linux host, ports 80/443 hit the privileged-port rule above. Either allow unprivileged binds on the host:
or move ddev's router to high ports inside the guest and keep everything unprivileged:
claude@myvm$ ddev config global --router-http-port=8080 --router-https-port=8443
claude@myvm$ ddev restart
after which the project URL is https://myproj.ddev.site:8443.
Expect a certificate warning
HTTPS certificates are minted by mkcert inside the guest, so the
guest trusts them (Claude's own requests succeed) but your host browser
does not. Either click through the warning, or trust the guest's root
CA on your machine: mkcert -CAROOT in the guest names the directory
holding rootCA.pem; copy it out (the g verb, or the download CLI —
see Shells and Files) and add it to your OS or
browser trust store.
Remote VMs: tunnel out with cloudflared
On a remote profile the same Lima forwarding happens — but to 127.0.0.1
on the remote host. That's a machine you can SSH into, not the one your
browser runs on, so a guest web server is reachable from nowhere except the
remote host itself. sand adds no port forwarding of its own over the
profile's SSH connection, and the
security model assumes remote hosts sit
on an isolated network behind whatever firewalling you already have.
The supported way in is outward: run cloudflared — preinstalled in every VM — inside the guest, and let it hand you a URL:
This starts a Quick Tunnel
— no Cloudflare account, no configuration — and prints a random
https://….trycloudflare.com URL that works from any browser, anywhere,
for as long as the process runs.
ddev projects
For a ddev project, don't build the tunnel by hand — ddev's router routes
by the request's Host header, which a bare tunnel to a port doesn't
preserve. Let ddev share
do it, pointed at cloudflared:
This runs the same cloudflared already in the VM (no account, no
authtoken), wires up the Host header and TLS to the router for you, and
prints the public trycloudflare.com URL. Make it the default so plain
ddev share uses it:
(ddev's out-of-the-box default provider is ngrok, which needs an ngrok.com
account and ngrok config add-authtoken first; cloudflared avoids both.)
A quick tunnel is a public URL
Anyone who has the URL can reach the server — there is no
authentication in front of it. The URL is random and unlisted, but
treat it as public: don't leave a tunnel running unattended, and
remember that everything a sand VM serves may be agent-written.
Quick Tunnels are also rate-limited and intended for testing, not
hosting.
Nothing stops you from using cloudflared on a local VM too — it's the easiest way to show in-progress work to someone who isn't at your desk.
What about SSH port forwarding?
For a private path to a remote VM — no public URL — plain OpenSSH forwarding works today, using the connection details already in your profile:
While that runs, localhost:8443 on your machine reaches the remote host's
loopback port 8443 — that is, a guest port Lima forwarded there. For the
high-ports ddev configuration above, https://myproj.ddev.site:8443 then
works from your browser: *.ddev.site still resolves to 127.0.0.1, where
the tunnel now listens, and the router still sees the project's name in the
Host header. The mkcert certificate warning applies exactly as with a
local VM.
The catches, and why this recipe is manual:
- The remote end must exist. The remote hosts
sandtargets are Linux, so the privileged-port rule applies there: out of the box Lima has not forwarded a guest's port 80/443 to the remote loopback at all. Move ddev to high router ports (easiest), or raisenet.ipv4.ip_unprivileged_port_starton the remote host. - The local end has the same rule. Binding your local port 443 (to
make a bare
https://myproj.ddev.sitework) needs root on a Linux laptop; macOS allows it unprivileged. A high local port with the:8443URL suffix avoids the question entirely. - One
-Lper port, one process to babysit. The forward lives and dies with thatsshprocess.
Connection profiles have no field for declaring port forwards — sand
neither creates nor manages SSH tunnels. If a guest port matters to your
workflow, script the ssh -L invocation yourself, or prefer cloudflared,
which needs nothing from the machine you're browsing on.