Route apps over HTTPS
devhost routes every service with a host through one shared Caddy server, which serves it over HTTPS on that hostname.
Prepare Caddy
Section titled “Prepare Caddy”Do this once per machine:
devhost caddy download # skip if `caddy` is already on your PATHdevhost caddy privileged-ports # Linux only: lets Caddy use port 443 without rootdevhost caddy start- devhost uses the downloaded Caddy when it exists, and the
caddyon yourPATHotherwise. It never downloads Caddy on its own. devhost caddy privileged-portsdownloads Caddy if needed and runssudo setcap cap_net_bind_service=+epon it. Run it again if the binary is replaced.- Caddy keeps running until
devhost caddy stop. Start it before your first stack; every stack on the machine then uses it.
Trust the certificates
Section titled “Trust the certificates”Caddy signs its certificates with its own local certificate authority. The first time devhost starts Caddy, it installs that authority into your system trust store, which can ask for your password (the prompt starts with caddy | ). Starting a stack or changing routes never asks.
devhost cannot ask without a terminal or under AGENT=1, and the installation can be declined or fail. In each case devhost prints a warning instead, Caddy keeps running, and browsers reject its certificates until you run:
devhost caddy trustTo trust a dev machine’s certificates from another Mac, run this on the Mac:
devhost caddy trust-remote devboxIt connects with ssh, runs devhost caddy print-root-cert on devbox, prints the certificate’s SHA-256 fingerprint, and installs it into the macOS System keychain. devbox needs devhost on its PATH and must have started Caddy at least once.
Route a service
Section titled “Route a service”Give a service a host and the port it listens on:
[services.web]command = ["bun", "run", "dev"]port = 3000host = "app.localhost"https://app.localhost now reaches 127.0.0.1:3000. Startup prints every URL, such as web (primary): https://app.localhost.
Share a hostname between services
Section titled “Share a hostname between services”Services in one manifest can split a hostname by path. A request goes to the most specific matching path:
[services.web]command = ["bun", "run", "web:dev"]port = 3000host = "app.localhost"
[services.api]command = ["bun", "run", "api:dev"]port = 4000host = "app.localhost"path = "/api/*"path defaults to /. Two different stacks cannot share a hostname.
Multiple hostnames for one service
Section titled “Multiple hostnames for one service”Set host to an array to route several hostnames to one service:
[services.web]command = ["bun", "run", "dev"]port = 3000host = ["app.localhost", "alias.localhost"]- Every hostname uses the same port and
path. - The first hostname is the service’s main one: it supplies
DEVHOST_HOST,{{ services.web.host }}, and the link in devtools. - If any hostname is already taken, the stack does not start.
Development servers that require a local origin
Section titled “Development servers that require a local origin”Some dev servers, such as Bun’s HTML/HMR server, reject requests addressed to a public hostname. Set proxyLocalOrigin on that service:
[services.web]command = ["bun", "src/index.ts"]port = "auto"host = "app.localhost"proxyLocalOrigin = trueThe server then sees its own local address, such as 127.0.0.1:41234, in the Host header, and in Origin when the browser’s origin matches the public hostname. This applies to pages, assets, sub-paths, and WebSockets.
- Requests with a different
Origin, an opaque one, or more than one are rejected with403. X-Forwarded-Hostkeeps the public hostname.- The local address comes from the service’s
bindHostandport, so the server must listen exactly there. devhost does not discover a port the server picks for itself. - Leave it off for services that accept cross-origin API requests.
Hostnames and DNS
Section titled “Hostnames and DNS”devhost routes hostnames; it does not create DNS records. A hostname must already resolve to the machine running devhost.
.localhostnames, such asapp.localhostandapi.app.localhost, resolve to the local machine everywhere and need no setup. Use them unless you need a real domain.- Other names need
A/AAAArecords pointing at127.0.0.1/::1, a wildcard DNS record, or/etc/hostsentries (exact names only). - Other devices, such as a phone, cannot use
.localhost, which always means the device itself. Route a hostname that resolves to the dev machine’s network address. Caddy listens on all interfaces by default, and the device has to trust Caddy’s certificate authority.
The hostname selected by [caddy].stackDirectoryHost in the shared config.toml (default devhost.localhost) is reserved for the running-stack directory, so no service can use it. That page accepts local clients by default; [caddy].stackDirectoryAllowRemote = true in config.toml enables reading it from other devices.
An unknown hostname gets a 404 page listing every hostname devhost routes.
Several stacks, one Caddy
Section titled “Several stacks, one Caddy”Every project can run its own stack at once. They share Caddy under these rules:
- A hostname belongs to one stack. A stack that claims a hostname another live stack holds fails to start, naming that stack.
- A fixed port belongs to one stack. When devhost can identify the process holding a port, the error shows its command line and manifest.
port = "auto"picks a free port and retries a few times when another process takes it first. Nothing coordinates automatic ports between processes, so a collision is unlikely but possible.
Stopping a stack removes only its routes. Caddy keeps running with its settings, even with no routes left.
Caddy settings
Section titled “Caddy settings”Caddy’s ports, bind address, admin address, and plain-HTTP listener are machine settings in config.toml, not in a manifest. See Proxy ports and listeners.
Each state directory has its own Caddy, so setting DEVHOST_STATE_DIR gives you a separate proxy.