Fronted mode and HTTPS
v1 In fronted mode, Caddy serves each app at
https://<slug>.<base-domain>, by default https://<slug>.localhost, with a certificate your machine trusts. It is
optional: turn it on with hallway setup and off with hallway teardown. For the
overview, see Direct and fronted mode.
Why Caddy
Section titled “Why Caddy”Hallway generates Caddy’s configuration; it never proxies app traffic itself. Caddy brings, out of the box:
- WebSocket proxying, transparently. Many apps need it: Vite and Astro hot reload, and apps with live state.
- Automatic TLS from a local certificate authority, and HTTP/2.
- Live config reloads through an admin API, without dropping connections. This is what lets the unprivileged daemon add an app without sudo.
Rejected alternatives: a Node proxy inside the daemon (WebSocket upgrades, TLS and HTTP/2 all reinvented), Traefik (built around container labels), nginx (manual config, no automatic local TLS) and localias (no wildcard subdomains, no persistence).
Hallway needs Caddy 2.7 or later. On macOS, hallway setup installs it with Homebrew if it is missing; on Linux,
put a release binary from caddyserver.com on your PATH (distribution packages
may be older, and may start a system Caddy on port 80), or point proxy.caddy in config.yaml at it.
How it fits together
Section titled “How it fits together”- Caddy runs as you, as a user-level job (
hallway.caddy: a LaunchAgent on macOS, a systemd user unit on Linux), listening on loopback only,127.0.0.1and[::1], on ports 4443 (HTTPS) and 4480 (HTTP), which need no privileges. Those areproxy.httpsPortandproxy.httpPortinconfig.yaml. - A fixed redirect sends loopback connections to ports 443 and 80 on to 4443 and 4480, so links need no port
number. It is a pf anchor on macOS and an nftables table on Linux, installed once by
hallway setup. - Its admin API is a private Unix socket in the data home,
state/caddy/admin.sock, readable and writable only by you (mode 0600). It never listens on TCP. - The daemon pushes the whole configuration to that socket when it starts and whenever an app file or
config.yamlchanges, and first writes it to~/.hallway/generated/caddy.json. Caddy starts from that same file, so routes are back after a reboot even before the daemon is.
Why it is built this way, and exactly what needs root, is on Setup, teardown and doctor.
Hostnames
Section titled “Hostnames”- The dashboard:
hallway.localhost(on a custom domain, its bare apex, e.g.myname.internal). It isdomain.dashboardinconfig.yaml. - Each app:
<slug>.<base>, plus any extra names in itsproxy.aliases. - The dashboard’s label (
hallway),wwwandapiare reserved slugs. - New apps get a hostname as soon as their YAML file exists: no sudo, no restart.
What a route looks like
Section titled “What a route looks like”For each app the daemon emits one route. It sends Caddy JSON; in Caddyfile notation, for readability:
{ # Every name gets a certificate from the local CA: needed for .test and # made-up TLDs, harmless for .localhost and .internal. local_certs}
slides.localhost { reverse_proxy 127.0.0.1:4321 { header_up Host {upstream_hostport} # rewrite Host by default }}
hallway.localhost { reverse_proxy 127.0.0.1:4255 # the dashboard keeps its Host}HTTP on port 80 is redirected to HTTPS by Hallway’s own route, which knows the public port. HTTP/3 is off.
Host-header rewriting
Section titled “Host-header rewriting”Caddy’s reverse_proxy preserves the browser’s Host header by default. But well-behaved local apps defend
against DNS rebinding by rejecting any Host that isn’t localhost: Local Review answers 403 forbidden host,
Vite has server.allowedHosts, and other dev servers have similar checks. Fronting them unmodified would break them
on day one.
So Hallway rewrites Host to the upstream by default: the app sees 127.0.0.1:4321, exactly as if you had
visited it directly. Caddy still adds X-Forwarded-Host, X-Forwarded-Proto and X-Forwarded-For, so an app that
needs its public name can read it.
For an app that builds absolute URLs from Host and doesn’t check it, opt out per app:
proxy: preserveHost: trueThe dashboard’s own route is the exception: the daemon gets hallway.localhost unchanged, and accepts it in fronted
mode.
Hot reload works through the proxy with Host rewritten: Vite’s HMR was checked with a real hot update and no
server.hmr.clientPort setting. (Astro runs on Vite but hasn’t been checked separately.)
Certificates
Section titled “Certificates”- Caddy’s local certificate authority lives in the data home (
state/caddy/data/).hallway setuptrusts its root certificate itself, with printed commands: in the System keychain on macOS (security add-trusted-cert), and in the system CA store on Linux (update-ca-certificatesorupdate-ca-trust), plus your own NSS database, which Chrome and Chromium read on Linux, without sudo. - Firefox uses its own certificate store: set
security.enterprise_roots.enabledtotrueinabout:config, or import the root by hand.hallway doctormentions this. - Caddy’s leaf certificates are short-lived and renewed automatically.
hallway teardownremoves the root from the trust store. The CA itself stays in the data home, unused.
Fronted mode without the redirect
Section titled “Fronted mode without the redirect”If you can’t or don’t want to install the redirect (in a container, say), set
proxy.publicPort to the same value as proxy.httpsPort:
proxy: publicPort: 4443hallway setup then installs no redirect, and links carry the port: https://slides.localhost:4443. Trusting the
certificate still needs sudo once.
Ports 80 and 443 may already be taken
Section titled “Ports 80 and 443 may already be taken”Container tools (Docker Desktop, Rancher Desktop, Lima, Colima and similar) often forward ports 80 and 443 from
their VM. hallway setup refuses to start while anything else holds 80, 443, or Caddy’s own ports, and says which
process (name, PID and command line) holds them; turning that tool’s forwarding of 80 and 443 off is usually the fix.
On macOS, where you can’t see root’s sockets, it also checks whether 443 answers with Hallway’s certificate.
hallway doctor reports the same.
When the daemon or an app is down
Section titled “When the daemon or an app is down”If the daemon isn’t running, Caddy serves a static fallback page (generated/fallback.html, rewritten by the daemon
on every change) at the dashboard’s hostname. It lists every app with a working link, so a dead dashboard still
answers “what do I have?”, and says how to restart it:
hallway service restartWhen an app isn’t answering, its hostname serves a page that says which app it is, where Caddy looked, and how to start it, instead of a bare proxy error.
App bind addresses
Section titled “App bind addresses”Many dev servers started with --host listen on 0.0.0.0, which exposes them to your local network. Behind a proxy
they don’t need to. Managed services get HOST=127.0.0.1; the dashboard marks any app listening on all interfaces as
exposed, and hallway doctor lists them.