HTTPS and remote access
Never expose Exhale’s plain HTTP port to the internet. Over http://, passwords and the session cookie travel unencrypted. On your own network that is an accepted risk; on the internet it is not.
You may not need any of this. Telegram reaches Exhale through outgoing connections, so reminders and buttons work from anywhere with nothing exposed. Remote access is about opening the web page away from home, and about cloud assistants (Claude.ai, ChatGPT, Grok), which need a public address: see Claude.ai, ChatGPT and Grok.
| You want | Use |
|---|---|
| The web from your own devices, anywhere | Tailscale |
A public https:// address |
A reverse proxy: Caddy or nginx |
| Claude.ai, ChatGPT or Grok to reach Exhale | A public address (a reverse proxy, or tailscale funnel), then Claude.ai, ChatGPT and Grok |
What Exhale needs from whatever sits in front
Section titled “What Exhale needs from whatever sits in front”Exhale itself speaks plain HTTP. Something in front adds HTTPS, and four things must be right:
- Tell Exhale it is behind HTTPS:
EXHALE_SECURE_COOKIES=true(or--secure-cookies). The session cookie is then only sent over HTTPS. Exhale does not readX-Forwarded-Proto; this setting is how it knows. Do not set it while you still use plainhttp://: the browser would not send the cookie and nobody could sign in. - The proxy must send
X-Forwarded-For. Exhale treats a request as coming from the server’s own machine when it arrives from127.0.0.1without that header. Such requests skip the setup code in the first-run wizard. A proxy on the same machine that omits the header makes every visitor look local. Caddy and Tailscale Serve send it by default; nginx needs one line. - The proxy must pass the
Hostheader unchanged. Exhale rejects cross-site writes by comparing the browser’sOriginwithHost. - No buffering. The web page receives live updates as a stream (
/api/v1/events), and so do AI clients on/mcp.
Also:
- Keep Exhale listening on
127.0.0.1:8484(the default), so only the proxy can reach it. In Docker, publish the port as127.0.0.1:8484:8484. - Never use
--trust-localon such a server. - Exhale sees the proxy as the client, so its limit on sign-in attempts per address is shared by all visitors. The limit per email still applies. Reading the real address from a trusted proxy is planned.
Tailscale: the simple private option
Section titled “Tailscale: the simple private option”Tailscale puts your devices in a private network. Nothing is exposed to the internet and there are no certificates to manage.
Install Tailscale on the server and on your phone and laptop. Then, on the server:
tailscale serve --bg 8484Exhale is now at https://<machine>.<your-tailnet>.ts.net, with a real certificate, for your devices only. Set EXHALE_SECURE_COOKIES=true and restart Exhale.
tailscale funnel is different: it publishes the address to the whole internet. Treat it like a public reverse proxy and read Before you go public.
Caddy gets and renews the certificate by itself. Point your domain at the server, open ports 80 and 443, and use this Caddyfile:
exhale.example.com { reverse_proxy 127.0.0.1:8484}Caddy passes Host, adds X-Forwarded-For and does not buffer streams. Nothing else is needed.
server { listen 443 ssl; http2 on; server_name exhale.example.com;
ssl_certificate /etc/letsencrypt/live/exhale.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/exhale.example.com/privkey.pem;
location / { proxy_pass http://127.0.0.1:8484; proxy_http_version 1.1;
# Required: tells Exhale the request did not start on this machine. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Required: Exhale compares the browser's Origin with Host. # $http_host keeps the port if you serve on one other than 443. proxy_set_header Host $http_host;
# Live updates (/api/v1/events) and MCP (/mcp) are streams. proxy_buffering off; proxy_read_timeout 1h; }}
server { listen 80; server_name exhale.example.com; return 301 https://$host$request_uri;}Get the certificate with certbot or your usual tool.
Check it
Section titled “Check it”After setting it up, from any computer:
curl -s https://exhale.example.com/healthz # okcurl -s https://exhale.example.com/api/v1/auth/sessionOn an instance that has no account yet, the second answer must contain "setupNeedsCode":true. If it says false, the proxy is not sending X-Forwarded-For: fix that before going further.
Then sign in with a browser and look at the cookie exhale_session: it must be marked Secure.
Before you go public
Section titled “Before you go public”- Use long, unique passwords. Exhale requires at least 10 characters and slows down repeated sign-in attempts. Turn on two-factor sign-in (Settings → You), or sign in through your identity provider (single sign-on).
- If only your household or team uses it, prefer Tailscale or a VPN to a public address.
- Keep Exhale up to date.