The cherrypicker logo: a cherry driving a cherry picker

Names for
your ports

Stable named .localhost URLs for local dev servers, on the BEAM. No DNS setup, no hosts-file edits, no root certificate and no Node.

API reference GitHub

escriptmix escript.install hex cherrypicker
mix.exs, client only{:cherrypicker, "~> 0.1"}

Port numbers wander between runs and read like noise; names do not. cherrypicker is a tiny reverse proxy for your own machine: tell it mysite lives on port 4000 and http://mysite.localhost works in every browser.

$ cherrypicker start
proxy up — routes serve at http://<name>.localhost (Ctrl-C to stop)
$ cherrypicker route docs 8080
http://docs.localhost

How it works

Three parts, one idea: apps register themselves, the proxy only proxies.

~/.cherrypicker/daemon.json (state file: the bound proxy port)
▲ write on start / remove on stop ▼ read by clients
┌────────────────────── Cherrypicker.Daemon ─────────────────────┐
│ Routes (GenServer + ETS) Finch pool Bandit :80/:N │
│ │ │
│ Host: cherrypicker.localhost → Control (REST, JSON) │
│ Host: <name>.localhost → Proxy → 127.0.0.1:<port> │
└────────────────────────────────────────────────────────────────┘
▲ PUT /routes/:name ▲ GET http://mysite.localhost
│ CLI verbs / library client │ browser / curl / agent

1. Names resolve for free. Every hostname ending in .localhost already resolves to your loopback address on Windows, macOS and systemd Linux, and browsers treat it as a secure context. cherrypicker invents no DNS; it simply listens on one port and reads the Host header of each request. A request for docs.localhost looks up docs in the route table and streams the request to 127.0.0.1:8080. The reserved name cherrypicker.localhost is the daemon's own control API on the same listener, so there is exactly one port to think about.

2. The daemon is found through a state file. On start the daemon writes its bound port to ~/.cherrypicker/daemon.json and removes it on shutdown. Clients (the CLI verbs, the Elixir library, or anything that can speak HTTP) read that file and talk to the control API. No daemon running costs a client exactly one failed connect, and it falls back to plain port URLs. Nothing breaks; you just lose the pretty names until the daemon is back.

3. Apps register, nothing gets wrapped. Tools like portless wrap your dev server as a child process and inject port flags, which means guessing every framework's flag taxonomy and inheriting every process-management quirk. cherrypicker refuses all of that: your app starts however it starts, then says where it is with cherrypicker route mysite 4000, one HTTP call to the control API, or a library call from inside the app itself. The proxy streams responses chunk by chunk, so server-sent events and live-reload connections stay live through the named URL.

The default port is 80, which is what makes the URLs bare. Where 80 needs privileges or is taken, start with --port N and URLs carry the suffix once, permanently: http://docs.localhost:7777. Everything is loopback only; nothing is reachable from off your machine.

The whole surface is five verbs and four HTTP endpoints: the API reference covers every one, with the JSON envelopes agents use.

From Elixir

Any BEAM app can register itself with the zero-cost client, which uses only the standard library:

case Cherrypicker.register("mysite", port) do
{:ok, url} -> IO.puts("also at " <> url)
{:error, :no_daemon} -> :ok
end

Cherry sites get this built in as cherry serve --name mysite.

Read on

  • API reference: CLI verbs, control API, and the Elixir library.
  • Design: principles, architecture and the TLS/WebSocket roadmap.
  • Source on GitHub, MIT or Apache-2.0 at your option.