In these strange times I often find myself building more than one feature at a time in different git worktrees. And in these strange times, the craft of engineering feels more about designing feedback loops for myself and for AI than it is about writing code.
Me: Can I lay eyes on what you are doing, Claude?
Claude: This is a great time for that question, and that’ll cost you $0.12.
Me: I want to see my app running in each git worktree you’re building in, Codex. I have hot reload, show me what you’re thinking about so I can stop you if you’re building the wrong thing.
Codex: It appears that the UI you have been staring at is connected to the wrong backend. I need to pause here because your feedback may have been based on backend behavior from a different git worktree.
Sighing, I paused them and went to the porch to clear my head.
My dog’s name is Duchess. She is a golden retriever. As came over, she opened one eye and wagged her tail.
Duchess: What’s wrong with you?
Me: I just wasted twenty minutes testing the wrong thing. My browser was confused, talking to the wrong backend.
Duchess: How does a browser get confused about that?
Me: Because both versions are just numbers. Port 3000, port 3001. I have to remember which is which, but I didn’t.
Duchess: That sounds like the problem exists between the keyboard and the chair.
Me: Okay smarty. But everyone hits this. If I want to see the exact difference they’re both reaching for the same number.
Duchess: So don’t run two.
Me: I need to compare them! I need to know if one of the agents is building the wrong thing, ASAP! That’s the whole point!
Duchess: Okay, okay. So the number is the problem?
Me: The number IS the address.
I thought for a second, trying to explain it in doggy terms.
Me: The treat dispenser has a number, and I have to remember it, or this happens again.
Duchess: Why not just call it something? Old-version, new-version. Whatever you want.
Me: Like a name instead of a number?
Duchess: Sure. “Treats,” not “3000.”
Me: …You just invented DNS.
Duchess: Like you said, I’m very smart.
Me: But it isn’t possible to have two processes listening on the same port but with different names on my computer.
Duchess: Has this happened to other people, or is it just you?
Me: Oh, constantly. My favorite is that time someone updated their Mac and suddenly Control Center was squatting on port 5000, a port half of Silicon Valley had already agreed belonged to Flask.
Duchess: Have you ever heard the expression “possession is nine tenths of the law”? It sounds like ports are valuable resources that anybody can take at any time so nobody can own.
Me: You’re mostly right…although there is an organization called the IANA, that “registers” ports for people. But nobody has to abide by it.
Duchess: It seems kind of silly to have a bunch of bureaucracy around port numbers instead of just using names and namespaces and things like that.
I began thinking systematically about this problem and realized, half of the solution was already there, and the other half was obvious. The half already there is this:
- When starting any kind of TCP server, you can bind to port 0 and the operating system will assign your process a random, available port.
Turns out this works for Docker containers as well—something I did not know!
The other half? I built that. Then I tested it and refined it; and now I’m releasing it to the public. I call it PortZero.
How it works
You tell your program to bind to port 0. The catch, obviously, is that now you don’t know which port your program landed on either. That’s where Port Zero comes in. You start your program with a PZ_TUNNEL environment variable that says what name you want it known by:
PZ_TUNNEL=master.mytodoapp.portzero.local:80 PORT=0 npm start dev
A small background daemon watches for processes and Docker containers with that environment variable. This background daemon creates a virtual network interface and a virtual IP address, wires up a DNS record for the name you chose, and forwards the port you specified in PZ_TUNNEL to whatever random port the OS actually handed your process. The result: http://master.mytodoapp.portzero.local just works in your browser, and the number underneath is nobody’s problem.
This also quietly solves the worktree problem. Testing a branch in a second git worktree? Give it its own tunnel name:
PZ_TUNNEL={branch}-mytodoapp.portzero.local:80
PortZero detects the current working directory of the process or container, detects the git branch, and uses that as a substitute for the {branch} expression in PZ_TUNNEL. Now master-mytodoapp.portzero.local and feat-auth-mytodoapp.portzero.local run side by side, no conflicts, using the same port 80. And because Port Zero forwards at the network level rather than doing HTTP-specific tricks, this isn’t limited to web traffic; it works for any TCP protocol, so this can work for your backends as well as PostgreSQL, Redis, and other infrastructure.
Local and Cloud
Port Zero ships in two parts:
- Port Zero Local is free, open source (GPLv3), and does not require an account; everything stays on your local machine.
- Port Zero Cloud extends the same idea to your LAN and the internet, with tunnels like
some-other-branch--mytodoapp--yourname.tunnel.portzero.cloud, namespaced by username. This part requires a subscription, and unlike Local, tunnel metadata and traffic pass through Port Zero’s servers to make that possible.
Same mental model either way: name the thing, don’t number it.
Try it
On MacOS:
brew tap PortZeroNetwork/portzero
brew install portzero
On Linux:
curl -fsSL https://portzero.net/install.sh | sh
Or download the MSI for Windows.
Then run:
portzero demo
That spins up a tiny built-in page on a random port and opens http://hello.portzero.local once the tunnel is live.
Full docs at portzero.net/docs.