2026 · Solo · Open source
Dropport
Local dev servers at real hostnames over HTTPS, with no port numbers. A thin wrapper around Caddy.
Architecture
Mission
Local development turns into a pile of port numbers you have to remember, and anything that needs HTTPS — secure cookies, service workers, OAuth redirects, the Clipboard API — either does not work or wants a self-signed certificate you click through every time. OrbStack solves this for containers. dropport does the same for whatever you are already running: add the app once, and it answers at https://myapp.dp.local with no port and no warning, still there after a reboot.
For whom: anyone running more than two dev servers at once. Constraints: Caddy does the proxying and the certificates, so the tool owns only the registry, the generated config, the hosts lines and the privileged service; every escalation is announced before it runs; and nothing it writes may leave the machine worse than it found it.
Manifest
| Layer | Choice | Version | Why |
|---|---|---|---|
| app | Node CLI, no dependencies | 20+ | One global install with nothing to compile; the whole tool is a few files of plain JavaScript. |
| infra | Caddy | — | Reverse proxy and local CA. local_certs is the load-bearing line: without it Caddy asks Let’s Encrypt for a name that can never be validated, and fails on every start. |
| services | launchd daemon / systemd unit | — | The only part that needs root: something has to hold 80 and 443 across reboots. |
| services | Bonjour / Avahi publisher | — | Publishes .local names through the responder that already owns port 5353; runs as a user agent, not as root. |
| data | ~/.dropport/apps.json | — | The registry is the state. The Caddyfile and the /etc/hosts lines are generated from it and overwritten freely. |
Decisions
Caddy does the hard parts. It already runs a local certificate authority and reloads without dropping connections. dropport generates the Caddyfile from its own registry and never asks anyone to edit it by hand.
Root for three things, each announced. A launchd daemon or systemd unit to hold 80 and 443, one tagged line per app in /etc/hosts, and the local CA in the system trust store. Hosts edits are staged to a temp file and copied in, so a failure cannot leave half a file behind, and lines dropport did not write are never touched.
Publish .local names instead of pretending they are fast. A .local lookup goes to multicast DNS, not the hosts file, so an unpublished name waits out the full timeout: 5,009ms measured on macOS 15, against 3 to 9ms once something answers. dropport registers its names with the system responder — Bonjour on macOS, Avahi on Linux — rather than speaking mDNS itself, supervised as a user agent because publishing a name needs no privilege.
Bare .local names are refused. That namespace is where every printer, phone and Mac on the network answers. Claiming printer.local means competing with real hardware on some networks and not others, so bare names expand under .dp.local instead and --force is there for anyone who means it.
One registry file, everything else generated. The Caddyfile and the hosts lines are derived from it, and adding an app while the proxy is running reloads it in place.
Impact
- npm
- 0.2.6
- .local lookup
- 5,009ms → 3–9ms
- Runtime dependencies
- 0
- Platforms
- macOS · Linux · Windows
- Commits
- 19
- Weekly downloads
- 634
Written and published in an afternoon: about 1,200 lines of JavaScript with no runtime dependencies, MIT, on npm as dropport 0.2.2. It is what puts The Remote & Ledger on a clean HTTPS hostname on my own machine.
What broke
The doctor lied. An earlier fix stopped dropport reporting its own proxy as a port conflict, but it short-circuited: if the proxy was running, both 80 and 443 were assumed to be ours. That is false the moment something else holds 80, which is the normal case next to OrbStack or Docker Desktop. Adding an app then regenerated the config without the line that leaves port 80 alone, Caddy tried to bind a port it did not own and exited, launchd restarted it sixteen times, and doctor showed a green tick throughout.
Ownership is now asked per port: the admin API reports which addresses the proxy is actually bound to, and anything bound outside that list belongs to someone else. doctor also stopped conflating installed with running, because a daemon that cannot bind looks identical to a healthy one from the outside.
Ship log
- First commit
- Published to npm the same afternoon
- 0.2.1 — mDNS publishing, bare names under .dp.local, per-port ownership
- Windows: a logon task instead of a daemon, and certutil instead of sudo
- Now: 0.2.6 — 634 weekly downloads on npm, trusts the store the browser actually reads