Skip to content

Tailscale and Cloudflare Tunnel

works

This is how the project’s author actually runs it: a home server, reachable through Tailscale and a Cloudflare Tunnel, hosting demo environments. No inbound ports, no certificates to manage, nothing on the open internet.

For most people running this for themselves, it is a better answer than Caddy, and it is less work.

When the daemon has a tailnet address, it reports a second connection string alongside the local one:

Terminal window
hobby connect blog --json
{
"connectionString": "postgresql://...@127.0.0.1:5432/blog",
"tailnetConnectionString": "postgresql://...@your-box.tailnet.ts.net:5432/blog"
}

Use the tailnet string from any other machine on your tailnet. Postgres over a tailnet was researched and filed before it was relied on, in docs/proxy/research/2026-08-13-postgres-over-tailnet.md.

An architecture decision record for the two ingress lanes is expected and has not been filed at the time of writing. The implementation is on main.

A tunnel gives an HTTP service a public hostname with no inbound port. Point cloudflared at the HTTP router:

~/.cloudflared/config.yml
tunnel: <your-tunnel-id>
credentials-file: /home/you/.cloudflared/<your-tunnel-id>.json
ingress:
- hostname: app.example.com
service: http://127.0.0.1:7433
- service: http_status:404

7433 is the HTTP wake router. Requests arriving through the tunnel wake a sleeping app or worker exactly as they would arriving through Caddy: the router does not care what is in front of it.

For Studio, point a second hostname at the daemon’s port, 7432, and put a Cloudflare Access policy in front of it. Studio’s own credential is a real boundary, and an Access policy in front of it means an attacker has to get through both.

Studio binds 127.0.0.1 only, on purpose: it is a control plane that can create and destroy databases, so it does not put itself on a network by default. Its tailnet address will refuse a connection. tailscale serve is the way across, and it is the recommended one, because it terminates TLS with your tailnet’s own certificate and exposes nothing beyond the tailnet.

Terminal window
tailscale serve --bg 7432

Then open the URL that prints, which is your machine’s tailnet name over HTTPS. To find it:

Terminal window
tailscale status --json | grep DNSName

Older tailscale builds want the longer form:

Terminal window
tailscale serve https / http://127.0.0.1:7432

To check or undo it:

Terminal window
tailscale serve status
tailscale serve --bg --https=443 off

Do not reach for tailscale funnel here. Funnel publishes to the open internet, which is the opposite of the reason to use a tailnet, and Studio is the last surface in this project that should be there.

Without Tailscale, an SSH tunnel does the same job

Section titled “Without Tailscale, an SSH tunnel does the same job”

Nothing to install or configure on the box:

Terminal window
ssh -L 7432:127.0.0.1:7432 you@your-box

Then open http://127.0.0.1:7432 on your own machine. The tunnel lasts as long as the SSH session.

A tunnel or a tailnet removes an entire class of problem rather than mitigating it. There is no certificate store to persist, no rate limit to hit, no port scan to worry about, and no v0-alpha authentication surface exposed to the open internet. Reaching for it costs about ten minutes.