A diagram explaining zoxy, the app I built. On the left side the user (represented by the success kid meme) types hugo.z in their browser, then an arrow points to a box labelled 'zoxy proxies' and that points to another box labelled localhost:1313.
Zoxy is an app that lets you type hugo.z, next.z, jupyter.z, or kibana.z instead of remembering localhost:1313, localhost:3000, localhost:8888, or localhost:5601.

Here’s me going to the local version of this blog by typing http://hugo.z

A screenshot of Firefox with hugo.z in the URL showing the local version of this website

Convinced? Skip down to here to download and use it.

What is the point of this, you ask? Why save a couple seconds typing out a port number? the thing is, each trick saves a lot more than the 2s it takes to type out something longer: it keeps your mind from overheating and makes interacting with the computer less like talking to a screeching alien, and more like expressing your creative intent.

For example, I really liked learning from a coworker you can toggle between two git branches with git switch - (or git checkout - if you’re like me and still haven’t learned the new git command names), the - is a shortcut for the last branch you were on.

The problem: localhost:wxyz got tiring

Another example of cognitive friction is remembering localhost:1234-type port numbers. At work we had to remember localhost:4000 for our API server and localhost:3000 for the SPA frontend 1, then we added lots of smaller apps maintained by different teams: one on :3001, another on :3030. There were also new web dashboards to check like Kibana on :5601.

At work there are better ways to fix the port sprawl problem 2, but even at home this comes up with apps that spin up their own localhost servers like Hugo (:1313) or Jupyter Notebook (:8888). Sometimes I’ll leave Hugo open a while writing a blog post and forget which port it came from, even if it printed its port at startup. I don’t want to have to context-switch to the terminal or a README to find what port to use.

This seems like an easy problem to fix too. Shouldn’t you be able to do this already, maybe with the hosts file?

Can you fix this with a hosts file?

For those unfamiliar, the /etc/hosts file on Unix systems is a place you can “hardcode” DNS answers. Sometimes a website’s DNS will be down but their actuall app will still be up, so you can hardcode its IP into the hosts file and website.com will still work in your browser. You can also use it to block a website from your computer by adding eg 127.0.0.1 instagram.com , which will make all instagram requests go to localhost 😛.

The hosts file doesn’t help here because it only affects DNS resolution, that is it maps domain names to IPs, but not port numbers. You can add z 127.0.0.1 in there to alias localhost to a single letter, but you’d still have to type the port numbers, eg z:8888 into your browser. I really didn’t want to have to remember the port numbers.

Can you fix this with the .local or .lan domain?

On your local network, you can ssh, curl, and generally access other machines using their hostnames , like ping hostname.lan . Can we do something where you get eg jupyter.amedee.lan ?

Turns out you can’t. The way that hostnames get resolved into port numbers is called mDNS (afaict when you look up some-hostname.lan a DNS packet gets passed around the network until the right machine gets it), but since it’s still DNS it would only give you an IP and not let you add a port number. We’re going to need a server that proxies requests from port 80 to eg :3030 or :8080 based on the URL it’s given. 3

I wanted the .z tld because it’s really short to type and it feels cognitively “easy” to remember as the last letter in the alphabet. The solution needed to let me type next.z into my browser and have it redirect/proxy to localhost:3000.

I came up with a small 200LOC Go program called zoxy that does this.

Zoxy

Zoxy proxies from localhost:xyz to myapp.z URLs and comes with a pre-built list of popular dev servers ports like webpack.z->:8080 and jupyter.z:8888.

On Mac4 you can install it with:

brew install amedeedaboville/tap/zoxy

# Tell your computer to use zoxy as the DNS resolver for .z
sudo tee `127.0.0.1` >> /etc/resolvers/z

brew services start zoxy

Then you can go to any of the default configured apps by browsing to http://name.z

Here’s another example going to next.z :

A screenshot of Firefox with next.z in the URL bar, showing the Nextjs welcome screen.

I quickly made a list of popular apps/local dev servers and their ports here. If you see a popular app that’s missing that you think should be added, please file an issue/submit a PR!

How do I add my own mappings?

Say you want want to map your own custom app from localhost:7000 to myapp.z.

You can add your own mappings to ~/Library/Application Support/zoxy/config.json. (You can print this path with zoxy config path):

{
	"ports": {
		"myapp": 7000,
		"app1": 3000,
		"app2": 3001,
		"app3": 3030,
		"kibana": 5892
		"backoffice-node": 3333,
	},
	"aliases": {
        "b": "backoffice-node"
	}
}

Personally, this helps a lot with all the individual apps at my work.

I also added a level of aliasing to get shorter names: if the name of an app like backoffice-node.z feels long, you can also add an alias like b.z that will map to the same port.

Just one note that people have given is that the browser often “helpfully” searches the .z URL instead of going to it. To get around this in the majority of browsers you can add a trailing / to the URL, like website.z/ and Chrome/FF/Safari will go directly to that URL.

Try it out and let me know how it works! The majority of it was written on an airplane in Go because it was the only language that had offline/preinstalled docs (go doc) off the top of my head.5 Go is also the perfect language to make little servers like this because of the built-in net/http package in its standard library, but I hadn’t used it in a few years and have completely forgotten the idioms. If you work in Go, I’d appreciate some feedback on the code.

How does it work?

When you type jupyter.z, your browser uses DNS to look up the IP address it should connect to.

  1. zoxy hosts the simplest possible DNS server on port 53, which replies to every DNS single request with:
whateveryouaskedfor.z A 127.0.0.1

With that answer in hand, your browser makes a request to 127.0.0.1 on port 80 with the Host header set to what’s in the URL bar:

GET / HTTP/1.1
Host: jupyter.z
Accept-Encoding: gzip, deflate
... # a bunch of stuff
  1. zoxy hosts a server on port 80 so it also gets this request. It reads the Host header, and sees if it has a matching port mapping for jupyter.z.

It also does a couple tricks like mapping eight.z to :8000, and 1234.z to :1234 in case you know the port but don’t want to type out all 9 characters that spell out localhost.

Once zoxy has figured out what port to map your request to, it changes the Host header to have the correct port, like localhost:8888 , and then proxies the request there as well with a Go ReverseProxy instance.

What about TLS/other features?

For now zoxy is HTTP only, but I plan on adding TLS on port 443 later. I know some people use local proxies to connect to their apps with HTTPS and test out browser features that require a Secure Context (which requires a https:// scheme), but I’ve never done it before.

At first I thought it might be cool to embed Caddy Server to do the reverse proxying. Caddy has TLS out of the box and a ton more features, but I’m going to hold off for now unless some other cool use cases come up, because I like how small the code is.

My next feature is going to be implementing file watching to avoid needing to restart the server when you edit the config file.

Why not use puma-dev?

After building this a coworker showed me puma-dev, which is the closest thing to what I wanted! It’s also written in Go, so I’l be going and reading that code to improve zoxy. Although puma-dev has more features, it also has more configuration and is more Rails-centered, which makes me prefer my little tool that works out of the box.

Conclusion

Try it out, read the source, recommend new port maps, tell me what you think! I like this little tool and I like the feeling of using something I built in my every day.


  1. Even :3000/:4000 was confusing at first, and I know for a fact at least a days’ worth of time was lost because it wasn’t clear to new employees which port to open to see their local version of the website (the frontend one). We eventually added a log line saying “Open your browser to localhost:3000” in the dev script. Good documentation pays for itself pretty fast. ↩︎

  2. The “correct” answer to this app sprawl is to use something like docker-compose/minikube. This has many benefits, chief of which is the ability to spin up the whole system with one command (instead of each app being a delicate pet), but on top of that it creates a local network and gives each app its own DNS name like kibana.mynamespace.k8s so you don’t have to remember port numbers. ↩︎

  3. At first I thought this would be port forwarding , but actually what we’re doing is a HTTP reverse proxy because our code needs to read the HTTP Host header to decide which port to use, and port forwarding is typically a lower-level affair rewriting individual IP packets. ↩︎

  4. I can only vouch for Mac installation and use atm, but plan on doing Linux shortly. The app compiles and runs on Linux, but the packaging situation is more complicated and running it requires dnsmasq. ↩︎

  5. Apparently the equivalent Rust invocation is rustup docs --book, which definitely isn’t as memorable. ↩︎


comments powered by Disqus