Friendly HTTPS names for local apps.
cd my-app
doorman # runs your package.json "dev" script
# → https://my-app.localhostDoorman gives every dev server a stable, named HTTPS URL instead of a port number. It
picks a free port, tells your framework to use it, and routes
https://<name>.localhost (or your own domain) to it through a local proxy. A native
macOS companion shows routes, live traffic, and domains.
The educational documentation site lives in docs/, built with
Astro Starlight and the Material Design 3 theme. It covers first-time setup, project
integration, team onboarding, monorepos, and everyday workflows.
cd docs
npm ci
npm run devcurl -fsSL https://getdoorman.dev/install.sh | shThe installer downloads the latest universal (Apple Silicon and Intel) release, checks
its SHA-256, installs Doorman.app in /Applications, and links the doorman CLI into
~/.local/bin, adding it to your shell's PATH if needed. Then:
doorman setup # once: trust HTTPS and finish custom domains
doorman service install # optional: start Doorman at loginInstaller options: DOORMAN_VERSION=0.2.0 pins a release, DOORMAN_INSTALL_DIR and
DOORMAN_BIN_DIR change where things go, DOORMAN_NO_MODIFY_PATH=1 leaves your shell
profile alone, and DOORMAN_NO_LAUNCH=1 skips opening the app.
cargo run -p doorman-cli -- <command> # or doorman-daemon / doorman-desktop
cargo install --path crates/doorman-cli --locked && cargo install --path crates/doorman-daemon --locked
cargo install cargo-bundle --version 0.11.0 --locked # matches release CI
scripts/package-macos.sh # outputs/Doorman.app + Doorman-macos.zip (needs cargo-bundle)
DOORMAN_UNIVERSAL=1 scripts/package-macos.sh # both architectures, as releases are builtTo release, bump version in Cargo.toml and push a matching tag (git tag v0.2.0 && git push origin v0.2.0). The release workflow tests, builds the universal app, and publishes
Doorman-macos.zip, its checksum, and install.sh to GitHub Releases.
doorman # package.json "dev" script, name inferred
doorman run next dev # any command
doorman run --name api pnpm start
doorman api pnpm start # shorthand for the line above
DOORMAN=0 doorman # bypass Doorman entirely (e.g. in CI)doorman run:
- Names the route from
doorman.json, the package.json"doorman"key orname(scope stripped), the git root, or the folder. - Picks a free port in 4000–4999 and passes
PORT,HOST=127.0.0.1,DOORMAN_URL,DOORMAN_NAME, andNODE_EXTRA_CA_CERTSto the command. - Adds port flags for servers that ignore
PORT(Vite, Astro, Angular, React Router, Wrangler, webpack-dev-server, Expo, React Native, Storybook), including when they sit behindnpm run/pnpm/yarn/bunscripts. Compound scripts (&&, pipes, env prefixes) are left untouched. - Handles git worktrees: a linked worktree on branch
fix-uigetshttps://fix-ui.my-app.localhostand a free port, even whenappPortis pinned, so it runs alongside the main checkout. - Cleans up: the route disappears when the command exits, and Ctrl-C or SIGTERM reaches every process the command started.
Optional doorman.json in the project root:
{ "name": "shop", "script": "dev", "appPort": 3000 }Routes started by doorman run remember their repository, branch, and worktree. The
desktop app groups routes by repository with a branch badge per checkout, lists the
other checkouts running the same app, and offers Open in editor, Reveal in Finder,
and Stop (which ends the dev server and its route). Traffic rows show the branch
too, and doorman list prints it next to each route.
For servers Doorman doesn't start (Docker containers, other tools):
doorman add db-admin 8080 # alias: doorman alias
doorman add api.shop 4000 # nested names work
doorman list
doorman get shop # prints the URL
doorman remove db-admin
doorman wildcard on # tenant.shop.localhost falls back to shopFixed routes and settings persist across restarts; doorman run routes don't.
Share an already-running route through an installed Cloudflare or ngrok client:
doorman tunnel shop
doorman tunnel shop --provider ngrok
doorman tunnel shop --provider cloudflareIn the macOS app, select a route and use Public tunnel in its details. Choose
Auto, Cloudflare, or ngrok, then Start public tunnel. The app runs the provider
directly—no Terminal window—and shows status, the public URL with Copy/Open actions,
and the latest 100 log lines. Choose View live logs to open the full-width
Tunnel logs tab, which follows new output and offers Pause/Resume and Copy logs.
Pausing freezes only the display; the tunnel keeps running and collecting output.
Use Stop tunnel to end it without stopping the app.
App-owned tunnels stop on quit and when their route is stopped, removed, or replaced.
Discovery checks PATH, /opt/homebrew/bin, /usr/local/bin, and ~/.local/bin,
including when Doorman is launched from Finder. Provider logs stay in memory.
Auto-detection checks PATH for cloudflared, then ngrok. Cloudflare quick tunnels
need no account; ngrok needs an account and its authtoken configured with the ngrok
CLI. Doorman does not install clients or store credentials. Provider startup errors
are shown directly; an authentication failure does not silently switch providers.
With the CLI, the provider prints the public URL and stays in the foreground. Ctrl-C stops the tunnel without stopping your app. Anyone with the URL can access the app: only share apps and data you intend to expose, and use app-level authentication where needed. Nothing is exposed unless you explicitly run this command.
Tunnels connect directly to the selected app's loopback HTTP port, rewriting the
Host header to its .localhost name. They do not expose the whole Doorman proxy or
appear in Doorman's traffic inspector. The port is selected at startup: stop the
tunnel when stopping/restarting the app to avoid exposing another app that reuses
that port. Apps that generate absolute local URLs or use a fixed HMR origin may
need their own public-URL configuration.
The daemon creates a local certificate authority on first start and issues a
certificate for each host on demand (HTTP/2, 397-day leaves). doorman trust (also
part of doorman setup) adds it to your login keychain; the desktop app offers the same
button. Browsers are redirected from http:// to https:// once it's trusted. Firefox
uses its own store unless security.enterprise_roots.enabled is on.
Doorman listens on 443 and 80 when they're free. macOS lets ordinary users bind low
ports on the wildcard address, so no root is involved; connections from other machines
are dropped. If another process holds a port, Doorman falls back to 3443 / 3210 and URLs
include the port. doorman restart picks 443 back up once it's free.
Every route answers on .localhost, which macOS resolves with no setup. Add your own
suffix to mirror production hostnames:
doorman domain add test # → https://shop.test (asks for your password once)
doorman domain add dev.acme.com # a subdomain you own
doorman domain list
doorman domain remove test # also deletes its resolver file
doorman domain setup # finish any domain that doesn't resolve yetdomain add writes /etc/resolver/<domain> for you right away. Pass --no-setup (or
run non-interactively) to skip that and run doorman domain setup later.
The daemon runs a DNS responder on 127.0.0.1:53535 that answers loopback (A and AAAA)
for your domains, wildcards included; /etc/resolver/<domain> tells macOS to ask it.
Doorman refuses .local (Bonjour) and bare public TLDs like .dev or .com, which
would hijack real sites.
doorman traffic --route shop --limit 50
doorman traffic --json
doorman clear-trafficThe inspector keeps the newest 200 requests in memory and records only method, host, path, status, timing, and sizes. Headers, cookies, and bodies are never captured. The desktop app can replay a request and copy it as cURL.
Browsers that reach an app that isn't listening, or a name with no route, get a page
with the fix and reload once things work. A request that loops back through Doorman
(for example a dev proxy without changeOrigin) gets a 508 explaining how to fix it.
WebSockets (including HMR) are relayed over HTTP/1.1 and HTTPS.
doorman start | stop | restart
doorman service install | uninstall | status
doorman doctor # ports, trust, DNS, domains, every route
doorman clean # removes state, keychain trust, and the login service| Variable | Default | |
|---|---|---|
DOORMAN_HTTPS_PORT |
443, else 3443 | HTTPS proxy port |
DOORMAN_HTTP_PORT |
80, else 3210 | HTTP proxy port (DOORMAN_PROXY_PORT still works) |
DOORMAN_HTTPS |
on | 0 disables HTTPS |
DOORMAN_DNS_PORT |
53535 | DNS responder |
DOORMAN_DAEMON_PATH |
auto | daemon binary the CLI and app start |
DOORMAN |
0 makes doorman run a plain passthrough |
doorman-core: protocol, validation, and shared typesdoorman-daemon: control socket, HTTP/HTTPS proxy, certificate authority, DNS responderdoorman-cli:doorman run, route management, setup, and diagnosticsdoorman-desktop: Slint companion app
- The daemon never runs as root. Low ports are bound on the wildcard address, and every non-loopback connection is dropped before any bytes are read.
- The control socket and CA key live in a user-only state directory (
0700, key0600). - Certificates are issued only for
.localhostand your configured domains. - The proxy forwards only to registered ports on
127.0.0.1/::1. - The DNS responder binds to loopback and answers only for configured domains.
- Admin rights are used only for resolver files (
doorman domain add/remove/setup, with a visible sudo prompt) and when you approve the keychain prompt fromdoorman trust./etc/hostsis never touched. - Traffic capture is metadata-only, in memory, and bounded to 200 entries.
